Ctrl K
ComunidadGuías…

feature-map/sessions.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Sessions

A session is one conversation with an agent. Users manage sessions from the
sidebar list and from the session header menu: they pin, rename, archive,
unarchive, and delete them, fork or clone them into new sessions, and get them
running again when their runner or host goes away. Most actions are offered in
more than one place (the sidebar row, the right-click menu, bulk selection, and
the header menu), and each place is a separate entry point.

## Sub-features

- `siblings`: **Add nested agent** creates a private, fresh conversation in the
  same sidebar family. Its composer follows the server's Add Agent destination
  policy and offers only the clicked member's ephemeral host among existing
  managed hosts, preselected. Siblings have one
  indentation level with curved connectors and are absent from the Agents panel.
  Archive/delete promotes the oldest active member; compute survives while any
  session remains bound. The representative pins/moves the family; individual
  pins appear only in Pinned and return to their nest when unpinned. Existing
  sessions cannot join a family; ordinary forks remain standalone and may reuse the source host.

- `session-grid`: behind `grid_sessions_display_ui`, drag sidebar sessions into a
  conversation to split left/right or top/bottom with a hover preview. Up to four
  independent chats fit in two rows and two columns. Grids live at `/grid/<id>`
  and are saved only in this browser, scoped to server and user.
- `pin`: pinned sessions move to their own section and back.
- `pin-undo`: unpinning shows an Undo toast that re-pins the session into its
  old Pinned slot. Dragging a pinned row into a folder unpins it without one.
- `pin-reorder`: drag a pinned row onto another pinned row to change the Pinned
  order, which persists across reloads; dropping an unpinned row onto a pinned
  row pins it above or below that row, by the half it's released over, and
  releasing it just below the last pin pins it at the bottom.
- `custom-order`: with `sidebar_grouping_ui` (on by default), drop one of your sessions onto
  another row of the same list, project or group (or use Move up/down) to switch
  Ordering to Custom. Positions are saved on this device and new sessions appear
  above them; a drop on another project's row still files the session there.
- `rename`: from the row, the header menu, the header title, or Cmd/Ctrl+Alt+R
  on desktop; long titles are limited.
- `title-instructions`: Settings → General → Sessions → **Title instructions**
  saves personal guidance (up to 4,000 characters) for automatic titles of
  sessions you own. The server adds it after any operator
  `session_title_instructions`; Clear removes only the personal guidance, so
  titles fall back to the operator setting or, without one, the default prompt.
- `search`: the sidebar's Search or ⌘K / Ctrl+K opens the command palette.
  Sessions whose title matches list under **Sessions**, then sessions matching
  only in chat content under **In messages** with a snippet; each group has its
  own Show more. Archived sessions match too, marked **Archived**; the recent
  sessions shown before typing leave them out.
- `archive`: archived sessions leave the main list and appear in the archived
  view, which can be filtered by project and paged. On a server with worktree
  cleanup, archiving a session that has a git worktree first asks whether to
  also delete the worktree (the branch is kept); Remember my choice saves the
  answer, which Settings → Git can change later. Archiving a session whose agent
  is still working, waiting on an approval, or running background work first
  asks to confirm, since archiving stops it; Cancel or Escape keeps the session.
- `row-quick-actions`: sidebar rows show hover Pin and Archive buttons beside the
  "…" menu. Settings → Appearance → Sidebar → Show Pin and Archive buttons on
  session rows (on by default) turns them off, leaving both actions in the row
  menu and right-click menu.
- `stop`: Stop session on a hosted parent ends its runner, including side chats
  and sub-agents sharing that runner. Conversation histories are kept.
- `side-chat-lifecycle`: generic side chats reuse their parent's live runner;
  closing one stops only that chat. Starting one from a stopped generic hosted
  parent relaunches the parent first, then shares its replacement runner. An
  existing side chat whose shared runner exited does the same on its next
  message or Resume. Only the session's owner can start side chats. A side
  chat started on one device appears as an existing side-chat tab on the
  owner's other devices. Selection and closing tabs stay local.
- `side-chat-settings`: choose a supported model or effort in the side-chat composer;
  selections apply to that chat's next turn independently of its parent, in the
  desktop panel and mobile drawer. Generic side chats allow selection before or
  after the first message. Native Codex controls appear after child creation and
  require a runner that supports side-chat settings.
- `side-chat-quotes`: Ask in side chat adds selected parent text to the visible,
  writable side chat; Reply on side-chat text quotes it in that chat. Both preserve
  interleaved comments and existing attachments; removing a quote keeps its comments.
  Live side chats accept pasted images in every reply text field.
- `unarchive`: offered on archived rows, in bulk selection, and in the header
  menu of an archived session. An archived session opened from ⌘K search or a
  link also gets its row on the live sidebar views while it is open, outside
  Pinned and project folders, so you can unarchive it from that row. A shared
  session opened from search lists on All and Shared, without Unarchive unless
  you own it. A shared session opened by direct link gets no row unless you
  have owner-level access, because the session response carries no owner.
- `delete`: confirmed, then removed from the list and the server.
- `export`: Export in the header menu saves the transcript as a local file.
- `unread`: Mark as unread flags a session to revisit and survives a reload,
  including for the session being viewed. Its dot appears once the turn is
  idle; approval and error indicators can take precedence. A row showing the
  unread dot offers Mark as read instead. Bulk selection also offers these actions.
  Behind `automations_v2`, an automation's detail page shows the same unread state
  on each run that started a session: a run that succeeds while the page is open
  is marked unread, and opening its session from the run row clears it there and
  in the sidebar.
- `project`: Add to project (Move session once filed) puts a session in a
  project folder from the row menu, the header menu, or the header's folder tag.
  Agents file sessions the same way with `sys_session_update` (see
  [session orchestration](session-orchestration.md)).
- `project-rename`: the project menu opens a dialog with the name selected;
  typing replaces it without losing characters as the menu closes.
- `project-show-more`: an expanded project folder lists its five newest
  sessions; each Show more slides five more in until none remain. Re-expanding
  starts at five again, the open session stays listed once the sidebar has
  loaded it, and Show more carries the badge of a hidden session that needs
  attention or is unread. Filing a session into the folder leaves its listed
  rows as they are, so an older one lands behind Show more. Settings →
  Appearance → Sidebar sets the count per Show more (5 by default, 1–50, per
  device); turning off "Collapse project sessions behind "Show more"" lists
  every loaded session and loads more as the sidebar scrolls, with no Show more
  row.
- `bulk-actions`: select several rows, then mark as read or unread, archive,
  unarchive, or delete them.
- `fork`: fork the whole session or from a message; the fork keeps images and
  their files, elapsed "worked for" time, and can switch agent or host.
  For a native target, the fork dialog also offers model, reasoning effort,
  and permission mode pickers. A long model list from several creators (Pi)
  shows creator headings; it has no search or collapsible groups.
  Closing the dialog mid-clone keeps the clone running: a toast reports it
  (with **Open** on success), and reopening the dialog shows it again.
  Dismissing that toast stops tracking the clone, so a new one can start.
- `fork-custom-agent`: switch to one of your custom agents (installed, imported,
  or discovered from an existing session), as well as to a built-in agent; the
  fork uses the chosen agent.
- `fork-sandbox-repositories`: edit the inherited repository list before cloning
  onto a fresh sandbox. Add repositories from connected GitHub search or by URL,
  remove repositories, and choose each branch independently. Providers that
  support multiple repositories allow several; switching to a single-repository
  provider keeps the selection visible and asks you to remove the extras.
  Each repository starts on its current branch if pushed, otherwise its original
  launch branch. A quiet note explains an unpushed branch's fallback; unavailable
  source workspaces retain their original selections.
- `fork-access`: require read access to the source and, for a custom target,
  its owning session. The caller owns the fork; source grants are not copied.
- `unsupported-sandbox-actions`: Databricks Sandbox and Arclet host-switch
  controls stay visible but disabled, with hover and keyboard explanations.
  Their forms cannot submit. Fork is not restricted for these sessions.

- `clone`: copy a session into a new workspace, including a typed `~` path.
  Participating sandbox providers require a [profile selection](sandbox-profiles.md)
  before submission, including branch clones.
- `reconnect`: a stopped or stranded session shows a reconnect affordance and a
  dialog with the command to run; the desktop app reconnects a local host
  itself, and starts an Arca host recognized by its saved ID or default name
  from the same action (the desktop's Arca console still asks before running).
  A failed attempt opens the dialog for retry; so does an Arca connect after
  which the server still lists the session's host offline (usually because the
  host came back under a new ID), with **Switch host** to move the session.
  States: reconnecting (spinner), reconnect failed (retry), host offline.
- `workspace-recovery`: a missing-workspace error offers **Continue in another
  directory…**. Choose an existing host directory, optionally with a new git
  worktree of it, to fork the conversation into a new session; the original
  remains available and missing files are not restored.
- `compaction-model-recovery`: a Codex turn that failed while compacting on the
  previous model shows the provider's error and why on a gray error card, with
  **Fork session…**. A spent-budget card is gray for every harness, since it
  isn't an Omnigent failure. Rate-limit and transient failures keep **Retry**
  instead and point to Fork in the session menu. A fork onto another model
  rebuilds the conversation from its history instead of cloning Codex's rollout,
  and fails rather than starting empty if that rebuild fails; the original is
  unchanged.
- `message-recovery`: a message racing initial runner binding or replacement
  reaches the available runner without a false failed turn. Native sessions
  initialize before delivery, including when an idle reconnect already restored
  the tunnel. Later messages reuse that generation's completed initialization;
  SDK sub-agents reuse their loaded session state.
- `import-progress`: import recent sessions or one session by ID from an online
  machine. Settings shows a spinner and count; the sidebar heading retains
  progress after leaving Settings. Results stay compact, with expandable failure
  details and retry of the original selection.
- `resume-imported`: an imported session can be resumed onto a chosen local host.
  Pi imports retain readable history and compaction checkpoints; continuation
  uses the latest summary, retained context and subsequent turns.
- `recent-switcher`: the desktop app opens the five most recent sessions with
  Control+Tab; Tab and Shift+Tab cycle, releasing Control switches, and Escape cancels.
- `browser-storage`: browser soft tabs, including one opened by the agent, share
  cookies within a session; different sessions stay isolated. Navigation stays
  per-tab. On macOS the browser toolbar can also import the current site's
  cookies from a local Chrome profile into that same session jar (Site scope
  picks the host or a parent domain); the copy is in-memory and never written
  back to Chrome.
- `canvas-workspace`: Main and project boards show only your sessions, including
  cached previews and reloads. Canvas collapses the shared sidebar on entry; clicking a
  card opens its session beside the project board. Switching cards keeps
  drafts and board state; resize, focus, and close controls adjust the split.
- `canvas-mobile`: the board and selected session use the standard top-left
  hamburger to open navigation. Back to canvas retains the selected project
  across deep links and reloads.
- `canvas-navigation`: Canvas uses the same collapsed-sidebar button and hover
  preview as other pages, including the macOS titlebar controls. Opening it
  reveals the shared destinations, including Usage when enabled.
- `canvas-create`: the plus button on Main or a project opens the new-session
  composer; sending returns to that board with the new session selected.
- `canvas-session-health`: an opened Canvas session receives host health and
  live updates even before it appears in the loaded sidebar pages.
- `view-options`: the one filter menu, on the Projects header, with Grouping
  (Default / Status / Harness / Created / Updated), Display (All / My / Shared with me / Archived), Ordering
  (Updated / Created / Custom) and Show (Updated / Environment / Repo / Branch) submenus,
  saved per device. Created ordering keeps sessions newest-created first as messages
  arrive;
  requests, pagination, and project folders use that order too. Switching order
  clears the cached rows and shows loading while session timestamps refresh,
  including project folders, pins, and Archived. With the flag off,
  sessions always use Updated regardless of the saved preference. Sessions shared
  with the viewer get their own "Shared with me" section above Sessions, without a
  per-row people icon. Status groups are Needs attention, Unread,
  Working, Draft, and Done. Working stays visible with muted “No sessions” text
  when empty. Starting or running sessions enter Working immediately; idle must
  last 10 seconds before they leave it. Errors and approvals bypass that delay.
  Background work of a native Claude or Codex session keeps it in Working, and
  a prompt its turn waits on in the terminal puts it in Needs attention; see the
  `working-status` rules in [Native harnesses](./native-harnesses.md).
  This only affects grouping, and existing hover/rename holds still apply.
  Harness groups sessions under alphabetically ordered harness names (such as
  Claude Code, Codex, and Pi), including custom agents using the same harness.
  Sessions without harness information appear under Unknown harness; pins stay separate.
  Created and Updated group by their respective date, independently of ordering,
  with the Created/Updated prefix only on the first date section, followed by
  headings such as “Yesterday” and “Earlier”. Environment metadata shows an icon
  with its label available to screen readers. Branch also resolves the workspace's
  checked-out branch when none was recorded at session creation, without hovering.
  All groupings replace
  Projects, Shared and Sessions with titled groups; the first group's header takes
  the Projects header's place. Select sessions sits beside the filter and selects every list below it at once. Behind
  the `sidebar_grouping_ui` release flag, on by default; with
  `OMNIGENT_FEATURES=-sidebar_grouping_ui`, the Sessions header keeps a flat
  Display filter and the tooltip keeps its plain layout.
- `mobile-drawer-swipe`: on phone layouts, swiping left on the open sidebar
  drawer or the dimmed chat strip beside it drags the drawer shut. Lifting past
  a third of its width, or after a quick flick, closes it; a shorter drag springs
  back. List scrolling, row taps, long-press reordering and horizontal scrollers
  keep their own touch handling.

## How to get to it (user POV)

- For siblings, open an owned active session's sidebar kebab, right-click menu,
  or header menu and choose **Add nested agent**. Mobile row/header menus offer
  the same action. On desktop, Cmd/Ctrl+Alt+A opens **Add nested agent** for the active
  session. Select a host in the composer and send the first message.
  Server admins configure the available targets under Settings → Admin → General →
  **Nested agent destinations**.

**Session grids (desktop only):** enable `OMNIGENT_FEATURES=grid_sessions_display_ui`, open a
session, and drag another sidebar row over it. Hover **Split left/right/top/bottom**
to preview, then drop. Split a remaining full-width or full-height pane again
to reach three or four panes. Alternatively, use a row's menu or right-click
menu → **Open in grid** and choose the pane and direction. Each pane has its own
transcript and composer; terminal-capable sessions have a Chat/Terminal switch in
each cell header. The shortcut ⌘/Ctrl+Alt+\ switches
only the active cell. Choices survive reloads and follow the session into full
screen; drafts and terminal connections survive view changes. A managed session
whose sandbox never started shows its saved first message as **Not delivered**
in its cell too, with **Copy message to new session**.
Render failures in one chat or terminal show **Try again** in that view while
cell controls, the other view and neighboring sessions stay usable. Clear the
fault and retry to recover.
Shared-session terminals are read-only for non-owners. One shared Workspace panel follows the active cell. Open the session full screen
for additional controls; close a pane to expand its neighbor. Closing down to
one pane returns to `/c/<id>`. Reopen saved grids in **Grids**, above Projects and Pinned, or use
their `/grid/<id>` URL on the same desktop browser. All sessions in the open grid
are highlighted in the sidebar. Mobile-width viewports, touch-primary devices, and
the iOS/Android apps hide grid menus and saved entries; grid URLs show a desktop-only
message without changing saved grids. Narrowing an open grid unmounts its cells
and disconnects terminal views; widening it restores the saved layout and reconnects.
The coarse-primary-pointer check also blocks tablets with attached keyboards;
touch-capable desktops whose primary pointer is fine remain eligible.
Double-click a grid title or use
its Rename menu action to edit it inline.
Enter or blur saves; Escape cancels. Grid names persist locally. Click anywhere
in a cell (or focus its controls) to activate it: its outline and inverted header
identify the session shown in Workspace. ⌘/Ctrl+Alt+Shift+] / [ activate the
next / previous cell and focus its terminal or composer. The grid header button
(⌘/Ctrl+Alt+] in a browser tab, ⇧⌘B / Ctrl+Shift+B in the desktop app) toggles the shared panel, and ⌃1–9 (Alt+1–9 on Windows/Linux) shows the active cell's Nth fixed tab; it spans the full app height beside the grid header and cells. Its width
is saved per grid and stays fixed when switching sessions; the header shrinks
with the cells when the panel opens.
Files, tabs, and unsaved edits remain session-specific. Maximizing Workspace
fills the grid area, including its header. Drag a cell titlebar onto another cell
to swap positions. With exactly two sessions side by side, drop a titlebar
near the whole grid's top/bottom edge to stack them. When stacked, use the
left/right edge to place them side by side, including along the dragged cell's
side. Only edges that change orientation appear; their preview spans the grid.
The center swaps positions and Escape cancels the preview.
Alternatively, focus the title and press Space, choose a cell with the arrow keys, and
press Space again. Escape cancels. The sidebar's Delete grid menu action forgets
only the grid, leaving sessions untouched. Unknown local grids show a recovery link.
Grids wait for account identity; if it is unavailable, use Reload to retry.
Failed sends restore text and supported attachments unless the cell has a newer draft.
Quoted replies recover as serialized text.

**Import:** open Settings → Import sessions, or choose **Import your recent
sessions** on the empty landing. The X beside the landing button dismisses the
suggestion and keeps it hidden after reload in this browser; Settings → Import
sessions remains available. Choose a machine, harness, and recent count
or session ID. After starting, go Back to the session list; its heading shows
progress and links back to Settings. Imported sessions appear in the normal list,
except Codex threads archived in Codex, which land in Archived, so the list can
show fewer sessions than the import count.
On phones, the progress link opens the Import panel directly; its header arrow
returns to the Settings menu.
For a local Pi session, the CLI also accepts
`omnigent import --harness pi --session <id> --server <url>`.
Open the imported session and choose a host and working directory to resume it,
then continue from Chat or Terminal.

**Sidebar row:** hover a row and open its menu, or right-click the row. Both
offer pin, fork, rename, mark as unread or read, create automation (when
automations are on), add to project (or move session), archive or unarchive,
and delete. Create automation opens the New automation dialog for the session;
saving files its copy in the automation's project and opens the automation. The
dialog and the automation's page link the session it starts from under Starts
from. Deleting a session an automation runs from asks to Delete both, and leaves
the automation's page for the automations list. With automations off for the
caller, the session is deleted alone and its automation pauses at its next run.

**Project folder:** right-click its header or open its three-dot menu, then
choose Rename project. Start typing as soon as the dialog opens.

**Project folder sessions:** expand a project with more than five sessions,
then click Show more under its rows. Opening one of its older sessions from
the ⌘K / Ctrl+K palette or a link also lists down to that session, once the
sidebar has loaded it. To change the count or list every session instead,
use Settings → Appearance → Sidebar.

**Pinned section drag:** drag a pinned row onto another pinned row to reorder
the Pinned section, or drop an unpinned row onto a pinned row to pin it into
that slot: the line marks whether it lands above or below the row. Release it
just below the last pin to pin it at the bottom.

**Custom order:** with `sidebar_grouping_ui` on (the default), drag one of your sessions onto
another row in the same list, project or group and release it; a line like the
Pinned section's marks where it lands. Move up and Move down in the row menu or
right-click menu do the same. On mobile, press and hold a row in the sidebar
drawer, then drag it. Ordering → Custom in the filter menu keeps the order shown.

**Bulk selection:** select several rows in the sidebar, then use the selection
actions (mark as read or unread, archive, unarchive, delete). Deleting sessions
automations run from names those automations and deletes them first.

**Session header menu:** open a session and use the menu next to its title for
pin, fork, export, rename, mark as unread, create automation, add to project,
archive or unarchive, and delete. Clicking the title also renames, and the title's folder
tag also moves the session into a project. Sub-agent sessions hide owner-only
actions. On phones, the sidebar toggle has a circular glass surface; host,
harness/model, background tasks, and session actions share the top-right glass
group. Buttons are 44px tall with 20px icons. These surfaces use the shared
React Bits glass treatment with a transparent fill, diffused backdrop, subtle
rim, and soft shadow. Menus and the sidebar use the more strongly blurred menu
variant. Browsers without backdrop filtering receive a solid theme surface.
The header controls sit 16px from the top and sides.
The mobile header omits the session title and fading backdrop,
retaining a Back link for child sessions.

**Desktop Session menu:** in the desktop app, Session → Pin (⌥⌘P), Archive
(⇧⌘A) and Fork Session… (⌥⌘F) act on the open session (Ctrl+Alt+P,
Ctrl+Shift+A and Ctrl+Alt+F on Windows/Linux). They are enabled only while the
session's header menu offers the action, and the open session's sidebar row
shows the same chords on its pin button and in its row menu.

**Rename shortcut:** in an owner-managed top-level session on desktop, press
Cmd+Option+R on macOS or Ctrl+Alt+R elsewhere, including while writing a draft.
With the sidebar open, the session's sidebar row becomes the same inline rename
field as its Rename menu item, text selected, so you can click the next row and
rename again. With the sidebar closed, or the row hidden in a collapsed project
or section, the header title becomes the input instead. Enter or blur
saves; Escape cancels. The shortcut leaves terminals, code editors, and open
menus or dialogs alone. It is listed in Keyboard shortcuts and Settings.

**Message actions:** fork through a specific assistant message, excluding later
turns. The header's Fork action copies the whole session instead. In either
dialog, keep the agent or choose another built-in or custom agent; for a
native target, the dialog also shows model, reasoning effort, and permission
mode pickers. Your custom
agents (installed with `omnigent agent add`, imported in the Create custom agent
dialog, or uploaded by running a session with their spec) stay available until
you remove them.

**Sandbox fork repositories:** from the header menu, sidebar row menu,
right-click menu, or a message's Fork action, choose a sandbox destination. The
repository list appears directly in the dialog with the source's repositories
preselected. Search connected GitHub repositories or paste a repository URL,
choose a branch for each, and remove any repositories the clone does not need.
Removing every repository starts an empty sandbox. The phone header's Fork
action offers the same choices.

**Archive worktree prompt:** archive a worktree session from the row menu,
bulk selection, or header menu; the "Also delete the worktree?" dialog offers
"No, archive only" and "Yes, delete worktree", plus Remember my choice.
The prompt explains that worktrees inside sandboxes or used by other sessions,
including archived sessions, are retained.
Dismissing it cancels the archive. Settings → Git → Worktrees holds the saved
choice only when the server supports cleanup.

**Archive running-session prompt:** archive a session whose agent is working or
awaiting approval from the quick button, row menu, right-click menu,
bulk selection, header menu, or desktop Session → Archive. "Archive running
session?" (or, for bulk, how many of the selected sessions are running) offers
Cancel and Archive; Archive continues to the worktree prompt when that applies.

**Row quick actions setting:** Settings → Appearance → Sidebar → Show Pin and
Archive buttons on session rows. Turning it off removes the hover buttons from
every sidebar row; the session header never had them. On desktop the row menu's
Pin item is shown only while the setting is off (mobile always shows it).

**Archived view:** choose Display → Archived sessions in the sidebar filter
menu, then filter by project.

**Sidebar filter menu:** with `sidebar_grouping_ui` on (the default), the funnel on the
Projects header holds Grouping, Display, Ordering and Show. In
Status, Harness, Created or Updated grouping, Projects is hidden and the funnel sits in the same spot on the
first group's header. The Select sessions button sits beside it and selects
project, Shared and Sessions rows together; the Sessions header keeps only New
session. On Display → Shared with me, Projects is hidden (folders hold only the
viewer's own sessions) and the Shared with me header carries the funnel, New
session and Select sessions. Hovering a row opens
its tooltip; the repo and PR rows load from the session's GitHub info only once
the tooltip opens, and fall back to the PR the server stored for the session
when the live lookup has none, with its age once that state is over five
minutes old. With the flag off, the funnel sits on the Sessions header and
offers Display only.

**Offline pull requests:** link PRs to a session, then stop its host. Open the
Pull Requests tab from the Workspace rail or composer (desktop and mobile).
On phones, Refresh and Link sit beside Close in the single drawer header;
with multiple PRs, the details header title becomes the back-to-list button.
A single PR opens its stored summary directly under **Pull Requests**, with its
title, status, last sync time and reconnect guidance. Unlink is in the header.
With multiple PRs, the list retains stored titles and status pills; each row
shows **Host offline** (or **Sandbox asleep**) instead of checks and Merge.
Hover or focus that notice
for the last sync time and reconnect guidance. Click a row for its stored summary,
then **All pull requests** to return. The row menu can open, copy, or unlink the
PR while offline; linking needs the host. Reconnect and verify live checks and
merge controls return. Unlink the last PR and verify it stays absent after refresh.
A session not yet imported to the server says so. Older servers retain their
host-dependent unlink behavior.

**Canvas:** choose Canvas in the sidebar, select Main or a project, and click
a session card once. Expand navigation manually to keep it open while switching
cards. Choosing Canvas in the sidebar returns to the selected board.
Drag the divider to resize, or focus it and use Left/Right arrows to
adjust the split and Home/End to reach its limits. Double-click the divider to
restore the default split. Focus the session to use the full area; Escape
restores the board. Close the session to return to the board. Opening, closing,
or maximizing Workspace keeps the canvas visible. Session focus is independent: Show canvas
restores the board without closing Workspace. Each card remembers whether Workspace is open.
A card's PR link comes from the session list, so it stays while the host is offline; its
tooltip gives the state's age once that's over five minutes old. Sessions the server
hasn't imported yet fill in as it imports them.
On mobile, the top-left hamburger opens the sidebar from the board or a selected
session. Back to canvas returns to the selected project. A Canvas session link
opens the same layout directly. If a session is replaced, for example after `/clear`,
its replacement stays beside the same board.
The plus button starts a new session in the selected canvas. After sending,
the new session opens beside that board; on narrow screens, Back to canvas
returns to it. Reloading a temporary session before creation finishes returns to
the same board. Open sidebar appears on the board or, when the board is hidden,
in the conversation header. On desktop, hovering previews the sidebar and clicking
opens it. At md and wider, the macOS app uses its existing titlebar toggle, Search,
and Settings; in a narrower window it uses the conversation header's Open sidebar
button and the drawer's Search and Settings, all right of or below the window buttons.
On macOS desktop, the board and its controls stay below the window title bar,
including while focusing a conversation or using a narrow window.
Open a Canvas session outside the loaded sidebar pages to see its current host
status and live session updates, including when the app is embedded.

**Reconnect:** in a session whose agent stopped, use the reconnect affordance
in the chat. Its **Clone** tab uses the shared fork form, including the searchable
sandbox-profile picker and source-profile default for participating providers.
The dialog shows the command for this situation (for example
`omnigent host` when the host is offline, or the harness's `--resume` command
when a local session is stranded). In the desktop app, **Reconnect host** acts
directly for this machine or a recognized Arca host; Arca's connect console
asks for confirmation before running, and a failed attempt opens the dialog,
whose **Reconnect Arca** action retries. Success is reported only once the
server lists the session's own host online; otherwise the dialog says the host
is still offline and points to **Switch host** in case Arca came back as a
different host. In a session on an offline Arca host, the desktop app keeps the
composer open: sending starts the same Arca reconnect, the message waits in the
queue as **Waiting for host** without reaching the server, and it sends once the
host is back. In a sandbox session (for example an Arclet) whose host is
asleep, **Reconnect host** in the composer's host menu has the server wake the
sandbox and start the agent without sending a message, in the browser too.
The host status dot in the composer chip and its menu spins while the sandbox
starts and turns green once it is online. A toast appears only when the wake
fails or, after the server answers, three regular host health checks still
find the host offline. Extra checks, such as the one run right after the
server answers, don't count. The checks count only once the server stops
reporting launch progress. If the server reports the launch failed or waiting
on the user, the dot stops and Reconnect host returns. If the server is still
reporting launch progress 5 minutes later, Reconnect host returns without a
toast while the dot keeps spinning on the server's stage.
Reloading during the wake keeps it spinning, with the stage (for example
"Provisioning sandbox") in the chip's tooltip and in the chat. Reconnect host
stays in the menu then, in case that stage never finishes.
Opening such a session presses Reconnect host for the user once the session
has stayed open for a second in a visible tab, and so does picking Files,
Changes or Pull Requests in the Workspace rail. This only happens for editors,
on an idle session whose launch isn't already running, and does exactly what
pressing Reconnect host does. Each opening checks once, so a session that sleeps again
while it stays open waits for Reconnect host, a Files, Changes or Pull
Requests pick, or a message. Every way of waking
the sandbox (Reconnect host, opening the session, Files or Changes, the
terminal's Resume) joins one wake per session, including after switching to
another session and back, and the Files and Pull Requests views say the
sandbox is starting while it runs or the server reports its launch.

**Message recovery:** send the first prompt while a runner is starting, or send
another message after its runner restarts. Also open a sub-agent's conversation
and send a follow-up after its parent runner is replaced.

**Missing workspace, desktop and mobile:** open a conversation whose working
directory is no longer available and choose **Continue in another directory…**
on the error card. The dialog explains the new session and missing files, then
lets you type or browse for an existing directory on a connected host. Optionally
name a branch under **Git worktree** to continue in a new worktree of that
repository instead. Choose **Continue** to open the new session with the copied
conversation. A missing destination stays in the dialog with an error and
creates no session.
The action also appears on an eligible error card inside a Canvas conversation
or side chat; it continues that conversation, including when another parent
conversation is open alongside it.

**Codex compaction on the previous model:** in a Codex session whose turn failed
while compacting on the model its last turn used, choose **Fork session…** on
the error card. The fork dialog opens for that session; pick the model to
continue on. A rate-limit or transient failure shows **Retry** instead; fork
from the session header menu. The action also appears on such a card inside a
Canvas conversation or side chat, and forks that displayed conversation rather
than the main session.

**Mobile:** the header menu and the sidebar drawer offer the same actions; touch
devices fold some row controls into the menu.

**Closing the mobile drawer by swipe:** at phone width, open the sidebar with
the header's Open sidebar button or, in the iOS app, a right swipe from the
left edge. Swipe left on the drawer or the dimmed chat strip beside it; past a
third of the drawer's width or after a quick flick it closes on the current
chat, otherwise it springs back. Settings and desktop widths don't use it.

**Databricks Sandbox and Arclet sessions:** Fork is enabled in the header,
sidebar menus, and message actions, and offers the same destinations as any
other session: connected hosts and each offered sandbox provider. Sessions
labelled `omnigent.host_type: managed` fork the same way. The source's own
sandbox is offered as **Same sandbox** when shared-sandbox support is available;
it keeps the working directory and creates a standalone fork with copied history.
Other sessions' ephemeral sandboxes are omitted, and an offline source cannot
be selected. Older servers keep the fresh-sandbox choices. A fork
of any sandbox session defaults to a fresh sandbox from the source's provider
when the server offers it, otherwise to its reusable online sandbox, then to a
connected host.
**Continue in another directory…** can use a connected host or the same online
sandbox, with an explicitly chosen existing directory; it never offers a new sandbox. The composer's host
menu shows a disabled **Switch host…** item when you have write access.
Read-only viewers retain the existing host menu without switching controls.
Hover or focus a disabled action to read why it is unsupported.
The reconnect dialog also disables Switch host, and Clone wherever Fork is
disabled; directory selection cannot enable either action.
While support is being checked, actions stay disabled: Fork waits only for the
session snapshot, and Switch host also waits for the source host record. If a
check fails, the explanation asks you to reload. A missing or failed source-host
lookup keeps switching disabled while the session stays forkable. Direct dialogs show a loading status without
an action button until the check finishes; supported forms then receive keyboard focus.
If the reconnect dialog is already on Clone when an unsupported result arrives,
it selects Reconnect and explains the restriction.
An open switch dialog closes when switching becomes unavailable and stays closed
if support returns; choose Switch host again to reopen it.

**Stop session:** open the parent's sidebar row menu or right-click the row and
choose Stop session while a side chat or sub-agent is working. On mobile, open
the sidebar drawer and long-press the row. The current-turn interrupt control is
a separate action that leaves the session connected.

**Side-chat lifecycle:** as the session's owner, use **Workspace → + → Side
chat**, type `/side` in the parent composer, or choose **Start a new side chat**
from the composer's add tray. Selecting assistant text also offers **Ask in side
chat** unless the composer is read-only. Viewers and editors of a shared session
see none of these. On mobile,
side chats open in a drawer; the header's **Conversation actions → Side
chats** reopens it. A generic hosted parent can start a new side chat
after stopping; this relaunches the parent and both use one runner. Sending to
an existing side chat or choosing Resume after the parent's runner exited (for
example, on its idle timeout) also relaunches the parent, then moves the side
chat to that runner. Close a side
chat with its tab's close button; the parent and sibling chats keep running.
On another device, open the same parent: saved side chats appear in the existing
Workspace tab strip or mobile Side chats drawer. Discovery fetches up to 20 chats
without changing the active panel or tab. Tabs closed on this device stay closed
after reload; other devices keep their tabs. Side chats created before server
discovery was added remain local to their original browser.
In the Electron desktop app, Cmd/Ctrl+W also closes the selected side-chat tab
in a visible workspace rail. With no closable visible tab it returns to the
new-session page; Cmd/Ctrl+Shift+W closes the window. Grid workspaces are
excluded from tab dispatch and retain the window-close behavior.
With Settings → General → **Open sub-agents in the Workspace panel** on, a
plain click on a sub-agent (transcript link, composer indicator, Agents row or
graph node) opens it as a tab in the same strip, marked with a bot icon, and
reveals the panel; ancestors still open full screen. The tab's **Open full
screen** link opens the sub-agent's own page. Claude Code and closed sub-agents
are read-only there too. Closing a sub-agent tab only hides it; the sub-agent
keeps running. Covered by `tests/e2e_ui/agents/test_subagent_side_panel.py`.
While a side chat works, typing a follow-up turns Interrupt into Send, which
queues or steers it as the main composer does; Escape still interrupts. Its queue
strip offers edit, steer, reorder and delete, and Mod+Enter steers them all.
Closing the side chat discards the follow-ups queued on that device. Other
devices keep their tabs and queued work. Closing still stops the chat's running
turn, as before; a sealed Codex side chat refuses further input. A chat-only
side chat can also send messages from its direct URL without choosing a
workspace. The side-chat composer opens the same `/` menu
affordance as the main composer but with a narrower set — `/help` and the
session's skills: `/help` is answered locally and a recognised skill runs
in-process through `slash_command`; a native terminal side chat shows no menu
and forwards slash text to the vendor TUI.

**Side-chat settings:** for harnesses other than native Codex, open an unsent
side chat from the Workspace menu,
composer add tray, or Ask in side chat. Before sending, open the model pill
beside the microphone, open its configuration row, and select a model or effort.
Switch tabs and return to confirm the draft keeps the choice. Send the first
question and check it uses that selection. Change it again before a follow-up;
the parent's selection should stay unchanged. Repeat in the mobile drawer.
Changing only the model preserves unspecified effort and compatible explicit effort;
an incompatible explicit effort is cleared. Sending waits until a settings save
finishes, including after reopening the pane. A failed save preserves the message
draft and allows retrying the settings or sending with the previous selection.
If the native session's model list is empty, the picker uses the same host
catalog as the main composer until the session's list arrives.
Available choices follow the harness's capabilities; viewers and closed chats
cannot change them. A failed start keeps the draft and selections for retry
without changing the parent's settings. Native Codex shows these controls only
once the side-chat child exists, when both server and runner support them.
The first message uses the existing native fork flow and inherited settings;
later model and effort changes apply to the child's next turn. Native Codex
requires a concrete model selection; model-reset aliases are rejected without
saving changes. Effort can still be reset to the selected model's default.

**Side-chat quotes:** select text in the parent conversation and choose **Ask in
side chat**. It appends a removable quote to the visible side-chat tab, including
an unsent tab, while keeping the draft. With no eligible tab visible, it opens a
new unsent tab. In the desktop side-chat pane or mobile drawer, select transcript
text and choose **Reply** to quote it in that chat's composer. Adding a quote
keeps existing comments before that quote. Edit between quotes or after the last
one, then send when ready. Paste an image into any reply text field after the
side chat has started. The first question is text-only; an image paste there
explains that the chat must be started first. Grid sessions use the same reply
composer and support interleaved comments and pasted images too. A viewer whose
composer is read-only (for example a Read grant) gets neither **Reply** nor
**Ask in side chat** when selecting parent text.

**Desktop browser:** choose **+ → Browser** in the Workspace panel or press
⌘/Ctrl+T. Agent browser requests and chat links with in-app opening enabled
create or select a closable Browser soft tab automatically.

**Desktop recent sessions:** hold Control and press Tab to open the five most
recent sessions. Continue pressing Tab (or Shift+Tab) to cycle, release Control
to switch, or press Escape to cancel.

## Driving it with the repro environment

Preconditions: a running instance (`verify-env start`, then `verify-env
doctor`), the built web UI, and at least one session (the `seeded_session`
fixture creates one). Run each test through the instance:

```sh
verify-env run -- python -m pytest <test> --ui-skip-build --video=on \
  --output="$VERIFY_EVIDENCE/sessions"
```

Tests that stop a runner, restart the server, or read the database cannot run
through `verify-env run`. They are marked "own environment" below; run them with
plain `uv run pytest`, which starts a private server for the test.

- **`session-grid` (browser contract):**
  `tests/browser_ui/chat/test_session_grid.py` drives sidebar drops, previews,
  the four-pane limit, independent sends, rejected-send recovery and live updates, reloads, pane closure,
  per-cell terminal input, switching without reattaching, active-cell shortcuts,
  keyboard cell cycling and workspace toggling,
  keyboard menu creation, inline rename/cancel/persistence, active-cell selection,
  shared workspace contents and width persistence, grid-wide maximize, rejected
  drops without navigation, sidebar
  reopening/deletion, highlighting all grid members, titlebar rearrangement and cancellation,
  mobile and tablet entry-point gating (including resize and reload), identity recovery, and missing/disabled grid routes. With the flag absent or false,
  ordinary session routes retain native link dragging and expose no grid menus,
  drop targets, or saved-grid sidebar entries. Run with `--browser-ui-skip-build --video=on` after building.
  Layout combinations and malformed local storage have unit coverage in
  `web/src/grid/gridLayout.test.ts`. For a real-server drive, enable
  `grid_sessions_display_ui` on the isolated server. Also check a collapsed sidebar manually.
  Per-view render recovery, preserved neighboring drafts, usable cell controls
  and independent chat/terminal retry have component coverage in
  `web/src/grid/GridSessionPane.test.tsx`. To check manually, inject a render
  exception into one chat or terminal, confirm the other views stay usable,
  clear the fault, and choose **Try again** in the failed view.

- **`pin`:**
  `tests/e2e_ui/sessions/test_sidebar_pin_unpin.py::test_unpin_moves_session_back_to_recent`
- **`pin-undo`, sidebar row:**
  `tests/e2e_ui/sessions/test_sidebar_pin_unpin.py::test_undo_unpin_restores_pinned_slot`.
  The row menu (mobile) and header menu Unpin have web unit coverage only:
  unpin a pinned session, click Undo on the toast, and expect it back in the
  same Pinned position.
- **`pin-reorder`:**
  `tests/e2e_ui/sessions/test_sidebar_pin_unpin.py::test_drag_reorders_pinned_sessions`
  also pins an unpinned row by releasing it just below the last pin. Dropping
  an unpinned row onto either half of a pinned row has web unit coverage only.
- **`custom-order`:**
  `tests/browser_ui/test_sessions_appearance.py::test_custom_order_keeps_the_list_and_commits_on_drop`
  (built SPA, mocked backend) covers the menu, the insertion line,
  committing on drop, reload, new sessions above, per-tab state and the flag-off
  fallback.
  `tests/e2e_ui/sessions/test_sidebar_projects.py::test_move_session_into_new_project`
  files a session dropped on another project's row and releases a row over
  itself without navigating. Group limits, both Move menus and the click after a
  drag have web unit coverage in `web/src/shell/Sidebar.pinReorder.test.tsx`. No
  test drives a touch drag: in a narrow window open the drawer, press and hold a
  row, drop it on another row of its list, and expect the new order without the
  session opening.
- **`title-instructions`:**
  `tests/e2e_ui/sessions/test_session_title_instructions_setting.py` saves and
  clears the field in the browser, then checks the next Claude SDK session's
  title request against a scripted mock model. The field appears only when
  `/v1/info` reports `session_title_instructions`. Per-user isolation, limits
  and invalid stored values are covered in `tests/stores/test_user_settings_store.py`
  and `tests/server/routes/test_user_settings_routes.py`.
- **`rename`:**
  `tests/e2e_ui/sessions/test_sidebar_rename.py::test_rename_session_enforces_user_title_limit`,
  `tests/e2e_ui/sessions/test_header_session_menu.py::test_header_session_menu_renames_owner_and_hides_for_subagent`.
  The desktop shortcut, including draft preservation, cancellation, and reload:
  `tests/e2e_ui/sessions/test_sidebar_rename.py::test_rename_hotkey_edits_sidebar_rows_in_sequence`,
  `tests/e2e_ui/sessions/test_header_session_menu.py::test_rename_hotkey_from_composer_persists_and_cancels`.
  The iOS drawer rename under the soft keyboard:
  `tests/e2e_ui/mobile/test_rename_keeps_sidebar_sessions_reachable.py::test_rename_keeps_sidebar_sessions_reachable`,
  `tests/e2e_ui/mobile/test_rename_keeps_sidebar_sessions_reachable.py::test_rename_scrolls_edit_row_into_view_above_keyboard`
- **`mobile-drawer-swipe`:**
  `tests/browser_ui/chat/test_mobile_sidebar_swipe_to_close.py::test_swipe_left_closes_open_drawer`,
  `tests/browser_ui/chat/test_mobile_sidebar_swipe_to_close.py::test_drawer_stays_open_for_scroll_and_short_drag`.
  These touch-drive the built SPA in desktop Chromium at a phone viewport with
  mocked APIs and a stubbed iOS bridge. Thresholds, multi-touch and long-press
  hand-off are covered in `web/src/hooks/useSidebarSwipeToClose.test.tsx`.
- **`archive`:**
  `tests/e2e_ui/sessions/test_sidebar_bulk_actions.py::test_bulk_archive_moves_session_to_archived`,
  `tests/e2e_ui/sessions/test_archived_project_filter.py::test_archived_project_filter_narrows_and_resets`,
  `tests/e2e_ui/sessions/test_archived_project_filter.py::test_archived_project_filter_load_more_pages_through`
- **`archive`, running-session prompt:** component coverage in
  `web/src/shell/Sidebar.archive.test.tsx` (quick button, row menu, bulk
  selection, chaining into the worktree prompt) and
  `web/src/shell/HeaderConversationMenu.test.tsx` (header menu and desktop
  Session → Archive); `web/src/shell/AppShell.test.tsx` covers a session opened
  outside the loaded sidebar pages. Manually: send a message that keeps the agent busy, hover
  its row, click Archive, and expect the dialog; Cancel leaves the row.
- **`row-quick-actions`:** component coverage in
  `web/src/shell/Sidebar.archive.test.tsx` and `web/src/pages/SettingsPage.test.tsx`
  (Reset to defaults); export and import in `web/src/lib/settingsPortability.test.ts`.
  Manually: turn the switch off and hover a row; only the "…" button appears,
  and its menu lists Pin and Archive.
- **`archive`, worktree prompt:**
  `tests/server/integration/test_sessions_archive.py::test_archive_stop_removes_worktree_when_requested`
  and
  `tests/server/integration/test_sessions_archive.py::test_delete_worktree_requires_archive`
  (plain `uv run pytest`) cover the opt-in teardown that keeps the branch and
  the rejected request without archive. The prompt, remembered choice, and the
  archive-only fallback on a server without cleanup have component coverage in
  `web/src/shell/Sidebar.archive.test.tsx` and
  `web/src/shell/HeaderConversationMenu.test.tsx`. Manually: archive a
  worktree session from the row menu, bulk selection, and header menu, and
  expect the dialog each time; an idle session without a worktree archives
  silently.
- **`unarchive`, header menu:**
  `tests/e2e_ui/sessions/test_archived_session_header_menu.py::test_archived_session_header_menu_offers_unarchive`.
- **`unarchive`, open archived session's sidebar row:**
  `tests/e2e_ui/sessions/test_open_session_sidebar_reveal.py::test_archived_session_opened_from_search_shows_unarchive_in_sidebar`
  opens an archived session from ⌘K and unarchives it from its revealed row.
  Placement, shared sessions, older live sessions and the display cap are
  covered in `web/src/shell/Sidebar.openSession.test.tsx`. Direct link,
  manually: archive one of your sessions, paste its `/c/<id>` URL into a new
  tab, and expect its row in the sidebar with Unarchive on hover.
- **`unarchive`, Archived view row and bulk selection:** web unit coverage
  only. Manually: archive a session, open the archived view, choose Unarchive
  on the row, and expect the session back in the main list.
- **`unarchive`, Undo toast:**
  `tests/e2e_ui/sessions/test_sidebar_lifecycle.py::test_sidebar_session_organization_round_trip`
  archives two sessions and restores both to Mine through Undo, including after
  a reload.
- **`delete`:**
  `tests/e2e_ui/sessions/test_sidebar_delete.py::test_delete_session_removes_row_and_from_store`,
  `tests/e2e_ui/sessions/test_sidebar_bulk_actions.py::test_bulk_delete_removes_sessions`
- **`export`, header menu (own environment):**
  `tests/e2e_ui/sessions/test_export_chat_menu.py::test_header_session_menu_offers_export`,
  `tests/e2e_ui/sessions/test_export_chat_menu.py::test_header_export_downloads_transcript`
  seed the transcript into the spawned server's database; run them with plain
  `uv run pytest`, outside `verify-env run`.
- **`unread`, sidebar row menu:**
  `tests/e2e_ui/sessions/test_sidebar_mark_unread.py::test_mark_unread_lights_the_dot_and_persists_across_reload`,
  `tests/e2e_ui/sessions/test_sidebar_mark_unread.py::test_unread_dot_survives_reload_from_localStorage_when_server_seed_is_empty`.
  The header-menu rename test above lists Mark as unread; manually choose it
  and expect the row's dot. The row dropdown's Mark as read action has coverage
  in `web/src/shell/Sidebar.readStateActions.test.tsx`. Right-click read-state
  actions need a manual check: mark an idle row unread, expect its dot, then
  choose Mark as read and expect it cleared.
- **`unread`, bulk selection:** coverage in
  `web/src/shell/Sidebar.readStateActions.test.tsx`, without a browser test. Select
  idle sessions with at least one unread row, choose Mark as read, and expect
  their dots to clear and selection mode to close. Select only read sessions,
  choose Mark as unread, and expect their dots to appear and selection to close.
- **`project`, sidebar row and header folder tag:**
  `tests/e2e_ui/sessions/test_move_to_project.py::test_move_session_into_project_regroups_sidebar_row`
  and, seeding its own environment,
  `tests/e2e_ui/sessions/test_header_move_to_project.py::test_header_folder_tag_moves_session_into_existing_project`.
  Repeat from the header menu's Add to project by hand and expect the row to
  regroup under that project.
- **Right-click menu:**
  `tests/e2e_ui/sessions/test_sidebar_context_menu.py::test_right_click_opens_session_actions_menu`
- **`project-rename`, project header right-click and three-dot menus:**
  `tests/e2e_ui/sessions/test_project_rename_dialog_focus.py::test_rename_dialog_preselects_name_so_typing_replaces`
  covers typing across the menu's exit animation from both menus.
- **`fork`, message cutoff:**
  `tests/e2e_ui/fork_session/test_fork_from_middle.py::test_fork_from_middle_truncates_history`,
  plus agent switching (own environment):
  `tests/e2e_ui/fork_session/test_fork_switch_agent.py::test_fork_switch_agent_carries_history`.
  Its SDK-to-SDK target is a built-in agent that only the test's own server
  loads; run it with plain `uv run pytest` so that target is available.
- **`fork-sandbox-repositories`, header, message, sidebar row, right-click, and phone header:**
  `tests/browser_ui/chat/test_fork_sandbox_repositories.py::test_sandbox_fork_edits_multiple_repositories_and_branches`
  runs without a repro instance using `uv run pytest <test>
--browser-ui-skip-build --video=on --output=<evidence-directory>`.
  Each entry checks current-branch prefilling and the unpushed-branch fallback.
  Each also selects **Same sandbox**, checks directory reuse and exclusion of
  unrelated sandboxes, then switches back to a fresh sandbox.
  Desktop and phone header variants remove a repo, search for another, choose
  independent branches, and check the fork request and navigation. The message
  variant submits the inherited selection and checks the history cutoff; sidebar
  variants stop after opening and checking the prefill. The built SPA uses mocked
  sandbox and GitHub responses and advertises support for editable fork workspaces;
  this proves browser behavior and the submitted payload, not sandbox provisioning.
- **`workspace-recovery`, desktop and mobile conversation, Canvas, and side-chat cards:**
  `tests/browser_ui/chat/test_workspace_missing_recovery.py::test_continue_missing_workspace_in_another_directory`
  runs without a repro instance using `uv run pytest <test>
--browser-ui-skip-build --video=on --output=<evidence-directory>`.
  The browser contract covers the error action, visible explanation, blank
  destination, rejected missing directory, directory browser, preflight before
  fork and launch, and navigation to copied history while the original remains
  available. The new session renders the source's workspace failure as a
  historical notice without another recovery action, including after reload.
  All three surfaces run at desktop and phone widths; the side-chat pane and
  mobile drawer recover the child while leaving the neighboring parent intact.
  Host filesystem and fork/launch responses are mocked; this does not prove a
  native CLI received history or started in that directory.
- **`compaction-model-recovery`:**
  `tests/harnesses/codex_native/forwarder/test_turn_errors.py::test_error_during_compaction_reports_the_previous_model_it_ran_on`
  covers detection;
  `tests/server/integration/test_sessions_endpoints.py::test_native_failures_are_coded_live_and_after_reload`
  (plain `uv run pytest`) covers the card live and after reload, including
  retryable and expired-login failures that keep their own codes;
  `tests/runner/test_codex_model_pick_fallback.py::test_fork_onto_another_model_rebuilds_history_instead_of_cloning`
  covers the rebuilt fork, and
  `tests/runner/test_codex_model_pick_fallback.py::test_fork_onto_another_model_fails_when_its_history_cannot_be_rebuilt`
  its failed rebuild. The card's hint and **Fork session…** action have
  component coverage in `web/src/components/blocks/StatusBlocks.test.tsx`.
  No browser test drives the card into the fork dialog.
- **`fork`, whole session and resources:**
  `tests/e2e_ui/fork_session/test_fork_preserves_image_attachment.py::test_fork_carries_image_reference_and_its_resource`.
  This uses the header menu and checks that the fork's image URL loads.
  The dialog's permission-mode picker for native targets has component
  coverage in `web/src/shell/ForkSessionDialog.test.tsx`; drive the model and
  effort pickers by hand: fork a Codex session, pick another effort, and expect
  the new session's pill to show it.
  Duration coverage:
  `tests/e2e_ui/fork_session/test_fork_retains_worked_for.py::test_fork_retains_worked_for_duration`,
  and transcript-copy integration coverage (plain `uv run pytest`):
  `tests/server/integration/test_sessions_fork.py::test_fork_copies_transcript_content_with_fresh_ids`
- **`fork-custom-agent`, message action:**
  `tests/e2e_ui/fork_session/test_fork_session_scoped_custom_agent.py::test_fork_switches_onto_session_scoped_custom_agent`
  creates a custom-agent session, selects it as the target, and checks the
  copied transcript and bound agent. The header/custom-agent combination needs
  a manual drive: choose that target from the header's Fork dialog and check
  that the full transcript and chosen agent survive in the new session. Record
  the selection, navigation to a different session, transcript readback, and
  the target agent returned by the session API. Repeat from a message with a
  later turn present to distinguish message cutoff from the whole-session fork.
- **`fork-access` (server integration, plain `uv run pytest`):**
  `tests/server/integration/test_sessions_permissions.py::test_fork_session_requires_read_access`,
  `tests/server/integration/test_sessions_permissions.py::test_fork_switch_binds_session_scoped_target_with_access`,
  `tests/server/integration/test_sessions_permissions.py::test_fork_switch_denies_session_scoped_target_without_access`.
  These exercise authenticated routes and stores; the single-user browser
  recipe cannot prove cross-user access rules.
- **`clone`:**
  `tests/e2e_ui/sessions/test_clone_session.py::test_clone_session_copies_transcript_and_navigates`,
  `tests/e2e_ui/fork_session/test_typed_workspace_enables_clone.py::test_typed_tilde_workspace_enables_clone`
  Closing mid-clone has component coverage in `web/src/shell/ForkSessionDialog.test.tsx`
  (`clone outliving its form`). To drive it by hand, delay `POST …/fork` in the
  browser, close the dialog after Clone, and expect a toast whose **Open** opens the clone.
- **`unsupported-sandbox-actions`:**
  `tests/e2e_ui/fork_session/test_sandbox_disabled_controls.py::test_sandbox_fork_enabled_and_switch_host_disabled`
  checks that the header, sidebar context menu, and message action offer Fork, and
  that the composer host menu's Switch host stays disabled, at desktop and phone
  widths. It checks hover, keyboard focus, and ignored activation for Switch host.
  The Arclet session forks to connected hosts and a new Arclet:
  `tests/e2e_ui/fork_session/test_sandbox_disabled_controls.py::test_arclet_session_forks_onto_connected_hosts_and_a_new_arclet`.
  The server and transcript are real; Databricks Sandbox and Arclet metadata is
  patched at the browser boundary, and no live sandbox is provisioned. Direct form
  and reconnect guards have component coverage in `web/src/shell/ForkSessionDialog.test.tsx`,
  `web/src/shell/SwitchHostDialog.test.tsx`, and
  `web/src/shell/ReconnectSessionDialog.test.tsx`. Header fallbacks, including
  mobile, are covered by `web/src/shell/ChatHeader.test.tsx`.
  Failed lookups, recovery, hostless sessions, and shared sessions with unlisted
  hosts are covered by `web/src/hooks/useSessionActionRestrictions.test.tsx`.
  Host-menu loading/error explanations and the default status badge's disabled
  action have component coverage in `web/src/components/HostBadge.test.tsx`.
  Loading-to-enabled keyboard focus is covered there and in
  `web/src/components/DisabledActionTooltip.test.tsx`. The fork and switch-host
  dialog suites also cover loading-to-form focus; the tooltip suite checks that a
  cleared explanation does not reopen without interaction.
  Supported host-switch UI coverage:
  `tests/e2e_ui/sessions/test_host_badge.py::test_host_badge_switches_the_session_to_another_host`
  checks the release/launch requests with stubbed host APIs.
- **`reconnect`, spinner (own environment):**
  `tests/e2e_ui/chat/test_reconnecting_spinner.py::test_reconnecting_state_shows_spinner`
  seeds an error item directly into the spawned server's database.
- **`reconnect`, offline-host cause (own environment):**
  `tests/e2e_ui/sessions/test_session_host_offline.py::test_offline_native_host_refuses_input_and_keeps_reconnect_action`
  creates Claude/Codex native-wrapper sessions on a real disposable host, stops
  the host and its runners, and checks that both client retry contracts refuse
  without transcript input, failed status, or pending work. It covers missing
  and stale runner bindings, reload, the host menu's Reconnect action, and a
  refused retry with no extra items. Repeat with `--device='iPhone 13'` for
  the mobile entry point; keep browser evidence outside the checkout.
- **`reconnect`, stopped session (own environment):**
  `tests/e2e_ui/sessions/test_sidebar_stop.py::test_stopped_session_shows_reconnect_affordance`
- **`reconnect`, completed turn after host handoff (own environment):**
  `tests/e2e/test_runner_exit_replica_handoff_e2e.py::test_host_report_after_handoff_preserves_only_completed_turns`
  moves a real host tunnel to a different server replica while its runner
  stays on the original replica, then kills that owned runner. Reloaded API
  snapshots keep completed work idle; interrupted work and runners that never
  connected still fail. The OpenAI Agents harness uses local scripted replies.
- **`reconnect`, idle replica handoff (own environment):**
  `tests/e2e_ui/sessions/test_idle_runner_handoff.py::test_idle_session_stays_healthy_after_unreadable_replica_handoff`
  moves a real runner between two servers sharing storage, then makes the old
  server's session and liveness reads unavailable through its production grace
  period. A completed legacy transcript without saved lifecycle state must
  remain readable without a disconnect error, including when the browser
  returns to the old server after its reads recover.
- **`reconnect`, completed Claude Task child (own environment):**
  `tests/e2e_ui/sessions/test_claude_native_idle_handoff.py::test_completed_claude_child_survives_stale_status_handoff`
  drives a real Claude-native parent and its Agent tool through child completion,
  then stages stale saved `running` state and moves the runner to a fresh server.
  After a real tunnel loss and the production disconnect grace, the child's
  result stays readable without a chat error or failed Agents-row status,
  including after reload. Only model replies and stale persistence are staged.
  Requires Claude Code and tmux; machine-managed Claude credentials need an
  isolated container for the local model endpoint.
- **`stop`, `archive`, active sub-agents (own environment):**
  `tests/e2e/test_parent_stop_subagents_e2e.py::test_native_parent_teardown_preserves_child_outcome`
  drives real Claude and Codex parents, native children, a host daemon, and its
  dedicated runner through the public Stop/Archive APIs. Only model replies are
  scripted. It waits through the production disconnect grace and includes a
  real runner crash that must still report a failure. Requires both native
  CLIs and tmux; Claude's machine-managed credentials require an isolated
  container for the local model endpoint.
- **`stop`, `archive`, host-bound sub-agent children:**
  `tests/server/integration/test_session_host_launch.py` (`-k shared_runner`)
  creates a child through `POST /v1/sessions` with `host_id` and
  `parent_session_id`, so it rides its parent's runner over a real host tunnel.
  Archiving, stopping, or relaunching the child leaves that runner up for the
  parent and its siblings, including a grandchild still bound to the root's
  runner; a child bound to a runner of its own still tears it down.
- **`stop`, `side-chat-lifecycle`, hosted side chats (own environment):**
  `tests/e2e_ui/sessions/test_stop_side_chats.py::test_stop_session_stops_hosted_side_chats`
  drives both desktop sidebar menus and the mobile long-press menu with a real
  host. Side chats share their parent's runner. Closing one leaves its parent
  and sibling working; stopping the parent stops that shared runner and keeps
  all histories. An unrelated session stays online, and a new side chat
  relaunches its parent before both share the replacement runner.
  Only model replies are scripted.
- **`side-chat-lifecycle`, shared-runner recovery (server integration, plain `uv run pytest`):**
  `tests/server/integration/test_session_host_launch.py::test_side_chat_message_relaunches_exited_source_runner`,
  `tests/server/integration/test_session_host_launch.py::test_side_chat_retry_relaunches_exited_source_runner`,
  and
  `tests/server/integration/test_session_host_launch.py::test_side_chat_message_rebinds_to_already_relaunched_source_runner`
  use a mock host. After the parent's runner exits, a side-chat message or
  Resume relaunches the parent on its host and initializes the side chat on the
  replacement runner. A parent that already has a replacement is reused
  without another launch.
- **`side-chat-lifecycle`, shared-runner recovery (real host, plain `uv run pytest`):**
  `tests/e2e/test_host_e2e.py::test_host_side_chat_recovers_after_shared_runner_exits`
  kills the shared runner while the host stays connected. The side chat's next
  message relaunches the parent's runner and both continue on it; after a
  second kill, Resume on the side chat does the same. Only model replies are
  scripted.
- **`side-chat-lifecycle`, creation entry points:**
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py` covers `/side`, the Workspace
  menu, the composer add tray, selected text's Ask in side chat action, and the
  mobile side-chat drawer. Its stale-branch scenarios simulate a stopped parent
  runner and verify parent recovery before binding; the hosted lifecycle test
  above proves live-parent reuse with actual runner processes.
- **`side-chat-settings`, desktop panel and mobile drawer:**
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py::test_side_chat_sends_with_stale_branch_metadata`
  checks that a legacy harness without advertised picker capabilities hides the
  control after slash and Workspace creation;
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py::test_side_chat_opens_in_a_drawer_on_mobile`
  checks the same gate in the mobile drawer.
  `tests/browser_ui/chat/test_model_catalog.py::test_side_chat_model_settings_before_and_after_first_message`
  checks the add tray and Workspace entry points on desktop and mobile with mocked
  session APIs, including fork-time draft settings and later child-only updates.
  `web/src/components/chat/SideChatSettings.test.tsx`
  covers explicit child targeting, compatible effort choices, rejected updates,
  harness capabilities, parent-runner reconnect recovery, and viewer/closed states.
  `tests/server/integration/test_sessions_child_sessions.py::test_codex_side_settings_are_saved_and_forwarded_only_to_child_turn`
  covers a parent whose live reported model differs from its saved web selection:
  an untouched child follow-up retains the live model. It also checks that model
  reset aliases fail without changing the saved model or effort.
- **`side-chat-quotes`, desktop Ask in side chat and Reply:**
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py::test_ask_in_side_chat_opens_a_quoted_side_chat_tab`
  checks pending/live tab reuse, interleaved comments, pasted images, and replies
  sent only to the child. Hide Workspace and repeat Ask in side chat to verify
  a new tab opens instead of changing the hidden draft.
  `tests/e2e_ui/collaboration/test_sharing_journey.py::test_share_grant_downgrade_revoke_journey`
  checks that an Edit collaborator is offered Reply on selected text and a
  Read viewer is not.
- **`side-chat-quotes`, mobile drawer Reply:**
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py::test_side_chat_opens_in_a_drawer_on_mobile`
  checks that Reply stays on screen and interleaved comments with a pasted image
  reach only the side chat. Component coverage in `web/src/components/chat/SideChatPane.test.tsx`
  checks side-chat and grid composers, quote removal, preserved text/files, read-only chats, and starting-tab
  eligibility; `web/src/pages/ChatPage.composer.test.tsx` checks target routing
  and parent isolation. In an embedded app, repeat text selection in both the
  parent and side chat and check the popup's theme and button styling.
- **`side-chat-lifecycle`, another device:**
  `tests/e2e_ui/chat/test_side_chat_entrypoints.py::test_side_chat_follows_the_user_to_another_device`
  starts a side chat in the desktop rail, opens it from a phone-sized browser
  context with its own storage using the automatically restored tab, sends a follow-up,
  closes it there, and checks that only the phone tab stays closed after reload.
- **`side-chat-lifecycle`, direct chat-only URL (browser contract):**
  `tests/browser_ui/chat/test_side_chat_resume.py::test_runnerless_side_chat_sends_from_its_direct_url`
  opens a runnerless child directly and sends a message without a directory
  picker. Session metadata and message dispatch are mocked.
- **`side-chat-lifecycle`, `side-chat-settings`, native Codex:**
  `tests/e2e_ui/chat/test_native_codex_side_chat.py::test_native_codex_side_chat_inherits_context_and_closes_independently`
  drives a real Codex CLI and app-server with scripted model replies. It checks
  inherited context in the model request, discovery and a follow-up from an
  independent browser context, isolated follow-ups, and a parent
  that stays usable after closing its side chat. Desktop/mobile variants create
  a side chat through the Workspace menu or `/side`, then change its model and
  effort before a follow-up. Scripted provider requests verify the actual model
  switch, inherited context, effort reset and unchanged parent settings.
- **`reconnect`, desktop app (own environment):**
  `tests/e2e_ui/sessions/test_reconnect_local_host_from_app.py::test_desktop_reconnect_performs_local_host_reconnect`,
  `tests/e2e_ui/sessions/test_reconnect_local_host_from_app.py::test_desktop_reconnect_failure_offers_retry`,
  `tests/e2e_ui/sessions/test_reconnect_arca_from_app.py::test_desktop_reconnects_arca_from_current_chat`,
  `tests/e2e_ui/sessions/test_reconnect_arca_from_app.py::test_desktop_arca_reconnect_failure_offers_retry`,
  `tests/e2e_ui/sessions/test_reconnect_arca_from_app.py::test_desktop_send_reconnects_offline_arca_then_sends`
  drive the real SPA and server with an injected desktop bridge and offline host
  responses. The Arca journeys cover a remembered host and a host that was not
  saved; they check one-action start, progress, failure and retry,
  and a message that waits for the host, not live SSH or the desktop's consent
  console.
- **`reconnect`, asleep sandbox:**
  `tests/e2e_ui/sessions/test_host_asleep_composer.py::test_opening_asleep_session_wakes_sandbox_in_status_dot`
  drives the real SPA and server with the session patched to an asleep Arclet
  host. It checks that opening the session sends `retry_session` on its own,
  spins the chip and menu dots with no toast, and turns them green once
  `/health` reports the host online. Sibling tests cover a failed wake's error
  toast and Reconnect host retry, a session opened mid-wake, and a wake whose
  stage outlasts its limit. The open dwell, Files/Changes/Pull Requests picks, and the
  one-wake-per-session sharing are covered by
  `web/src/hooks/useSandboxAutoWake.test.ts` and
  `web/src/hooks/useSessionReconnect.test.ts`. None
  of them resume a live sandbox.
- **`message-recovery`, native binding races (own environment):**
  `tests/e2e/test_native_runner_binding_races_e2e.py::test_native_send_rechecks_binding_after_runner_miss`
  drives real servers, runners, and Claude/Codex CLIs with a mock model. It
  covers first binding on a sibling replica and local runner replacement,
  including explicit retry on the owner and exactly-once prompt/reply checks.
- **`message-recovery`, native host crash (own environment, live model):**
  `tests/e2e/test_native_host_reconnect_e2e.py::test_native_message_survives_host_restart`
  uses a real server, host daemon, host-launched runners, and Codex CLI. After
  a successful tool-using turn it kills the daemon, sends one follow-up, and
  restarts the same host beyond the ten-second runner grace. The original
  input must complete its file write exactly once on a new runner, without
  resending or persisting a failed turn. Requires `codex`, `tmux`,
  `OMNIGENT_E2E_CODEX_NATIVE=1`, and live `--llm-api-key` credentials (optionally
  `--profile`). Logs and timing evidence remain in pytest's temporary directory.
- **`message-recovery`, host relaunch (server integration, plain `uv run pytest`):**
  `tests/server/integration/test_session_host_launch.py::test_message_relaunch_classifies_replacement_runner_liveness`
  distinguishes a replacement live on another replica from a failed launch
  with an old heartbeat or a local heartbeat. This controls liveness evidence;
  it does not drive an actual cross-replica host migration.
- **`message-recovery`, already-connected native runner (server integration):**
  `tests/server/integration/test_session_host_launch.py::test_relaunch_posts_session_init_before_forwarding_message`
  checks Claude and Codex first-input initialization, suppression of replaying
  persisted input, and reuse on the next message without repeating child recovery.
- **`message-recovery`, rotated Codex session (own environment):**
  `tests/e2e/test_codex_rotated_bridge_relaunch_e2e.py::test_rotated_codex_session_relaunches_into_its_bridge_directory`
  drives `/new` in real Codex, stops that session's host-launched runner, then
  sends a chat message to relaunch it. Chat and terminal replies must both
  reach the rotated conversation with retained or missing bridge state. Only
  model replies are scripted; no browser interaction is claimed.
- **`message-recovery`, SDK sub-agent (server integration, plain `uv run pytest`):**
  `tests/server/integration/test_sessions_child_sessions.py::test_sdk_subagent_recovery_skips_session_init`
  covers recovery during ancestor healing and during the final binding refresh,
  without initializing the SDK child's already-loaded session again.
- **`import-progress`:** run
  `tests/e2e_ui/sessions/test_import_sessions.py` for Settings recent imports,
  exact-ID imports, and the empty-landing entry point (mocked import responses).
  Run `tests/e2e_ui/sessions/test_import_progress_in_session_list.py` for a real
  isolated host reading transcripts, streaming to the server, leaving Settings,
  progress beside the sidebar heading, and clearing it on completion. Desktop
  and mobile cases follow the indicator back to the panel and verify that the
  content can receive clicks, including behind the mobile drawer boundary.
  `tests/e2e_ui/sessions/test_import_oversized_session.py` verifies recent and
  exact-ID imports through a real host when a transcript exceeds the tunnel's
  message cap; the batch summary links to all three sessions in the sidebar.
  Component coverage checks compact large batches, failure expansion, retry
  after navigation, and collapsed headings. `web/src/shell/NewChatDialog.test.tsx`
  covers the landing import link and dismissal, including persistence after
  remount and unavailable storage.

- **`resume-imported` (own environment):**
  `tests/e2e_ui/sessions/test_imported_session_resume.py::test_imported_session_resumes_onto_chosen_local_host`
  covers the shared host picker with Claude. For Pi checkpoint conversion, run
  `tests/test_session_import.py::test_pi_import_retains_compaction_for_resume`
  with retries disabled; it covers legacy/current formats, retained tool pairs,
  images and missing boundaries. Imported images remain in readable history;
  resume snapshots use the same text-only representation as live Pi checkpoints.
  This is component coverage, not a running Pi session.
  For real Pi acceptance, create a session with enough history to compact,
  compact it, then add another turn. Import it using the CLI and Settings
  (exact ID and recent sessions), and check duplicate imports remain a single
  session. Resume onto an isolated host and send from both Chat and Terminal.
  Check that the full imported history remains readable and the original file
  is unchanged. With a recording provider, verify that the outgoing context
  contains the summary, retained context and later turns, omitting summarized
  raw turns. Record the Pi version and mock/live provider scope separately.
- **`recent-switcher` (manual Electron):** open at least six sessions, hold
  Control and press Tab to show the five most recent, cycle with Tab and
  Shift+Tab, release Control to switch, then reopen and press Escape to cancel.
- **`browser-storage` (real Electron, own environment):**
  `web/electron/e2e/desktop_cookie_isolation.e2e.js`. Sign into a site in one
  tab, open it in another tab and the agent browser, and confirm both are signed
  in. Another session should be signed out. Log out and refresh the same-session
  tabs; all should be signed out. Chrome cookie import needs a real Keychain and
  Chrome profile, so it is manual: sign into a site in Chrome, open that site in
  the pane, click the cookie button, confirm the native prompt, and check the
  page is signed in while an unrelated site in the same session is not.
- **`canvas-workspace` (sidebar entry, project cards, chat, focus, and history):**
  `tests/e2e_ui/sessions/test_canvas_workspace.py::test_canvas_keeps_board_drafts_and_sidebar_while_switching_sessions`.
  Workspace open/close, independent focus, maximizing, and pane sizing on wide
  and smaller desktops:
  `tests/e2e_ui/sessions/test_canvas_workspace.py::test_canvas_stays_visible_when_workspace_opens`.
  Ownership filtering on desktop/mobile, grouping, feature gating, drag persistence,
  and live updates also run in
  `tests/e2e_ui/sessions/test_canvas_page.py`, with selection churn in
  `tests/e2e_ui/sessions/test_canvas_selected_highlight_persists.py::test_clicked_card_keeps_selected_highlight_across_live_updates`.
  The selection journey covers duplicate titles and a long list whose nodes
  move outside the rendered viewport after a live update.
  Unit tests in `web/src/canvas/canvasNavigation.test.ts` distinguish Canvas
  from similarly named routes under standalone and embedded mount paths.
  `web/src/canvas/CanvasWorkspace.test.tsx` verifies that dismissing a nested
  dialog with Escape preserves conversation focus and that resizing respects
  both panes' minimum widths; double-click restores and saves the default split.
  It also checks that the sidebar menu stays usable while the board is loading.
  `web/src/pages/CanvasPage.test.tsx` checks viewport
  translation across successive resizes, hiding, and restoring the pane,
  refitting untouched boards after card changes, and preserving queued viewports
  during rapid project switches and resizes. `web/src/shell/AppShell.test.tsx`
  verifies Workspace's saved open/closed choice across card changes.
  `web/src/hooks/useConversationRedirect.test.tsx` delivers session replacement
  events and checks the same board, query parameters, and replaced history under
  standalone and embedded routes.
- **`canvas-session-health` (card selection and direct session links):** component
  tests in `web/src/hooks/useActiveConversationId.test.tsx` cover standalone and
  embedded session routes, switching cards, and returning to the board.
  `web/src/hooks/RunnerHealthProvider.test.tsx` and
  `web/src/hooks/SessionUpdatesProvider.test.tsx` verify that a Canvas session
  outside the loaded sidebar pages enters health polling and the live watch
  set; changing cards updates the watched session and encoded temporary ids
  are excluded.
- **`canvas-mobile` (deep link, back button, and reload):**
  `tests/e2e_ui/sessions/test_canvas_workspace.py::test_canvas_deep_link_and_mobile_return_keep_the_project`.
  At phone width and just below the desktop breakpoint, the journey compares the
  hamburger with an ordinary session, opens navigation from both Canvas views,
  and returns to the selected project.
- **`canvas-navigation` (collapsed/expanded, Usage enabled/disabled):**
  `tests/e2e_ui/sessions/test_canvas_creation.py::test_canvas_uses_standard_sidebar_navigation`.
  The journey compares the toggle's icon, position, and size with an ordinary
  session, previews and opens navigation, and checks that split and focused
  conversations retain one toggle. It compares destinations and returns through
  Canvas, then reloads to confirm the selected project remains remembered.
  macOS title-bar clearance, sidebar opening from the titlebar toggle or mobile
  hamburger, and conversation focus/return at wide and narrow widths run in
  `tests/e2e_ui/sessions/test_canvas_workspace.py::test_canvas_controls_clear_the_macos_titlebar`.
  This Chromium journey emulates the macOS preload bridge and user agent; it
  verifies the app's layout around the title bar, not native traffic-light rendering.
  `web/src/embed.test.tsx` checks that the compiled Canvas clearance selector
  matches the embedded app's Mac scope root. For an embedded desktop build,
  enable Canvas only in the isolated test host, enter through the sidebar and a
  Canvas deep link, then collapse/preview/reopen navigation and focus/restore a
  session. Check that the Canvas heading and restore control stay below the
  native title bar at 14px/16px host roots and 80%, 100%, 110%, and 125% page zoom.
  `web/src/shell/ChatHeader.test.tsx` checks the shared toggle's desktop/mobile
  icons, peek cancellation, and hover/keyboard tooltip behavior.
- **`canvas-create` (Main/project plus button, desktop/mobile):**
  `tests/e2e_ui/sessions/test_canvas_creation.py::test_canvas_plus_creates_session_on_the_selected_board`.
  The composer uses a fixture host; session creation, binding to the isolated
  runner, and the mock-backed first reply use the real server. Temporary-id
  replacement, server-first creation, feature gating, and failed-create draft
  recovery also run in `web/src/shell/NewChatDialog.flow.test.tsx`.
  Stale temporary routes return to their board in
  `tests/e2e_ui/sessions/test_canvas_creation.py::test_stale_canvas_creation_url_returns_to_its_board`;
  ordinary sessions still return to the new-session page in
  `tests/e2e_ui/sessions/test_canvas_creation.py::test_stale_regular_creation_url_returns_to_new_session`.
- **`view-options`:**
  `tests/e2e_ui/sessions/test_sidebar_view_options.py::test_harness_grouping_persists_across_reload`
  (desktop and narrow-browser navigation),
  `tests/e2e_ui/sessions/test_sidebar_view_options.py::test_grouping_and_show_toggles_persist_across_reload`,
  `tests/e2e_ui/sessions/test_sidebar_view_options.py::test_tooltip_fetches_repo_and_pr_only_when_opened`,
  `tests/e2e_ui/sessions/test_sidebar_view_options.py::test_created_ordering_persists_and_flag_off_uses_updated`
  (the flag-off case disables it by patching `GET /v1/info`). Unit tests in
  `web/src/shell/Sidebar.test.tsx` ("sidebar_grouping_ui release flag") check
  that the flag-off sidebar ignores stored view options.
- **`project-show-more`:** no browser test drives it. Component tests in
  `web/src/shell/Sidebar.test.tsx` ("Sidebar project Show more") cover paging,
  the next server page, the slide and its reduced-motion skip, the open-session
  reveal, row-menu filing leaving the listed rows as they are, the
  hidden-session badge, and the ⌘[ / ⌘] hotkey;
  `web/src/shell/Sidebar.shiftSelect.test.tsx` covers shift ranges, bulk
  actions, and bulk filing. The Settings → Appearance → Sidebar controls are
  covered by the Sidebar tests "lists the configured number of sessions per
  Show more click", "lists every session and auto-loads more pages when
  folders list all sessions" and "lists and reorders every member of a
  Custom-order folder listing all sessions", `web/src/pages/SettingsPage.test.tsx`
  (defaults, invalid counts, reload, reset),
  `web/src/lib/projectFolderPaginationPreferences.test.ts` (storage and the
  legacy `omnigent:project-folder-pagination` migration), and
  `web/src/lib/settingsPortability.test.ts` (export/import). By hand: create a
  project (`POST /v1/projects`), file six or more sessions into it with
  `PATCH /v1/sessions/{id}` and `project_id`, one second apart so `updated_at`
  orders them, then expand the folder and click Show more. In Settings →
  Appearance → Sidebar, set "Sessions to show before "Show more"" to 3: the
  folder lists three rows and each Show more adds three. For the switch, file
  more than 20 sessions (one server page) and turn off "Collapse project
  sessions behind "Show more"": the open folder lists every loaded session with
  no Show more, and scrolling to its end loads the next page. Load more stays
  under the folder while pages remain; after three automatic loads (the default
  budget), further pages need a click on it. Turn the switch back on for the
  saved count plus Show more.

For the desktop Close shortcut, select a side-chat tab in an ordinary
session and press Cmd/Ctrl+W. Only that side chat should close; the parent and
siblings stay open. If stopping the side chat fails, its tab, draft and queued
messages must remain. Narrow the desktop window below 768 pixels and repeat:
the hidden rail's tabs stay open and the app returns to the new-session page.
Native accelerator delivery requires an Electron run; component tests cover
selection, visibility, and stop failure.

Own-environment sibling CUJs: `uv run --no-sync pytest
 tests/e2e_ui/sessions/test_sibling_sessions.py --ui-skip-build --reruns 0`.
This uses real replicas/hosts/runners with local sandbox allocation and a scripted
model. It covers desktop kebab/right-click/header and mobile long-press/header
composer navigation, shared/fresh compute, pin/project/fork actions, archive/undo,
and survivor messaging after representative deletion. It also checks that expanded
family rows hide actions when not hovered, deletion keeps peers nested before the
server responds, and personal pins appear only once. Set `OMNIGENT_DEMO_DIR`
outside the checkout to retain recordings.
The component tests in `web/src/shell/HeaderConversationMenu.test.tsx` and
`web/src/components/KeyboardShortcutsDialog.test.tsx` cover Cmd/Ctrl+Alt+A and
its discoverability.

## Gotchas

- Family membership is owner-only navigation context, not a sharing grant.
  Siblings start private even if the source has readers. Edit-shared sources
  cannot create siblings. Shared viewers see flat rows.
- Families are bounded to 100 retained members, including archived members.
  Use the row chevron to **Collapse session family** or **Expand session family**.
- Nested creation and same-sandbox fork selection require the default-off
  `OMNIGENT_FEATURES=sibling_sessions` flag. Reload the UI after enabling it.
  Opening a saved **Add nested agent** link while disabled or on an older server
  shows an unavailable message instead of a composer that cannot submit.
- If the source disappears after a rejected create, **Continue in a new session**
  carries the recovered prompt and attachments into the ordinary composer.
- Sandbox expiry and explicit whole-host destruction remain separate from
  deleting one session. Shared compute means a shared filesystem, not history.

- Existing side chats on separate runners must still be closed individually.
  Hostless CLI Stop keeps its existing per-conversation behavior.
- Starting a side chat after its parent stopped relaunches the parent. The new
  chat shares that replacement runner and stops with the parent again.
- A side chat has no host of its own. When its shared runner is gone, recovery
  goes through the parent. It requires edit access to the parent and, as for
  attaching a runner, ownership of the parent's runner. Another editor gets an
  error saying the parent runs on another user's runner, and their side chat
  stays unbound.
- Ask in side chat reuses only a visible, writable tab belonging to the current
  parent. Hidden, ended, and currently starting tabs are ineligible; another
  selection opens a new unsent tab. Ended chats do not offer Reply, and a
  read-only parent composer offers neither Reply nor Ask in side chat.
- A phone-width browser run does not prove the mobile side-chat drawer in the
  embedded web app or the native apps. Both style the drawer only inside the
  app's own page area: placed outside it, the drawer stays off screen when
  embedded and sits under the status bar in the native apps.
- Canvas is feature-gated, including conversation deep links. Its browser
  tests enable that flag explicitly; they do not enable it on a deployed app.
- Canvas entry collapses navigation once. Selecting another card must not
  collapse navigation that the user has manually expanded. Card dragging must
  not open a session. Workspace must not hide the canvas or change session
  focus; both panels resize to leave room for the conversation controls.

- Browser storage sharing is limited to one desktop window and app run;
  restarting the app clears it. Closing an individual tab does not.
- Archive and unarchive exist on the row, in bulk selection, and in the header
  menu. A fix to one of these does not reach the others; check each, and check
  that the undo toast restores the session.
- The header menu of an archived session must offer Unarchive, not Archive.
  Open an archived session directly to see it.
- The worktree prompt appears only when the server advertises worktree cleanup
  and the session has a worktree. A remembered answer skips it on every later
  archive; reset the choice in Settings → Git before re-testing.
- Forking copies files and images into the new session. After a fork, open the
  forked session and confirm the image still loads; the transcript text alone
  does not prove the file came along.
- Databricks Sandbox, Arclet, and managed sources fork to connected hosts and
  every offered sandbox provider, including shared sessions. Workspace
  recovery offers connected hosts only. Verify a managed sandbox fork with
  `tests/e2e_ui/fork_session/test_fork_managed_sandbox.py::test_fork_onto_managed_sandbox_with_no_host_online`.
- A custom agent outlives its sessions: forks of your own sessions share it,
  and forking someone else's session gives you your own copy. Appearing in the
  picker does not prove the fork API accepts it; check the bound agent after
  navigation.
- A copied web transcript does not prove the native CLI received that history.
  Native variants of the agent-switch test skip without `LLM_API_KEY`; record
  those skips and use a configured test harness before claiming native coverage.
- Continuing in another directory preserves conversation history, not files
  from the missing workspace. A new worktree starts from the `HEAD` of the
  chosen repository's main checkout, never from the missing worktree's branch.
  Existing worktree recreation and sandbox creation belong to the ordinary fork
  dialog, not this recovery action.
- The reconnect command depends on why the session stopped (host offline vs. a
  stranded local session vs. a sandbox). Reproduce the reporter's reason, not
  just any stopped session.
- A web-composer message to a native session whose host is offline waits as
  "Waiting for host" instead of becoming a failed turn (see
  [Composer](./composer.md)). The offline-host recipe posts through the API,
  which still records the failed turn.
- The desktop app's direct reconnect is not available in a browser; a browser
  shows the command instead.
- Tests marked "own environment" fail under `verify-env run` with an explicit
  message. That is expected; run them with plain `uv run pytest`.
- Custom positions apply to loaded rows: a session moved up from deep history
  takes its place again once its page loads. History loaded after Custom started
  follows Created or Updated until a move below it saves those positions. A move
  made while Created or Updated is selected starts Custom over from that list. Each
  tab reads the saved order once, so another tab's change shows after reload and
  the last tab to save wins. Only your own live sessions can be dragged; shared
  and archived rows keep their Custom place but can't be moved.
- Grouping and ordering apply to the loaded pages only: an older session joins
  its group once it is paged in. Grouped views hide Projects, so dragging a
  session onto the list never removes it from its project there.