feat(desktop): session-list overhaul + cancellable install#37379
Conversation
Long-running sessions auto-compress: the gateway ends the original session and surfaces the live continuation under a new id (list_sessions_rich projects the root forward to its tip). Two symptoms fell out of the id rotation: - A pinned session "vanished" — the pin is stored as the pre-compression root id, but the sidebar only matched on the live id, so it was filtered out. Pins now resolve on the durable lineage-root id (`_lineage_root_id`, already surfaced by the projection): the sidebar indexes sessions by both ids, pin/ unpin and reorder operate on the durable id, and `sessionPinId()` is shared with the Cmd+P toggle. Existing pins keep working with no migration. - A freshly-continued session was missing from the list until you ungrouped + "load 50 more" — the list paginated by original start time, so an old-but- active conversation sat past the first page. The desktop now requests `order=recent` (GET /api/sessions gains an `order` param backed by the existing recency CTE), surfacing live continuations on the first page.
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
2 |
First entries
tests/hermes_cli/test_web_server.py:327: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `Connection | None`
tests/hermes_cli/test_web_server.py:325: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
✅ Fixed issues: none
Unchanged: 5012 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
- Sidebar: rows within a workspace group now sort by creation time instead of last activity, so they stop reshuffling every time a message lands (muscle memory). Groups still float up by recency. - Sessions only persist a workspace cwd when one was explicitly chosen; an auto-detected launch directory is no longer stamped on the row, so untargeted sessions group under "No workspace" instead of "desktop". The agent still runs in the detected directory.
Adds a search box above the session list. Loaded sessions match instantly client-side; a debounced full-text search (existing /api/sessions/search FTS) covers the rest so all sessions stay findable at 699+. Results replace the pinned/agents sections while a query is active and resume on click.
The install overlay had no way to stop a running install — the runner already supported an abortSignal, but nothing drove it. Wire it end to end: - main.cjs holds an AbortController for the active runBootstrap and aborts it on a new hermes:bootstrap:cancel IPC and on app quit, so quitting/cancelling mid-install actually kills install.sh/ps1 instead of orphaning it. - runBootstrap bails before spawning anything if the signal is already aborted. - Install overlay gains a "Cancel install" button while a bootstrap is active; a cancel surfaces the recovery overlay (retry/repair). Test: electron/bootstrap-runner.test.cjs asserts the already-aborted early return (no spawn) via `node --test`.
There was a problem hiding this comment.
Pull request overview
This PR overhauls the desktop session list behavior to stay stable and searchable across session auto-compression (continuation tips), and adds end-to-end cancellation for the first-launch installer flow.
Changes:
- Add durable session lineage support for pinning and “recent” ordering so compressed conversations remain visible and correctly pinned.
- Update workspace grouping semantics so only explicitly-chosen workspaces persist (defaulting others to “No workspace”), and stabilize intra-group ordering.
- Add a sidebar session search UI (local filter + debounced server FTS) and add cancellable bootstrap install (IPC + AbortController + runner short-circuit).
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tui_gateway/server.py | Persist cwd only when explicitly chosen; track explicit_cwd on session creation / cwd updates. |
| tests/test_tui_gateway_server.py | Adds tests for explicit-cwd persistence and default “No workspace” behavior. |
| hermes_cli/web_server.py | Adds order query param to /api/sessions and passes through to SessionDB.list_sessions_rich. |
| tests/hermes_cli/test_web_server.py | Tests order validation and “recent” ordering surfacing compression tips with _lineage_root_id. |
| apps/desktop/src/types/hermes.ts | Extends SessionInfo with optional _lineage_root_id for compression lineage. |
| apps/desktop/src/store/session.ts | Adds sessionPinId() helper to pin by durable lineage root id. |
| apps/desktop/src/store/session.test.ts | Unit tests for sessionPinId() behavior (live id vs lineage root). |
| apps/desktop/src/hermes.ts | Updates listSessions() to request order=recent by default. |
| apps/desktop/src/global.d.ts | Adds cancelBootstrap API typing; minor type formatting cleanup. |
| apps/desktop/src/components/desktop-install-overlay.tsx | Adds “Cancel install” button + cancelling UI state and layout tweaks. |
| apps/desktop/src/app/desktop-controller.tsx | Toggles pins using durable lineage-root id via sessionPinId(). |
| apps/desktop/src/app/chat/sidebar/virtual-session-list.tsx | Pins from virtual list now use sessionPinId(session). |
| apps/desktop/src/app/chat/sidebar/index.tsx | Implements durable pin resolution, stable per-workspace ordering, and session search UI. |
| apps/desktop/package.json | Adds electron/bootstrap-runner.test.cjs to the desktop platform test script. |
| apps/desktop/electron/preload.cjs | Exposes cancelBootstrap IPC to the renderer. |
| apps/desktop/electron/main.cjs | Tracks an active bootstrap AbortController; handles cancel IPC and abort-on-quit. |
| apps/desktop/electron/bootstrap-runner.cjs | Adds early abort short-circuit before any spawn when signal is already aborted. |
| apps/desktop/electron/bootstrap-runner.test.cjs | Tests that early-aborted signals bail out before spawning anything. |
Comments suppressed due to low confidence (1)
apps/desktop/src/components/desktop-install-overlay.tsx:219
cancellingis set totruewhen the user clicks Cancel, but it is never reset. If the IPC handler is missing/returns no-op (older Electron build, transient IPC error, etc.) whilestate.activeremains true, the Cancel button will stay disabled for the rest of the install attempt. Resetcancellingwhen the bootstrap becomes inactive (and before starting a new bootstrap).
export function DesktopInstallOverlay({ enabled = true }: DesktopInstallOverlayProps) {
const [state, setState] = useState<DesktopBootstrapState>(EMPTY_STATE)
const [logOpen, setLogOpen] = useState(false)
const [copied, setCopied] = useState(false)
const [cancelling, setCancelling] = useState(false)
const [now, setNow] = useState(() => Date.now())
const logEndRef = useRef<HTMLDivElement | null>(null)
// Tick once a second while a bootstrap is in flight so running steps show a
// live elapsed timer. Stops when nothing is active to avoid idle renders.
useEffect(() => {
if (!state.active) return
const id = window.setInterval(() => setNow(Date.now()), 1000)
return () => window.clearInterval(id)
}, [state.active])
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| activeRoot: backend.activeRoot, | ||
| sourceRepoRoot: SOURCE_REPO_ROOT, | ||
| hermesHome: HERMES_HOME, | ||
| logRoot: path.join(HERMES_HOME, 'logs'), | ||
| abortSignal: bootstrapAbortController.signal, |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
A batch of desktop session-list fixes (plus a cancellable installer), most rooted in auto-compression rotating a conversation's session id — the gateway ends the original session (
end_reason='compression') andlist_sessions_richprojects the root forward to its live continuation tip.Pinned session "vanished" after compression
A pinned long-running session disappeared from the Pinned list (no data loss — the conversation is live under the new continuation id). The pin is stored as the pre-compression root id, but the sidebar matched only on the live id, so the projected tip never matched and the pin was filtered out.
Fix: resolve pins on the durable lineage-root id (
_lineage_root_id, already surfaced by the projection, stable across every compression). The sidebar indexes sessions by both ids; pin/unpin/reorder and the Cmd+P toggle use a sharedsessionPinId(). Existing pins keep working with no migration.Freshly-continued session missing from the list
After a compression the continuation was invisible until you ungrouped and hit "load 50 more", because the list paginated by original start time. The desktop now requests
order=recent;GET /api/sessionsgains anorder(created|recent) param backed by the existing recency CTE, so live continuations land on the first page.Stable in-directory ordering
Rows within a workspace group reshuffled on every new message (sorted by last activity). They now sort by creation time within a group, so muscle memory holds; groups still float up by recency.
"No workspace" as the default
Sessions only persist a workspace cwd when one is explicitly chosen; an auto-detected launch directory is no longer stamped on the row, so untargeted sessions group under "No workspace" instead of "desktop". The agent still runs in the detected directory.
Sidebar session search
A search box over the session list: loaded sessions match instantly client-side; a debounced full-text search (existing
/api/sessions/searchFTS) covers the rest so all sessions stay findable at 699+.Cancellable first-launch install
The install overlay had no way to stop a running install. Wired end to end:
main.cjsholds anAbortControllerfor the activerunBootstrapand aborts on a newhermes:bootstrap:cancelIPC and on app quit (so quitting/cancelling actually killsinstall.sh/ps1); the runner bails before spawning if already aborted; the overlay gains a Cancel install button.Tests
tests/hermes_cli/test_web_server.py—ordervalidation + a recency/compression-tip surfacing test asserting_lineage_root_idis exposed.tests/test_tui_gateway_server.py— explicit-cwd persists, no-cwd defaults to "No workspace".apps/desktop/src/store/session.test.ts(durable pin id) +apps/desktop/electron/bootstrap-runner.test.cjs(abort short-circuits before any spawn).npm run type-check+ lint clean for new code; full suites green in CI.Not included (need a repro/screenshot or external spec)