Repository navigation
Workspace page name; @labkit/ui in folders; repeated UI code extracted - #675
Merged
Merged
Conversation
…d docs The page at /app/<API path> was still called the Trace Console in its component, folder, CSS root class, test file, log prefixes and storage keys, while the app on screen is LabKit. The names now follow the workspace: - src/ui/trace/ is src/ui/workspace/; TraceConsole.tsx is WorkspacePage.tsx and exports WorkspacePage. - trace.css is workspace.css; its root class .trace is .workspace-page. - Log messages start "workspace page:". - The saved layout keys labkit.trace.* are labkit.workspace.*. A browser's saved layout resets to the defaults once. - tests/trace-console.test.tsx is tests/workspace-page.test.tsx. The e2e assertion that the top bar does not say "Trace Console" is removed: no code produces that text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
The package had 30 files side by side. They now sit in folders: - base/: Surface, the theme button and the loader. - content/: content of unknown length (card, code, diff, markdown, plots, values) and the contexts it draws with (links, records). - conversation/: the transcript, its blocks, the activity indicator, grouping of tool runs, and items leaving. - tool/: tool call cards and the per-tool views. - composer/: the composer, attachments and the session controls. - requests/: the agent's requests to the person (permission and elicitation). - overlay/: unchanged. Only paths and relative imports change. The package's public surface, index.ts and the two stylesheets, is the same. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
format.ts held three unrelated things: the line diff and patch parsing, the tool call status labels, and the cost and context-window formats. - The diff code is content/diff-rows.ts, beside the diff view that draws it. - The status labels are in tool/tool.tsx, their only user. - formatCost and fillPercent are base/format.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…rmats Two functions wrote a byte size, with different results for the same size: formatBytes in attachments.tsx (rounded to whole kilobytes) and compactBytes in tool.tsx (one decimal place below 10, and "2.0 KB" for 2048 bytes). base/format.ts now has one formatBytes: one decimal place below 10 without a trailing ".0", rounded from 10 up. A visible change: - An attached file of 1536 bytes reads "1.5 KB", not "2 KB". - A linked resource of 2048 bytes reads "2 KB", not "2.0 KB". compactCount and shortDuration move from step-stats.tsx to base/format.ts, and compactCount uses the same rounding helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
- isRecord was written out in value.tsx, tool.tsx and tool-views.tsx, and as isPlain in the app's values.tsx. value.tsx exports it, and the package's index exports it for the app. - links.tsx (fetchable) and elicitation.tsx (webAddress) each parsed an address and accepted only http and https. Both use httpUrl in links.tsx. A link the host resolves is still read against the page's address; an elicitation's URL must still be absolute. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
ContentView draws any ACP content block, in a message as well as in a tool's result, but lived in tool.tsx, so a message imported the tool card's module to draw its text and images. It and its image, audio and resource views are content/content-block.tsx. An image or audio block loads from its bytes, or else from its link once the host resolves it. ImageView, AudioView and the sent-image view in attachments.tsx each wrote this out; useMediaSource in links.tsx is the one copy, and AudioView no longer casts its block to read a uri. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…e page Each icon-only button wrote out the same five things: type="button", its class, an aria-label, the same text as its title, and an icon hidden from assistive technology. IconButton in base/icon-button.tsx takes a label and an icon component, plus: - on: adds the `on` class, for the rail and filter toggles. - unavailable: disables the button and makes the reason its hover text, as the rail's section buttons and the chat button did. - title and size, where the hover text or the icon's size differ. It draws the message toolbar's buttons, the theme button, the composer's attach and mention buttons, the remove button on an attachment, the session settings button, the toast's dismiss button, the modal's close button, the rail's buttons, the chat button and the filter button. The composer's send and stop buttons, which have a style of their own, and its `/` button, which is a character rather than an icon, are unchanged. A visible change: the toast's Dismiss and the modal's Close buttons now show their label as hover text, as the other icon buttons do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…abkit/ui The "(prefers-reduced-motion: reduce)" query was written out six times: in the loader, tool.tsx, leaving.tsx, active-box.tsx, the workspace page's section motion and GraphView. base/media.ts in @labkit/ui has: - REDUCED_MOTION, the query. - prefersReducedMotion(), which reads it once (true outside a browser, as leaving.tsx's copy did). - useMediaQuery(query), moved from the workspace page, which keeps a component current as the query starts or stops matching. GraphView reads reduced motion through useMediaQuery instead of adding and removing its own change listener. The loader still reads one MediaQueryList on each frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
Ten class attributes were built from a list of conditional parts, in four ways: join with filter, join without filter, join and trim, and a template string. The two without a filter wrote doubled or trailing spaces, such as "sidebar collapsed" and "active-box fill placed ". classNames in base/class-names.ts leaves out false, null, undefined and empty parts and joins the rest with one space. It builds the classes of the content card, Surface, the modal, the frame, the sidebar, an explorer section, a resource row, the chat sidebar, a commit row and the active box. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…again
Frame did not let a page set what the LabKit name does, so the workspace
page built its own root (palette class, theme attribute, icon defaults)
and top bar beside Frame. Frame now takes onHome and passes it to the
top bar, and it sets the icon defaults (ICONS) for every page. The
workspace page renders <Frame className="workspace-page" onHome={home}>,
and TopBar is no longer exported.
The pages that use Frame draw no icons outside a conversation, which
sets the same defaults itself, so no icon changes size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
… one object File, Source and Record Explorer each had their own saved open flag, saved share of the height, section ref, share object for the borders, rail button call and Explorer props, written out three times. - SECTIONS names each section's name, icon, whether it starts open and its first share of the height. - useSection(id, unavailable) keeps a section's open flag and share under labkit.workspace.<id> and labkit.workspace.<id>.grow, as before. - The rail's buttons are drawn from the list of sections; Explorer and Splitter take a section. - FilterButton and FilterBox take the useFilter object, and FilterBox draws nothing while its filter is hidden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
Five lists paged a collection with the same reader, written out three ways: `read` in explorers.tsx, `readCollection` in the workspace page, and an inline function twice in git-views.tsx. useCollectionPages(href) in hal-store.ts requests href, keeping any query such as ?limit=200, and reads each page with collectionOf. It pages a directory and the history's timeline and branches in the explorers, Record Explorer's two panes, and a commit's changed files and diff. git-views.tsx's hrefsOf duplicated hal.ts's linksOf and hrefOf; its callers use those. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
Six lists ended with the same choice, written out each time: a "Fetch failed" note, a loading line, an empty note, and a button for the next page. PagesTail in pages-tail.tsx makes that choice once; each list gives its note class, loading line, empty note and button text: - File Explorer's directories and Source Explorer's timeline (the explorers' own Tail component, now removed). - A commit's changed files and its diff. - Record Explorer's two panes. Pane's prop `empty`, which was the text shown while the first page loads, is renamed `firstLoad`. - The events of the Debug tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
The border between two sidebar sections (Splitter) and the border at the chat's left edge (ChatBorder) each wrote out the ARIA window splitter: the separator role and value, pointer capture on press, the drag's starting point, the move, release and cancel handlers, the double-click reset and the key handler. Separator in separator.tsx holds that once. Each border gives its orientation, value range, what a drag starts from, how it moves by a number of pixels, and its keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
`err instanceof Error ? err.message : String(err)` was written out 13 times in packages/app-web/src: in the server, the dev-server plugins and the browser code. messageOf in src/message-of.ts is that expression. The dev scripts and tests keep their own copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
Five components each wrote an effect that set a loading state, started
a request, ignored its result once the effect was cleaned up, and
turned a rejection into a failure message: the transcript mockup, the
session replay, the sessions page, the /sessions palette and the chat's
folder lookup. useLoaded(load, key, {enabled, deferred}) in loaded.ts
does it once and returns loading, failed with a message, or loaded with
the value. `deferred` keeps the one-tick wait that stops React's
development double-invoke from opening two agent connections.
The sessions page and the /sessions palette also shared their listing
and sorting code and their notes; agent/sessions.ts holds
useAgentSessions and sessionsNote. Changes a person can see:
- The sessions page says "Asking the agent for its sessions…" while it
waits, as the palette did, instead of showing nothing.
- The palette waits one tick before asking, as the sessions page did.
- When a search in the palette matches no session, it says "Nothing
matches" instead of "The agent has no sessions in <folder>".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
The chip for a record (class `mention`, in its type's colour, its type as hover text, opening the record when pressed) was written out in the prose of a resource, in each end of an event, and for each commit the acts ran against, with the dashed chip for an unknown handle written twice more. Mention in values.tsx draws its three forms: a button that opens the record, the open record marked and not pressable, and a handle with no known record. The prose switch names its "unknown" segments as a case of their own and checks that every kind of segment is handled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…t use biome reported the import as unused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
Since useLoaded, SessionReplay built `{ events }` while rendering, a new
object each time. The replay's playback effect depends on the
recording, so any new render of the page would restart its timer for
the next event. The load now resolves to the Recording itself, which
stays the same object until the session changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
/app/<API path>was still called the Trace Console in its component, folder, CSS root class, test file, log prefixes and saved-layout keys, while the app on screen is LabKit.@labkit/uikept its 30 files side by side, andformat.tsmixed the line diff, tool status labels and the cost format.@labkit/uiand in the workspace page: icon-only buttons, byte formats, type guards, reduced-motion checks, class-list joining, collection paging, paged-list tails, the two resizable borders, load effects, record chips and the error-message expression.Change
One commit per change:
src/ui/trace/issrc/ui/workspace/;TraceConsoleisWorkspacePage;.traceis.workspace-page; logs startworkspace page:; saved keys arelabkit.workspace.*.@labkit/uifiles sit inbase/,content/,conversation/,tool/,composer/,requests/andoverlay/. Only paths and relative imports change;index.tsand the two stylesheets are the public surface, unchanged.format.tssplitcontent/diff-rows.ts; status labels are intool/tool.tsx; number formats arebase/format.ts.formatBytesreplacesformatBytesandcompactBytes;compactCountandshortDurationmove beside it.isRecord, onehttpUrlisRecordwas defined four times (includingisPlainin the app); the HTTP(S) address check twice.ContentViewtocontent/useMediaSourceloads image and audio blocks for three views.IconButton@labkit/uiand the workspace page's rail, chat and filter buttons.unavailabledisables a button and makes the reason its hover text.REDUCED_MOTION,prefersReducedMotion()anduseMediaQuery()replace six copies of the reduced-motion query and the page's own hook.classNamesFramereuseFrametakesonHomeand sets the icon defaults; the workspace page uses it instead of building its own root and top bar.SECTIONStable anduseSection;FilterButtonandFilterBoxtake the filter object.useCollectionPageshrefsOfingit-views.tsxgives way tohal.ts'shrefOfandlinksOf.PagesTailSeparatormessageOferr instanceof Error ? err.message : String(err)for 13 sites inpackages/app-web/src.useLoadeduseAgentSessionsandsessionsNoteshared by the sessions page and/sessions.MentionuseRouterStatein the workspace page.Recordingobject, so a new render of the page does not restart playback's timer.What a person can see change
/sessionswaits one tick before it asks, and a search that matches no session says "Nothing matches" (it said "The agent has no sessions in ").Not done
From a survey of the app's
.tsxfiles, these were left, each for the reason given:.panelcards besideContentCard(Properties, related resources, events, commits): moving them toContentCardchanges how those cards look and fold.filterItems: it would change how a term matches (every word rather than one substring) and the order of rows in a paged list.ToggleGroupfor the Rendered/Source and structural/temporal toggles, aResourceHeadfor the badge, id and type triple, and aFetchErrorblock: two to four sites each.navigate({ search })calls, all in one function.@labkit/ui(thought body and transcript): they differ in slack and in the transcript's resize and gutter handling.Conversationbuilding its own root besideSurface:Surfacedraws adiv,Conversationasection.withSetting).ui.cssstays one stylesheet: splitting it by folder would reorder rules whose order decides which wins.Controls
bun run web:check-types, andtsc -p packages/uifor the package and its tests.bun testforpackages/uiandpackages/app-webafter each commit.bun run checkand the e2e suite on the final tree.Verification
bun run checkon the final tree: all 28 passed;bun test1105 pass, 0 fail.E2E_PORT_API=9019 E2E_PORT_DEV=8970 E2E_PORT_BUILT=8971 bun run e2eon the final tree: 175 passed, 0 failed, 11 skipped: in the built project, the gallery, the sessions page, the agent page and the chat exist only in development. Each of those 11 tests passed in the dev project.git grep -i "trace console"finds nothing.Linked issues
None.
🤖 Generated with Claude Code
https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs