Unify the interface on one design system, and finish the Wake and cc-switch ports - #40
Open
loadchange wants to merge 44 commits into
Open
Unify the interface on one design system, and finish the Wake and cc-switch ports#40loadchange wants to merge 44 commits into
loadchange wants to merge 44 commits into
Conversation
The window carried three visual languages at once: Wake's flat warm-grey columns, ccbuddy's white shadowed card wall, and four unrelated accent colours. Fold them into one set of tokens and one navigation model. - Design/DesignSystem.swift is now the only source of colour, type scale, spacing, radius and column geometry; Components.swift holds the shared controls. The Color.cc* shim is gone, persistent surfaces cast no shadow, and the stray orange accent resolves to Claude clay. - One rail instead of three stacked navigations. The duplicate icon strip and the source-directory group are gone; history roots moved to the new Settings > Session locations pane, and Settings is a scene reached from the footer gear and Cmd-, rather than a peer destination. - Agents are identified by their real brand marks (from Wake) instead of borrowed SF Symbols. Finish the halves of both ports that were never wired up: - Continue a session in Terminal, iTerm, Ghostty or Warp using each CLI's own resume dialect. - Star a session; the index has carried the column since day one with nothing reading or writing it. - Port cc-switch's provider catalog: ten hand-written presets become seventy, behind a search field. Their base URLs are normalised to this app's contract, which is one path segment longer than cc-switch's. Fixes found along the way: - The session stream stopped at 600 entries, hiding 343 of 943 sessions from both scrolling and search while the badge reported 600. - A legacy catalog could never reclaim free pages: auto_vacuum is a no-op on an existing database, so incremental_vacuum was inert. A real catalog held 2.4 GB of freelist inside a 3.8 GB file. - Session titles came from carried tool output rather than typed prose. - One unreadable transcript out of a thousand painted the column header red and raised an error toast.
A contrast test over every ink/material pair in both appearances found five light-mode near-misses, all between 4.0:1 and 4.47:1 where body-sized text needs 4.5:1 — secondary text on the two darkest fills, clay text on the selection wash, and the success and warning inks on their own tints. The materials are intentionally close in tone so columns can separate without borders, which is exactly what makes this easy to get wrong by eye. Darkening four inks by a few percent clears the threshold and keeps the hue.
Wake's Locations pane can silence a source without forgetting it; ours could only add or remove, so the only way to stop scanning a tree was to delete its configuration and re-add it later. A disabled location keeps its entry and its catalog rows — reconciliation is per scope, so nothing is discarded — and drops out of scanning, watching, listing and search. Switching it back on picks up incrementally rather than rebuilding. The row recedes as a whole instead of gaining a "Disabled" badge.
The catalog schema has carried a `pinned` column since it was created, with nothing reading or writing it — the same gap the star had. A pinned session holds the top of the stream regardless of when it last ran, marked in the row and toggled from the reading header. Star and pin are separate facts, so each patch names only its own field.
Cmd-K opened nothing; the rail's inline field searched the whole library from a control that looked like it filtered the column beside it. The rail row now opens Wake's panel — 680pt wide, large borderless input, brand-marked results with a snippet, and the keyboard path spelled out in the footer. Arrow keys, return and escape run through a local event monitor so the field keeps first responder while the list is driven from the keyboard; the deployment target predates onKeyPress and a focused field eats arrow keys before a keyboardShortcut would see them. The panel reuses the store's existing full-text search rather than opening a second search path, so the stream stays filtered to the same query afterwards and the rail row shows what that query is.
ImageRenderer draws through Core Graphics rather than the window server, so the palette, the control kit and the rail rows can be inspected in both appearances over SSH, on CI, or while the screen is locked — which is exactly when a visual regression is otherwise invisible. Scope is deliberately the design system and not whole panels: AppKit-backed controls come out as placeholders and ScrollView content does not draw, so a blank area in a panel sheet could not be told apart from a real regression. Opt in by creating /tmp/ccg-proof before running; an ordinary run stays quiet.
Two of the strings added in the previous commit already had translations in the shared dictionaries. Overrides win over the dictionary, so adding them silently rewrote copy that other locales had already settled on — a Japanese assertion caught one of them. Only strings the dictionary does not already cover belong in the override file.
The page was titled "About and updates" and contained only an update button. It now opens with Wake's identity block — product mark, name, running version, one line of what the app does, the repository, a short rule, then the license and where the data lives — before the update controls.
The stream row is the most repeated element in the application, so it gets a proof sheet of its own: pinned, starred, subagent, a search hit and an untitled session, in both appearances. The sheet immediately showed the subagent arrow drawn twice — once at the head of the title and again in the metadata line, where it added nothing. Only the first survives, and it now carries the label it always should have.
Role headers and tool rows used emoji as structural icons — 💭 ✏️ 🔎 🔧 and the rest — which is the one thing the design system rules out for interface icons: they vary in weight, colour and baseline, so every row in a transcript looked like a different kind of thing. Roles are now marked the way the rest of the app marks them: the person icon for you, the agent's own brand mark for the assistant. Names are no longer upper-cased, which did nothing to Chinese and shouted "CLAUDE CODE". Tool rows carry monoline symbols chosen to say what the tool did. A proof sheet covers the exchange so the reading card can be checked off-screen.
Qoder was the one agent still falling back to a lettermark. Its icon ships as a full app icon with an opaque background rather than a trimmed glyph, so brand marks now carry a flag for that shape and clip it to the squircle macOS gives app icons — drawn flat it would sit on the warm materials as a hard-edged block of its own background colour.
The usage scan collected every parsed record from every transcript before aggregating, so peak memory tracked the size of the library rather than the size of the result. On a 14 GB library that was 841 MB resident for a few hundred day buckets; it now stays around 180-290 MB. Deduplication sees the same records in the same order — it is the same rule, applied one record at a time instead of over a materialised array — so the aggregate is unchanged.
The rail listed "Sessions" as a destination and then, below it, the library that destination shows — a second door into the room you were already standing in. The row is gone; the library is always listed, and picking any scope in it is what takes you there. Two things had to follow. The catalog now activates with the shell rather than with the sessions page, or the resident rail would show empty agent and project groups everywhere else. Import and refresh moved out of that same conditional for the same reason. A scope still only reads as current while sessions are actually on screen.
The usage scan read every transcript in every configured location each time the app started, and the only cache was in memory — so a 14 GB library was read again from scratch on every relaunch. Measured here: 241 s of ~95% CPU before the panel showed a number. Parsed records are now kept per transcript, keyed by modification time and size, in ~/.ccbud/usage-records-v1.json. Records rather than day buckets, because deduplication runs across files — an assistant message appears in both a parent transcript and its subagent's — so the fold still has to see every record. Unchanged transcripts are answered from the cache; changed ones are parsed again; entries for deleted transcripts are dropped on commit. Same library, second launch: 241 s -> 6 s, from a 15 MB cache. The cache is disposable: losing or corrupting it costs one rescan and nothing else.
An earlier scripted edit dropped the Monitor page's padding entirely, leaving its content flush against the header's hairline and against the window edge. The inset is now one modifier the three gateway pages share, so it cannot go missing from one of them again. The metric strip was four fixed columns that collided with themselves on a narrow window — the endpoint and the timestamp had nowhere to go. It folds to two by two when there is no room for four. The library group of producing CLIs is called Tools, not Agents.
The sheets opted themselves out with XCTSkip when nobody had asked for the files, which broke the native gate: it requires zero skipped tests, and CI reported 500 passed, 0 failed, 5 skipped — exit 1. They now always render, which is the part worth having anyway: it proves each surface still lays out and draws, and the render is asserted to produce a real image. Only writing the PNGs is conditional on somebody wanting to look at them.
Sessions is no longer a destination row, so the localized-navigation test was asking for a button that cannot be there, and the navigation walk was clicking it. Both now cover the three gateway rows plus the footer gear, and the walk enters the library the way people do — through its "all sessions" row. Two robustness fixes alongside: the session column is an accessibility element in its own right, because a bare identifier on a container propagates into its descendants and the column matched several elements at once; and the providers assertions wait, since the shell appearing does not mean the destination inside it has laid out.
One shared `accessibilityContainerIdentifier` replaces the three per-area copies. It is the marker form throughout: SwiftUI pushes a plain identifier placed on a container down onto its descendants, where it replaced their own hooks, so Providers answered to `view.providers` for its hero and its buttons alike. The shell marker moves onto the background layer, which is the layer that actually spans the window; hung off the outer chain it measured the safe area and reported the shell starting a title bar below the window top. The window pixel samples now read the palette they are checking and are no longer flipped — counting rows from the bottom, the detail column reported its reading card instead of its title-bar band, which is what hid the flip until now. The Claude/ChatGPT analysis actions live in the overflow menu since the header gained its terminal-resume primary; the test opens the menu instead of looking for toolbar buttons that no longer exist.
The config and the Bifrost route builder already modelled an ordered failover queue, but nothing in the interface could turn it on or say what was in it, so the feature existed only for anyone hand-editing config.json. Gateway settings gains the queue: a switch, the ordered entries with their priority, and add/reorder/remove. Order is edited here rather than inherited from the provider list, because for this queue the order is the entire meaning. Providers carry a P1/P2 badge while failover is on, so the list explains why a request went where it did. Enabling with an empty queue seeds it with the active provider, and removing the last entry reseeds it — an enabled queue is the gateway's whole route set, so an empty one would mean no route at all. The overflow menu in the conversation header takes a coordinate click: a menu in a window that is not key reports itself unhittable on a headless runner.
A single coordinate click on a menu button in a window the runner has not brought forward opens nothing, which is how the assertion after it failed on CI while passing locally.
Three leftovers from the pre-indexed history path and the pre-design-system views: HistoryRepository's own Codex canonicalisation, project ordering and ancestor-preserving limit — 234 lines duplicating what HistoryCatalogProjection now does for both the repository and the catalog — plus an HTTP response scanner superseded by the rewriter and two view helpers with no call sites. Also gives the failover queue's add control the same quiet-button shape as every other action on the settings page.
A SwiftUI Menu publishes its items only once AppKit opens it, which the CI runner never does regardless of how the click is delivered; the actions behind it are covered by ConversationReplayTests.
The gate required exactly ten UI tests, so the eleventh failed the build by existing. Assert the property instead: every test that ran passed and none were skipped, with a floor that still catches a suite that stopped running.
An enabled queue routes every request in order, so its first entry is the provider in use — but the provider list took its badge from activeProviderId, which nothing tied to the queue. Enabling failover, reordering it, or removing its head could each leave one row marked 使用中 while a different row took the traffic. Normalisation now makes the head authoritative while the queue is routing, and choosing a provider from the list moves it to the front instead of setting a field the gateway ignores.
With no sessions to list, the workbench showed two empty states side by side: one saying there is nothing to read, the other asking you to pick something from it. The list keeps the explanation and the canvas stays plain.
The snippet generated a fresh bundle identifier per run, which is a fresh TCC subject each time and so a consent prompt each time, and it omitted the signing flags without which the runner cannot attach at all.
It failed once on a loaded runner with two terminal events and a count from some other scan, and the assertion messages said nothing about which scan produced what.
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.
Stacked on
codex/wake-conversation-refactor, so this PR also carries thatbranch's commits; the 19 commits from
c20ea40onwards are the new work.One visual language instead of three
The window carried Wake's flat warm-grey columns, ccbuddy's white shadowed card
wall and four unrelated accent colours at the same time.
Design/DesignSystem.swiftis now the only source of colour, type scale,spacing, radius and column geometry. The
Color.cc*compatibility layer isgone, persistent surfaces cast no shadow, and the stray orange resolves to
Claude clay.
from cc-switch expressed through the semantic status trio rather than its own
palette. Written down in
docs/design-system.md.Symbols, and the transcript no longer uses emoji as structural icons.
light-mode near-misses between 4.0:1 and 4.47:1; the inks were darkened.
One navigation model
The rail stacked an application list, a compact icon strip, a source-directory
group and a library tree, so the same session was reachable four ways and
counted differently each time. What remains is the library itself, always
listed, with the gateway destinations above it. Settings became a scene reached
from the footer gear and
Cmd-,, and history roots moved into it as aLocations pane where they can be added, revealed, removed and switched off.
Capabilities that were only half ported
own resume dialect.
created with nothing reading or writing them.
cc-switch behind a search field. Their base URLs are normalised to this app's
contract, which is one path segment longer than cc-switch's; a test pins it.
Fixes found along the way
scrolling and search while the badge reported 600.
auto_vacuumis a no-op onan existing database, so
incremental_vacuumwas inert. A real catalog held2.4 GB of freelist inside a 3.8 GB file.
in-memory cache. Measured on a 14 GB library: 241 s -> 6 s, peak memory
841 MB -> ~460 MB, from a 15 MB per-file record cache.
like
Script completed Wall time 1.8 seconds Output: {"plan":{}….raised an error toast.
Verification
BifrostSupervisorIntegrationTests,BifrostResponsesProtocolIntegrationTestsand
ClaudeCLIBifrostE2ETestshang indefinitely on this machine. Revertingthe branch's pre-existing failover changes does not help and
bifrost-httpstarts standalone in 4 s, so the cause is environmental rather than in this
work — but it needs confirming in CI.
DesignProofSheetTestsrenders the palette, controls, rail rows, session rowsand a transcript off-screen in both appearances, which is how the duplicated
subagent arrow and the emoji icons were caught.
Known gaps
no UI and no tests; it stays off by default and is deliberately not exposed.
pane does not.