feature-map/sessions.md
Rama main · réplica de lectura del repositorio original.
# 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.