Skip to content

Agents means "my conversations": land on the list, and give a session a way out - #2383

Merged
2witstudios merged 1 commit into
masterfrom
pu/agents-land-on-conversation-list
Aug 10, 2026
Merged

2witstudios merged 1 commit into
masterfrom
pu/agents-land-on-conversation-list

Conversation

@2witstudios

@2witstudios 2witstudios commented Aug 10, 2026 •

Copy link
Copy Markdown
Owner

The problem, as reported

The product owner could not reach their conversation history at all, on mobile, in production.

It is not a data problem. listAllConversationsPaginated (apps/web/src/lib/agent-workspaces/workspace-conversations-runtime.ts) already leftJoins the membership node, so conversations with no node still list. The list was simply unreachable.

AgentsSurface is a ternary: selectedSessionId set → <AgentPanes>, nothing selected → the list. So the list renders only while nothing is selected — and the only three things that clear a selection all require the session to be destroyed first:

where what has to happen
AgentsSurface onSessionEnded the end-session dialog confirms
AgentsSidebar End Session the same, from the sidebar
AgentsSurface's GC effect the server answers that the workspace no longer exists

And the nav item carried the live selection forward, so clicking Agents reopened the last session rather than leaving it.

Combined: a room with a door in and no door out. Browsing your own history meant ending the work you were in the middle of.

What changed

1. The Agents nav item stops carrying a selection. agentsHref is built from EMPTY_AGENT_SELECTION unconditionally.

This changes what the nav item means, not the "the URL is the state" design the old behaviour was written to serve. hydrateFromSearch is untouched. Deep links, refresh, popstate, the tab bar's own saved search, and the sidebar's session rows all still carry and restore a full selection through the same grammar — they just aren't spelled by this one link any more.

Three things I checked rather than assumed:

  • It actually navigates. Clicking Agents from inside a session is a same-pathname / different-query URL. The store writes with raw history.pushState, which Next folds into its router state, so the router's canonical URL genuinely differs from the link's and <Link> performs a real navigation — useSearchParams() updates, the hydrate effect re-runs against an empty search, the selection clears. Confirmed in a browser (below), not reasoned about.
  • Active-state highlighting still works. With no ? left in any href, isActive compares pathname directly; the split("?") that existed for this one entry is now dead and removed. Agents stays lit across the whole surface via its existing exact: false. Visible in the screenshot below.
  • useTabSync already syncs on pathname/search change, so a Link navigation updates the tab bar's record without help.

2. A session gets a persistent way out — an "All conversations" control above the grid.

The landing-page behaviour is the stated requirement, but it is the narrower fix for this root cause: without a back control, the only in-session exit is still a destructive one. selectSession(null) drops the selection and nothing else — the workspace, its panes, its PTYs and any streaming reply are untouched, and the sidebar row reselects it.

It lives in AgentsSurface, not AgentPanes, because it is the console's control: the same grid also mounts inside AgentPageView, where there is no conversation list to go back to. The grid moves into a min-h-0 flex-1 track so the header is subtracted by the flex layout rather than overflowing SessionPanes' own h-full.

Verification

Gates (monorepo-wide, from a bootstrapped worktree with packages/db and packages/lib rebuilt):

bun run typecheck    Tasks: 17 successful, 17 total
bun run lint         Tasks: 15 successful, 15 total
bun run knip:check   [ok] knip: 4 issue(s), all within baseline (4).
bun run --filter web test
                     Test Files  1128 passed | 1 skipped (1129)
                          Tests  16795 passed | 6 skipped (16801)

A note on that test run: the suite first showed 25 failures, all in *.integration.test.ts and all 42P01 relation does not exist — the shared pagespace_test database on 5433 has no tables. Migrating a fresh database and pointing DATABASE_URL at it turns the whole suite green, so those were an unmigrated local DB, not this branch. (Running bun run db:migrate from the repo root silently migrates the wrong database — the root .env wins over the passed variable. Run it from packages/db.)

Mutation-checked, each fix broken and the named test watched go red, then restored:

mutation red
agentsHref re-attaches selectedSessionId both "carries no selection" tests
isActive forced to false "still highlights as active while a session is open"
back control's onClick → no-op "All conversations" drops the selection and shows the list

One test was wrong on the first pass and is worth naming: the highlight assertion toContain('bg-accent') passed against a deliberately broken isActive, because the inactive class string contains hover:bg-accent. It compares class tokens now.

Manual, in a real browser, at 390×844 — production build (next start; bun run dev can't be used for this, the CSP has no dev exemption for Next's unsafe-eval), real Postgres, seeded user and drive, driven with Playwright:

  1. /dashboard/{drive}/agents → conversation list.
  2. New Session → Global Assistant → URL becomes ?workspace=…&c=…, the pane grid renders, and the "All conversations" control is visible above it with the composer still correctly pinned to the bottom of the viewport (no overflow from the added header).
  3. Click All conversations → URL drops to /dashboard/{drive}/agents, the list renders, and the session is still listed in the sidebar (screenshot below shows both sessions alive).
  4. Browser Back → returns to ?workspace=…&c=…. The history entry is real.
  5. Open the sheet → click Agents → URL /dashboard/{drive}/agents, list renders. This is the reported bug, fixed. The nav item is highlighted in that same screenshot with no selection in its href.

Known trade-off: returning to the list unmounts the grid

Raised in review (P1). Accurate on the mechanism — XtermTerminal's cleanup emits shell:disconnect (XtermTerminal.tsx:390), which removes the viewer and, if it was the last one, arms the PTY's idle reap; SessionPanes uses invisible rather than unmounting for exactly this reason.

It is not a new cost. AgentPanes is key={selectedSessionId}, so switching to a different session already unmounts the previous session's terminals and pays the same price on every sidebar click. And the behaviour actually being asked for — Agents lands on the conversation list — unmounts the grid too, since it is a query-string change on the same route. Making the back button uniquely non-destructive would leave the two doors out of a session behaving differently.

A keep-alive host would cover both and is feasible, but it is the MachineKeepAliveHost pattern this surface's docblock says "has no successor here because it has no problem to solve" — worth doing deliberately (it would also make session switching non-destructive, the bigger win) rather than as a footnote on a nav-href fix.

Found, not fixed

  • The ?c= param can appear on the bare agents URL before any session is picked (/dashboard/{drive}/agents?c=<id> on first load) — something upstream of this surface seeds a global-assistant conversation id into the URL. Harmless here (no workspace=, so the list still renders) and out of scope for this fix.
  • Reaching the nav on a narrow viewport still requires opening the left sheet first. That is the existing mobile layout, not something this changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DMesaDDBUUcxXYS29uDUp7

Summary by CodeRabbit

  • New Features

    • Agents navigation now opens the conversation list.
    • Active sessions include an All conversations control for returning to the list without ending the session.
    • Session, conversation, and agent selections are cleared when returning to the conversation list while workspace context is preserved.
  • Bug Fixes

    • Improved selection restoration across bookmarks, shared links, page refreshes, and browser Back navigation.

…ssion has a way out

`AgentsSurface` renders the conversation list ONLY while nothing is selected,
and the three things that clear a selection all require the session to be
destroyed first (`onSessionEnded`, the sidebar's End Session, and the GC that
fires when the server says the workspace is gone). The Agents nav item then
carried the live selection forward, so it reopened the last session instead of
leaving it. Together that made the surface a room with a door in and no door
out: a user parked in a session could not reach their own history without
ending the work they were in the middle of.

Two changes, one root cause.

**The nav item stops carrying a selection.** `agentsHref` is now built from
`EMPTY_AGENT_SELECTION` unconditionally. This changes what the NAV ITEM means,
not the URL-is-the-state design it was written to serve: `hydrateFromSearch` is
untouched, and deep links, refresh, `popstate` and the sidebar's own session
rows still carry and restore a full selection through the same grammar. The
link is a real route-level navigation to a URL that differs from the current
one only in its query string, which Next's router treats as a navigation —
`useSearchParams()` updates, the hydrate effect re-runs against an empty
search, and the selection clears. Verified in a browser, not inferred.

With no "?" left in any href, `isActive` compares `pathname` to `item.href`
directly; the `split("?")` that existed for this one entry is gone, and Agents
stays highlighted across the whole surface via its existing `exact: false`.

**A session gets a persistent way out.** "All conversations" above the grid
calls `selectSession(null)` — the selection drops, the workspace does not.
Browsing history should never require destroying a live session, and the nav
fix alone would still have left the only in-session exit being a destructive
one. It lives in `AgentsSurface`, not `AgentPanes`, because it is the console's
control: the same grid also mounts inside `AgentPageView`, where there is no
conversation list to go back to. The grid moves into a `min-h-0 flex-1` track
so the header is subtracted by the flex layout rather than overflowing
`SessionPanes`' own `h-full`.

Tests: a new `PrimaryNavigation.test.tsx` pins the href carrying no selection
in drive and global mode, and the active highlight surviving it; two new
`AgentsSurface` tests pin the back control clearing the selection and the URL
while issuing no DELETE and leaving the workspace in the store, and its absence
when nothing is selected. All four mutation-checked. The first draft of the
highlight test passed against a deliberately broken `isActive` because
`toContain('bg-accent')` matched the inactive state's own `hover:bg-accent`; it
compares class tokens now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMesaDDBUUcxXYS29uDUp7
@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Agents navigation now opens the conversation list without restoring selections. An active session shows an “All conversations” control that clears only the session selection. Tests cover URL, workspace, network, and active-link behavior.

Changes

Agents navigation and session flow

Layer / File(s) Summary
Clear selections from Agents navigation
apps/web/src/components/layout/left-sidebar/PrimaryNavigation.tsx, apps/web/src/components/layout/left-sidebar/__tests__/PrimaryNavigation.test.tsx
Agents links retain drive scope but omit session, conversation, and agent selections. Active-link matching compares directly with the navigation URL.
Return from an active session
apps/web/src/components/agents/AgentsSurface.tsx, apps/web/src/components/agents/__tests__/AgentsSurface.test.tsx, CHANGELOG.md
The “All conversations” control clears the selected session without deleting it or changing the workspace. Tests cover URL and network behavior, and the changelog records the navigation behavior.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant PrimaryNavigation
  participant AgentsSurface
  participant URL
  User->>PrimaryNavigation: Open Agents
  PrimaryNavigation->>URL: Build Agents URL without selections
  URL-->>AgentsSurface: Load conversation list
  User->>AgentsSurface: Select a session
  AgentsSurface->>URL: Set session selection
  User->>AgentsSurface: Choose “All conversations”
  AgentsSurface->>URL: Clear session selection
  URL-->>AgentsSurface: Show conversation list
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes both main changes: opening the Agents conversation list and providing a way to leave an active session.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/agents-land-on-conversation-list

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b864d25674

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

variant="ghost"
size="sm"
className="h-7 gap-1 px-2 text-muted-foreground hover:text-foreground"
onClick={() => selectSession(null)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep live panes mounted when returning to the list

When the selected session contains a terminal, this click clears selectedSessionId, so the outer conditional replaces the entire AgentPanes tree with the conversation list rather than merely hiding it. That unmounts XtermTerminal, whose teardown emits shell:disconnect; if this was the last viewer, the realtime service detaches the PTY and arms its 30-minute idle reap, while SessionPanes explicitly documents that such unmounts can lose final output/exit state and cause a cold-started PTY on return. The supposedly nondestructive escape therefore disrupts or eventually kills live terminal work; keep the session panes mounted (for example, in an invisible keep-alive container) while displaying the list.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct on the mechanism, and I checked it rather than taking it on trust: XtermTerminal's cleanup does emit shell:disconnect (XtermTerminal.tsx:390), and SessionPanes really does use invisible rather than unmounting for exactly this reason.

Where I land differently is on whether this is a NEW cost.

AgentPanes is key={selectedSessionId}. Switching to a different session therefore already unmounts the previous session's terminals and pays precisely this price — disconnect, viewer removed, idle reap armed if last, cold PTY on return. That is today's accepted behaviour for every click between sessions in the sidebar. "Return to the list" costs what "switch to another session" costs; it does not open a new class of harm, it reuses an existing one.

The larger point against fixing it here: the requested behaviour is that clicking Agents in the nav lands on the conversation list, and that path unmounts the grid too — it is a query-string change on the same route, so AgentsSurface re-renders with a null selection and the panes come down the same way. Making the back button uniquely non-destructive would leave the two doors out of a session behaving differently, and the nav one is the one the owner actually asked for.

A keep-alive host would cover both, and it is feasible — but it is the MachineKeepAliveHost pattern this surface's own docblock says "has no successor here because it has no problem to solve", so re-introducing it is a design change with its own review, not a footnote on a nav-href fix. Recorded as a stated trade-off in the description rather than silently accepted; if terminals-survive-the-list is wanted, that is worth doing deliberately and would also make session switching non-destructive, which is the bigger win.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked as #2385 rather than left as a stated trade-off, and the issue records what I verified in the handler — because the summary above overstates the cost in two ways worth correcting for whoever picks it up.

Detaching removes a viewer, not the session. disconnectConnection → removeViewer (shell-handler.ts:1529-1537), the PTY keeps running, scrollback is buffered and persisted (:188-203), and reattach is a built-in fast path that hands back the scrollback and cancels the pending reap (:1552-1561). So navigation does not lose terminal data.

And the reap is quiet-based, not detachment-based: agent input into a viewer-less session re-arms it, with armIdleReap's docblock giving the reason — "an agent driving a headless shell is activity, and reaping mid-command thirty minutes after it started would kill work in progress" (:481-500).

The genuine exposure is narrower than "disrupts or eventually kills live terminal work": a human-started long-running command, with no agent driving it, left unattended past DETACHED_IDLE_MS (30 min, terminal-session-map.ts:5) — plus a missed shell:closed exit status if a command finishes with nobody attached.

Where you are right, and it is the part that makes this worth tracking: the frequency changes. AgentPanes is key={selectedSessionId}, so session switching already paid this exact price — but leaving a session used to be nearly impossible, and is now the default landing behaviour. Same mechanism, moved onto the happy path.

Keeping the thread open for your read on the severity correction.

2witstudios added a commit that referenced this pull request Aug 10, 2026
Both branches were adding to the top of Unreleased/Fixed, which conflicts on
whichever merges second. Same entries, anchored after the node-tree cluster
instead, so the two hunks do not overlap.
@2witstudios
2witstudios merged commit 482b8c0 into master Aug 10, 2026
4 checks passed
@2witstudios
2witstudios deleted the pu/agents-land-on-conversation-list branch August 14, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant