Skip to content

Every page shares one frame: the Trace Console's top bar, palette and theme - #669

Merged
danbarua merged 1 commit into
mainfrom
ui-shared-styles
Oct 12, 2026
Merged

danbarua merged 1 commit into
mainfrom
ui-shared-styles

Conversation

@danbarua

Copy link
Copy Markdown
Owner

Problem

The gallery, the sessions list, session replays, the agent page and the error pages drew a header and a palette from before the Trace Console's. Their header (Bar.tsx) had its own old dark palette, bordered buttons and links to the gallery and the sessions list. The Trace Console's top bar was separate CSS under .trace, so no other page could use it, and the right-hand sidebar planned next would have copied it again.

Change

  • One frame. src/ui/chrome/Frame.tsx and chrome.css hold what every page shares:
    • the palette's short names (--panel, --line, --accent and the rest, as aliases of the --lk-* tokens);
    • the top bar, the rail, the resizable border, and the reset rules and .mono, .dim and .faint;
    • the page styles the old pages used: .page, the sessions table, .load-error, and quiet buttons in the top bar.
      These rules moved out of trace.css and styles.css, with their selectors changed from .trace to .lk-frame. The Trace Console's root now has .lk-root lk-frame trace.
  • TopBar and Frame. TopBar draws the LabKit mark and name, which are one link (or a button, when onHome is given), then the page's own controls, then end controls at the right, then the theme button. Frame is the page root with a TopBar. The Trace Console, the gallery, the sessions list, replays, the agent page, the load-error page and the not-found page all use them. Bar.tsx is deleted.
  • One theme. useSavedTheme is now one shared setting (useSyncExternalStore), not state per component. Before, a page that drew content in the theme and a toggle in the bar each kept their own copy.
  • Links. The top bar links to neither the gallery nor the sessions list. Both pages stay reachable by their addresses. The agent link stays until the chat moves into the sidebar.
  • styles.css keeps only the page reset and the graph canvas rules. The old :root palette, the #bar rules and the global button and select styles are gone.
  • docs/architecture.md describes the frame.

Controls

  • No visible redesign of the Trace Console. Its top bar is unchanged except that the live · host text and sidebar toggle already went in earlier PRs.
  • The old pages look different. They now use the Trace Console's palette, 52 px top bar and quiet buttons. I could not look at them in a browser in this session, so how they look is not checked beyond the tests below.
  • The gallery throws on its own. Under load, the gallery's building-blocks tiles log "Maximum update depth exceeded" and fail any test that visits it. This happens on main too: 4 of 12 runs of a test that only opens /app/gallery failed there, and I did not fix it. The new top-bar test uses the sessions page, which does not do this.
  • Replay controls. The replay page's buttons (play, speeds, end) are now quiet buttons in the top bar, since they use the .topbar button rule.

Verification

  • bun run check: exit 0, 1544 pass, 0 fail.
  • bun run e2e with E2E_PORT_API=9019 E2E_PORT_DEV=8970 E2E_PORT_BUILT=8971: 153 passed, 1 skipped (the dev-only test, in the built bundle).
  • New e2e tests:
    • The Trace Console's top bar has the LabKit button and no link named "gallery" or "sessions".
    • The sessions page has a top bar 52 px high with a LabKit link, and the theme button at its right edge (dev server only; repeated 8 times under load).

Linked issues

None. This prepares the right-hand chat sidebar.

🤖 Generated with Claude Code

… theme

The gallery, sessions list, replays, agent page and error pages used
their own older header and palette. They now use the frame in
src/ui/chrome, with the Trace Console's top bar, the palette's short
names and quiet controls. The theme is one shared setting, so the top
bar's button and the content agree. The top bar no longer links to the
gallery or the sessions list.

Co-Authored-By: Claude Sonnet 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 8298578 into main Oct 12, 2026
10 checks passed
@danbarua
danbarua deleted the ui-shared-styles branch October 12, 2026 00:10
danbarua added a commit that referenced this pull request Oct 12, 2026
…kspace's folder (#670)

## 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](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