Skip to content

The agent's chat in a right-hand sidebar, opening sessions in the workspace's folder - #670

Merged
danbarua merged 6 commits into
mainfrom
chat-sidebar
Oct 12, 2026
Merged

danbarua merged 6 commits into
mainfrom
chat-sidebar

Conversation

@danbarua

@danbarua danbarua commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Problem

The agent's chat was a page of its own (/agent), reached from a link in the top bar, and its sessions opened in one folder the dev stack names (.labkit-dev/labkit-effect/workspace), whatever workspace the Trace Console showed. Its /new command reloaded the page. A session the address named that the agent could not reopen disabled the composer, so /new could not be sent. The gallery's failed-turn tiles logged "Maximum update depth exceeded".

Change

The chat in the Trace Console

  • Right sidebar. In development, inside a workspace, the chat sits at the right of the page. A rail at the right edge stays when the chat is minimised; its chat button expands or minimises it. The top bar's agent link is removed.
  • Session lifetime.
    • The session connects the first time the chat is expanded.
    • It stays connected while the chat is minimised; only its view is not drawn.
    • The address names it (?session=), so a reload or a link reopens it.
  • Resizing. The border at the chat's left edge moves by drag, Left and Right (16 px), Home (320 px) and End (60% of the window). A double-click returns it to 400 px. The browser keeps the width.
  • Motion. The chat slides open and shut over 220 ms, as the left sidebar does, and follows the pointer without the transition while its border is dragged.

Sessions open in the workspace's folder

  • Resolver. readWorkspaceDirectories (src/server/workspace-directories.ts) reads workspaces.local.json and checks each entry. openRepositories opens the repository in each folder it returns. The API and the dev server both use it.
  • Dev-server endpoint. The dev server answers GET /agent-workspace/<slug>:
    • with { "cwd": "<folder>" } for a workspace the file maps;
    • with { "cwd": null, "reason": "…" } for one it does not map.
      It reads the file on each request, so an edit needs no restart.
  • The session's cwd. The chat sends that folder as the session's cwd. labkit-effect's ACP host roots each session's tools in its cwd, and filters session/list by it, so a workspace's sessions are its own.
  • Unmapped workspace. A workspace with no folder has no chat. The rail's button is disabled, and its tooltip gives the reason.
  • Leaving a workspace. On leaving a workspace, the session the address names is not reopened in the next one, and it leaves the address. Going home drops it too.

The agent's session, shared

  • Components. useAgentSession and AgentChat (src/ui/agent/) hold what the /agent page did. A page passes the session to reopen and is told the id of each session started. The /agent page uses them too, keeps its own address scheme, and still opens its sessions in VITE_LABKIT_ACP_CWD.
  • /new. Sending /new now connects a new session in place, without reloading the page.
  • A failed open or reopen. If opening or reopening a session fails, the composer takes input again, and the error ends with "Send /new to start a new session."

A render loop in @labkit/ui. currentActivity makes a new object on each call. useLingering stored it from an effect that depends on it, so each render set state and drew again, until React stopped it with "Maximum update depth exceeded". An activity equal to the one held now keeps it (sameActivity). The loop affected a failed turn and a running one: the gallery's failed-turn tiles, and a chat whose session failed to reopen.

Controls

  • Narrow windows. In a window 860 px wide or narrower, the chat is not a drawer: it takes its width from the page beside it. The left sidebar's drawer is unchanged.
  • A new session on each visit. Once the chat has been expanded on a page, each workspace visited opens its own session, even while the chat is minimised. Going home and back opens another session in the same workspace. With the real agent, each one is a saved session. A session could wait to connect until the chat is expanded in each workspace; that is a small change if wanted.
  • Where the folder comes from. The browser learns the folder from the dev server, and sends it to the agent as ACP's session/new requires. The server does not yet check that a session's cwd is the workspace's folder. That check is where the design is heading, and it is left for later.
  • The /agent page, the sessions list and session replays still use VITE_LABKIT_ACP_CWD. Nothing links to them.
  • A closed connection's DELETE can be answered 404. Closing a connection ends its event stream and then sends DELETE /acp. The ACP server (@agentclientprotocol/sdk 1.5.0) forgets a connection whose stream has ended (trackConnectionClose), so the DELETE can find nothing, and the browser logs the 404. The connection is closed either way. The browser tests excuse one such log line for each DELETE answered 404; any other 404 still fails a test.
  • Seen in a browser. On :8850, a session opened in bonsai-2026 listed ~/Code/PyCharm/bonsai-2026's own root. I did not look at the layout myself.

Verification

  • bun run check: exit 0, 1548 pass, 0 fail.
  • bun run e2e with E2E_PORT_API=9019 E2E_PORT_DEV=8970 E2E_PORT_BUILT=8971: 161 passed, 7 skipped (the dev-only tests, in the built bundle).
  • Unit tests:
    • tests/workspace-directories.test.ts: an absolute folder and a ~/ folder are mapped. A missing file, a file that is not a JSON object, and an entry that is not a string or not absolute are skipped, each with its log line.
    • tests/repositories.test.ts: a folder that holds a repository is opened, and one that does not is skipped with a warning.
    • packages/ui/tests/activity.test.tsx: two equal activities, including a failed turn's two new objects, are the same activity; another kind or tool label is not.
  • e2e, dev server (the built bundle asserts the rail is absent):
    • The rail is at the page's right edge in a workspace, and absent at /app/.
    • Expanding the chat sends session/new with alpha's mapped folder as cwd, and names the session in the address.
    • Minimising and expanding again keeps the session. Opening another record keeps it in the address, and a reload reopens it.
    • In beta, which the e2e mapping file does not map, the button is disabled, and its tooltip says the file maps no folder to beta. Going home dropped the session from the address.
    • With ?session=no-such-session, the chat says the reopen failed and suggests /new. The composer is enabled, and /new starts a session that the address then names.
    • /agent starts a session and takes its address, and /new starts another without a reload.
    • Dragging the chat's border 100 px left widens it from 400 to 500 px, and a reload keeps 500. Right gives 484, Home 320, End 768 (60% of 1280), and a double-click 400.
    • The gallery draws every tile without an error.
  • Before and after the render-loop fix, with 4 workers:
    • The stale-session test failed 8 of 8 runs before the fix.
    • A test that only opens the gallery failed 4 of 12 runs on main.
    • After the fix, the stale-session and gallery tests passed 16 of 16 runs.
    • The loop's stack, captured from React's warning, pointed at useLingering's effect.

Linked issues

None. This follows #669.

🤖 Generated with Claude Code

danbarua and others added 6 commits October 12, 2026 01:17
…new starts a session in place

useAgentSession and AgentChat hold what the /agent page did: connect,
reopen a session by id, the status line and the conversation. A page
passes the session to reopen and is told the id of each session started,
so the /agent page keeps its address scheme. Sending /new now connects a
new session without reloading the page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…that stays when it is minimised

In a workspace, in development, a rail at the right edge holds the chat's
button, which expands or minimises the chat. The session connects the
first time the chat is expanded, stays connected while it is minimised,
and is named in the address (?session=), so a reload or a link reopens
it. The top bar's agent link is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… to folder

readWorkspaceDirectories reads the mapping file and checks each entry;
openRepositories opens the repository in each folder it returns. The
dev server can now read the same folders for the agent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… turn stops redrawing forever

currentActivity makes a new object on each call. useLingering stored it
from an effect that depends on it, so each render set state and drew
again: React stopped it with "Maximum update depth exceeded". This
showed in the gallery's failed-turn tiles and in a chat whose session
failed to reopen. An activity equal to the one held now keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaves /new usable

- The dev server answers GET /agent-workspace/<slug> with the folder the
  workspaces mapping file gives the workspace, or cwd null and the
  reason. The chat sends that folder as the session's cwd. A workspace
  with no folder has no chat; the rail's button is disabled and says why.
- A session is its workspace's own: on leaving the workspace, the
  session the address names is not reopened and leaves the address.
- When opening or reopening a session fails, the composer takes input
  again and the error says to send /new.
- The e2e stack maps alpha to the git fixture's folder. The browser
  tests excuse the 404 the ACP server can answer to the DELETE of a
  connection whose stream it has already ended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A drag, Left and Right (16 px), Home (320 px) and End (60% of the
window) move it; a double-click returns it to 400 px. The chat follows
the pointer without its slide transition while the border is dragged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the area:web The web app and shared UI: app-web, ui, design, records, view-model, acp-client label Oct 12, 2026
@danbarua
danbarua merged commit 83da0d1 into main Oct 12, 2026
10 checks passed
@danbarua
danbarua deleted the chat-sidebar branch October 12, 2026 11:14
danbarua added a commit that referenced this pull request Oct 12, 2026
…dows, and lists sessions with /sessions (#671)

## Problem

After #670, once the chat had been expanded on a page, every workspace
visited opened a new session, even with the chat minimised. Going home
and back opened another session in the same workspace. With the real
agent, each of these was a saved session. In a window 860 px wide or
narrower, the chat took its width from the page beside it. The only way
to open an earlier session was the separate sessions page.

## Change

**When a session connects**
- A workspace's session connects the first time the chat is shown in
that workspace. A workspace the chat is never shown in has no session.
- Each workspace keeps its session for the life of the page. Leaving a
workspace and coming back reopens the session it had (`session/load`)
and names it in the address again, instead of starting another.
- The chat's state moves into one hook, `useChat` (`chat-sidebar.tsx`).
It holds the workspace's folder, which workspaces the chat has been
shown in, and each workspace's session.

**Narrow windows**
- In a window 860 px wide or narrower, the chat is a drawer over the
page from the right, at most as wide as the window. Its button moves
from the rail to the top bar, beside the theme button.
- The rail and the border to drag are left out at that width.
- The drawer uses the same scrim as the sidebar's drawer, and opening
one drawer closes the other.

**`/sessions`**
- In the Trace Console's chat, `/sessions` opens a palette of the
sessions the agent keeps for the workspace's folder (ACP `session/list`
by `cwd`).
- The list is newest first, with each session's title, last update and
id, and the open session marked. It is asked for each time the palette
opens.
- Picking a session reopens it and names it in the address.
- The `/agent` page, which has no workspace, has no `/sessions`. The
sessions list page stays, with no link to it.

**Fixed in passing.** A production build no longer asks the dev server
for a workspace's folder. It answered 404, which the browser logged.

## Controls

- **Session list ordering.** A session the agent gives no time for goes
last.
- **Narrow window drawer width.** The drawer is as wide as the chat's
saved width (400 px until its border is moved), or the window if that is
narrower. On a phone it covers the page, and its button in the top bar
closes it.
- **Server-side checks** on a session's `cwd` are parked while the agent
runtime is reworked.
- **Not seen in a browser** by me this session; the layout at narrow
widths in particular needs a look.

## Verification

- `bun run check`: exit 0, 1548 pass, 0 fail.
- `bun run e2e` with `E2E_PORT_API=9019 E2E_PORT_DEV=8970
E2E_PORT_BUILT=8971`: 164 passed, 10 skipped (the dev-only tests, in the
built bundle).
- **New e2e tests** (dev server):
- Opening `alpha` and following a link with the chat minimised sends no
`session/new` or `session/load`. Expanding it sends one `session/new`.
Minimising, going home and opening `alpha` again names the same session
in the address and sends one `session/load`.
- In a 600 px wide window, there is no right rail. The top bar's chat
button opens the chat as a drawer in the viewport, and a click on the
scrim beside it closes it.
- `/new` starts a second session. `/sessions` opens a palette named
"Sessions". Picking the first session reopens it, and the address names
it.
- **Changed:** the build's rail test passes again, because the build no
longer fetches a folder.

## Linked issues

None. This follows #670.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:web The web app and shared UI: app-web, ui, design, records, view-model, acp-client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant