Skip to content

Codex simplicity redesign of the desktop app and the website (R-01 to R-34) - #394

Merged
Broccolito merged 949 commits into
mainfrom
claude/crew-landing-redesign-91b46e
Oct 12, 2026
Merged

Broccolito merged 949 commits into
mainfrom
claude/crew-landing-redesign-91b46e

Conversation

@Broccolito

@Broccolito Broccolito commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Branch claude/crew-landing-redesign-91b46e into main. At a7d069304 it is 879 commits on base
30e1f0622, 1,358 files changed (160,308 insertions, 32,550 deletions). Every commit names the
requirement it serves ([R-nn]).

Do not merge without the owner: the landing site deploys from main.

Summary

The owner asked for a redesign of the whole desktop app, and a careful revision of the website,
with one rule: simplicity. Codex supplies the structure and the restraint; Biorouter keeps its
coral accent, warm ink, UCSF navy wordmark and three theme families in light and dark. Crew was the
reference design and was not redesigned. The records are in
docs/design/codex-simplicity-redesign/: requirements, implementation spec, progress tracker and
handoff.

By requirement:

  • R-01 Simplicity with Biorouter's identity. Codex-like structure and restraint; the accent
    stays on the active row rail, the focus edge, Send, the heatmap, status glyphs and lock badges.
  • R-02 Less text. Instructions and explanations nobody needs to read up front are gone or
    shortened.
  • R-03 Help on hover and focus. Paragraphs became InfoTips, reachable by keyboard and named
    for screen readers.
  • R-04 One family, few sizes. The website's Arial stack everywhere (Inter only in the
    wordmark), five sizes (12, 13, 14, 17, 24) in two visible weights, a 14px body, pinned by
    styles/typeScale.test.ts.
  • R-05 Compact sidebar. 28px rows on a 30px pitch with 13px labels at 216, 288 and 360px,
    measured in Chromium.
  • R-06 Group by and sort by. A View options menu groups chats by Date, Folder or None and sorts
    them by Last activity, Created or Name, remembered per user.
  • R-07 Right click Rename. Rename in place from the row menu, F2 or Cmd+Opt+R, with the full row
    menu (open in a new tab or window, diverge, export, copy ID, delete).
  • R-08 Docked summary rail. Once a chat has had a turn, a 240 to 280px card docks right of the
    760px conversation where it fits, and becomes a 280px popover where it does not.
  • R-09 Motion. Tabs grow in, the sidebar slides as one piece, previews and dialogs fade and
    move with shared motion tokens, nothing animates during a window resize, and reduced motion is
    respected.
  • R-10 Preview panel. Every content type sits under one 32px preview bar on the same paper,
    with no loading flash for a tab already seen.
  • R-11 Usage dashboard. Home's usage card follows the Claude and Codex usage pages and keeps the
    coral heatmap.
  • R-12 Chat interface. Messages, tool rows, cards and the floating message actions were
    redesigned, not only Home.
  • R-13 Composer. One input card with Send, a context row above and a compact footer below;
    monospace only for paths and code. The card is white with a hairline and the coral focus edge
    (R-32).
  • R-14 Crew kept. Crew is unchanged apart from shared tokens; every vision round checked it for
    regressions.
  • R-15 Component views. Workflows, Scheduler, Built apps, Extensions and Skills follow Crew's
    language: one page band, flat rows, filled controls.
  • R-16 Knowledge. Same structure and flows (selector, ingest, graph, change log), a much simpler
    look.
  • R-17 Settings. Rebuilt as short rows with InfoTips and one status line only while one
    applies.
  • R-18 One control style. One switch, segmented control, menu, select, field and dialog
    (ModalShell, pinned at the top with a header rule, as in Crew) across the app.
  • R-19 Icon audit. One registry; the same concept draws the same icon everywhere.
  • R-20 Chat kind icons. Chat, Crew, branch, sub-agent, app, scheduled, workflow and terminal
    each have a glyph; a private chat adds a lock badge, so privacy is a shape, not only a colour.
  • R-21 Tool icons. One module (utils/toolGlyph.ts) maps each built-in tool to a
    representative glyph; every external extension draws the generic extension glyph, and the
    wrench is the last fallback.
  • R-22 Website. landing/ follows the same fills, type and motion, keeps all of its content,
    and keeps Join Slack in the header.
  • R-23 Vision loops. Real screenshots of the running app, read by a vision model; see
    Verification.
  • R-24 Records. The requirements and the tracker live in the repository and are current.
  • R-25 Tools popover. The Extensions, Skills and Knowledge switcher sits at the bottom, at a
    fixed place 15px above the Tools chip whatever the list length or window height.
  • R-26 Effort slider. A three stop slider (Quick, Normal, Deep) on a new in-house primitive,
    components/ui/slider.tsx, with role="slider" and arrow, Home and End keys.
  • R-27 Fills over lines. Cards, buttons, chips, tabs and fields rest on --background-fill
    instead of a 1px outline; lines stay only where they are structural and on floating surfaces.
  • R-28 Fast sidebar reordering. A complete reordered list of 2,000 chats in about 0.2 s (was
    3.1 s); the list now draws only the rows near the viewport.
  • R-29 The wordmark is the logo. The unreferenced logo images and unused logo components are
    deleted.
  • R-30 Opening is not activity. Opening a chat, and everything opening does on its own, no
    longer moves it under Last activity; a rename, a turn and the other deliberate changes in ruling
    16 still do, and the renderer shows a person's own rename at once.
  • R-31 Retired names. The retired upstream project and its publisher are named nowhere in the
    tracked tree except NOTICE. A repo guard in just check-everything and in rust.yml's guards
    job keeps it so. On Windows the app folder moves to %APPDATA%\UCSF\biorouter.
  • R-32 Composer card. The composer (chat, Home and the in-place message edit card) is the page
    ground with its earlier hairline and the same coral focus edge, not a grey fill (owner message 17).
  • R-33 Knowledge drop zone. The Sources rail's drop zone fills the rail again, side-panel scale,
    and yields room to staged sources and the paste box down to a 240px floor (owner message 17).
  • R-34 First run. The screen a new install shows before any provider is set up was redesigned
    to the same standard: one band led by the wordmark, the catalog tabs on the page ground, short rows
    with InfoTips, one button rung, and a boot splash that draws the wordmark (owner message 18).

How it was verified

  • Final gate, round 2 at a7d069304, green:

    • npm run lint:check: tsc, eslint, themes, 1,148 contrast assertions, token mirrors.
    • npm run format:check.
    • npm run test:run (excluding the known artifactCdnAssets.browser teardown hang): 1,006 test
      files, 13,887 tests passed, 10 skipped. The first full run lost one Crew test to load
      (AgentTaskPane.test.tsx); it passed alone and the second full run was clean.
    • The site's consistency check, the Crew manual check, and 180 site tests (node --test landing/scripts/*.test.mjs).
    • cargo fmt --check, ./scripts/clippy-lint.sh, cargo test -p biorouter --lib -- session
      (509), cargo test -p biorouter --test privacy_guard_wiring (3).
    • cargo test -p biorouter-server --no-fail-fast with BIOROUTER_DISABLE_KEYRING=true, as
      rust.yml sets it: every target passed. Without the variable,
      tests/schedule_write_reach.rs fails its own precondition check; the file is unchanged from
      main.
    • A clean git status.
  • Vision loops. Four sandboxed dev app instances (shell and Home, chat, component views,
    Settings with dialogs and a Crew check) and the website, screenshotted in light and dark at
    several widths and read by a vision model. The app windows were resized by the operating system,
    never by viewport emulation.

    Round Findings High or medium
    1 107 45
    2 40 12
    3 28 6
    4 19 6 (none high)
    5 27 (app only) 9
    6 26 (app only) 4
    7 17 (app only) 8
    8 18 (app only) 4

    The website was clean in rounds 3 and 4 and was not reviewed after that. Round 8 found no high
    or medium issue in the shell, sidebar, Home and History group or in the Settings group. Every
    high and medium finding from every round has a fix committed; round 8's four (a model chip
    padlock flash, the first send's rail flash, a toast over the find bar, and a keyboard pick in
    the shared Select dropping focus) were fixed after the round and are pinned by tests, not yet
    re-checked in the running app.

  • First run (R-34), on a sandbox with no provider: findings fell 29 (18 high or medium), 10 (3),
    7 (1), 5 (2), 2 (1) over five rounds. The boot splash was watched live with a screen recording at
    normal speed and at 3x and 4x CPU throttle. Every finding has a fix committed; the last two
    (the day-one privacy notice waiting for the page, one coral button per catalog tab) are not yet
    re-checked live.

  • Still open, all low: about a dozen polish items from round 8 (for example a duplicated tool
    output, a lingering tooltip, an 8px gap in one menu, a focus return after Escape). A last polish
    and verification pass is running; this description will be updated when it lands.

  • Owner checks in every round from 2 on: no retired name anywhere in the DOM, the CLI help or
    the guard; opening a chat moved nothing in the sidebar, History or sessions.db; a rename and a
    sent message moved the chat at once.

Integration with #391, #392 and #393

The three non-UI PRs merged into main first (#392 built apps as 6eb1bcec0, #391 bug reporting as
d2ab66f4f, #393 local inference as cf554fb5d), each green on every check, and main stayed green
after each. #390 was closed as superseded by this PR. This branch then took main in two merge commits,
bf904e2e4 (#391 and #392, 17 conflicts) and ca7c64691 (#393, 14 conflicts), resolved to one rule:
keep this PR's primitives, layout, copy registries and type rules, and keep every behaviour the other
PR added. The resolution recipes, the semantic breaks the merge tool did not flag and the acceptance
tests for each are in the merge plan the integration followed (run folder, merge-plan-2026-10-12.md).

Needs human review

  1. The session listing SQL changed shape, and MAX_SIDEBAR_SESSION_LIMIT went from 50 to
    2,000
    (crates/biorouter-server/src/routes/session.rs:53).
    • aceb5d6e5: the sidebar's keyset listing walks idx_sessions_updated, and one page may carry
      the whole list.
    • 0403b8d63: the full list behind History and biorouter session list replaces its JOIN messages ... GROUP BY with an EXISTS probe and a correlated COUNT(*).
    • 18de68b57: a tied updated_at breaks by id ASC, as the sidebar does.
    • 6a4566b46: GET /sessions?limit= (History's first paint) reads one keyset page through the
      same keyset_listing_scope as the sidebar; without limit the answer is byte identical. The
      privacy census counts are unchanged; only their descriptions name the shared scope.
    • The privacy filters are still SQL predicates with the same meaning (privacy_tier = 'public'
      for an unproven caller, the Crew exclusion, the keyset cursor). A caller holding the secret
      can now read up to 2,000 rows it could already read in one request instead of 40. It is the
      listing every private chat passes through.
    • Ruling 28 (V2-R9-3), one named backend exception in the same route family
      (crates/biorouter-server/src/routes/agent.rs, resume_agent): when
      load_model_and_extensions is true the answer carries the metadata session and skips the
      second full read of the conversation, so opening a very large chat no longer sends its
      transcript twice. The false response is byte for byte unchanged. Both gates
      (session_reach, refuse_subagent_unless_user) are untouched, and the restore paths read
      metadata only. A route test pins both bodies behind the same refusals, and a renderer test
      pins that nothing reads session.conversation from the second response. A performance
      change; no privacy logic.
  2. Privacy UI presentation moved; no logic changed.
    • The Knowledge tier control is a badge in the page band; its confirmation logic is unchanged.
    • The model chip's privacy lines and the DR-17 disclosure moved into the model menu.
    • Two class only restyles under ruling 15 (6bb7daced): privacy/HostManagedModelPanel.tsx
      and privacy/CrossAffiliationAcceptCard.tsx swapped an outlined box for a fill and moved to
      the shared type roles. No copy, props, logic or test id changed.
    • One markup swap under ruling 15 (8f132acbb, first run FR-R1-14):
      privacy/FirstRunPrivacyNotice.tsx, the day-one notice on an upgraded install, renders through
      ModalShell (size md, top anchor, header rule, scrolling body, footer row) instead of a raw
      DialogContent, and its prose takes text-body. Copy, props, counting, the localStorage
      acknowledgement and every test id are unchanged; the one difference is that the
      first-run-privacy-notice id now sits on the body inside the shell, because ModalShell
      carries no test id. The count sentence is still the dialog's description.
    • Two class only changes to the same file under ruling 15: ef7aa092e (first run FR-S-14) moves
      the clinical-extension disclosure from the muted ink to the default ink and sets the notice's
      counts in tabular figures; 21b6dc3c4 (first run FR-R2-8) gives the four <strong> elements
      (the three counts and the extension names) font-semibold, so they compute to 600 rather than
      the browser's bolder 700. Nothing moves on screen (Arial draws 600 as bold); no copy, props,
      counting, test id or logic changed.
    • Three more class only font-semibold changes under ruling 15 (757d4e02e, vision finding V4-R7-6),
      the same defect as 21b6dc3c4: privacy/DisclosureProse.tsx (the two "what is not protected"
      clauses, shown by the non-private model disclosure and Settings > App > Privacy),
      privacy/PrivacyTiersOffNote.tsx (the headline) and privacy/CrossAffiliationAcceptCard.tsx
      (the word "strict"). Report a problem (ui/Diagnostics.tsx, not frozen) got the same for its two
      warning leads. Before and after renders are byte-identical PNGs; no copy, props, logic or test id
      changed.
    • One focus change under ruling 15 (51d145c72, vision round 6 V3-R6-2): where focus lands when
      the shared typed confirmation ui/DangerousConfirmDialog.tsx opens. It reaches every privacy
      surface built on it: Knowledge's "Make public…" (KbTierControl), DeclassifySessionDialog
      and Crew's typed confirmations (make a connection public, allow a workspace public, set an
      institution, remove a person). A key open (Enter or Space on the opener, a key pick from a
      menu) still puts focus on Cancel, ringed. Any other open (a click, a pointer pick from a menu,
      a dialog the app raised itself) rests focus on the dialog box, which draws no ring; the next Tab
      reaches the phrase field, then Cancel. Before, Cancel always took focus, and after a pointer
      pick Chromium drew the keyboard ring on it. Unchanged: copy, props, the phrase gate, Enter never
      confirms, the destructive button is never the first stop, and the frozen KbTierControl logic.
      docs/security/privacy-tiers.md §12.3 now says so (it said Cancel always holds initial focus).
    • Two dialog anatomy swaps under ruling 15 (vision round 7, V3-R7-1), both presentation only, in
      the one flow that marks a private chat public. 849d49e3a: the shared typed confirmation
      ui/DangerousConfirmDialog.tsx is a ModalShell size sm (top anchor, header rule, 16px
      insets; the box is 400 wide, was 480), so Knowledge's "Make public…", DeclassifySessionDialog's
      typed confirmation and Crew's typed confirmations open where every other dialog opens. Copy,
      props, the phrase gate, the swallowed Enter, busy = no x/Escape/backdrop and the key-open
      focus rule are unchanged. 21026e401: the undo window of DeclassifySessionDialog ("Marking
      this chat public…") is a ModalShell too, instead of a centred raw DialogContent, so the
      flow no longer jumps between two anatomies. Copy, props, the 5-second held request and the
      callbacks are unchanged, and the shell keeps its default purpose, so Escape, the x and a
      backdrop press are the Undo exactly as before (five new tests pin each way out, and a shell
      given purpose="required" fails four of them).
    • One markup swap under ruling 23: privacy/NonPrivateModelDisclosure.tsx, the DR-16 disclosure
      a person must answer the first time a chat runs on a model that is not private, is a
      ModalShell (size lg, purpose="required", testId="non-private-model-disclosure",
      scrolling body, top anchor, header rule) instead of a raw DialogContent. It is the one
      dialog that cannot be dismissed (DR-17 requirement 3), so that property now comes from
      purpose="required" rather than from dismissible={false}. Please check three things: Escape,
      a backdrop press and the absence of the x each do nothing, so the button is the only way out
      (tests fail the day purpose stops being required); focus lands on the prose, not on "I
      understand"; and the dialog keeps its test id on the role="dialog" node. Copy, props, the
      acknowledgement error banner and the busy disabling are unchanged (96820ea11). One
      placement: the banner ("not saved", shown when the daemon refuses the acknowledgement) sits in
      the footer on its own row above the button, outside the scrolling body, as it sat outside the
      old scroller; at the end of the body it scrolled out of view at the smallest window and the
      largest text size while the button changed to "Continue" (measured in Chromium). This was the
      last reason to keep DialogContent's p-6, gap-4 and sm:max-w-lg defaults.
    • One named exception to ruling 15's "no copy, no behaviour", ruling 25:
      privacy/HostManagedModelPanel.tsx, the first-run panel a browser user sees on biorouter serve, draws biorouter configure with the shared CopyField instead of a bare <pre>. The
      command text, the panel's test id, title, reason and footnote are unchanged. New: a Copy
      button with its own accessible name ("Copy biorouter configure"), a clipboard write, and
      Copied and Copy failed feedback in a live region. The panel shows only on a browser surface,
      where the clipboard works.
    • One named exception to the same rule, ruling 27: settings/privacy/PrivacyPanel.tsx seeds the
      Privacy tiers switch from the shared config cache on its first render (the cache the
      composer's "enforcement off" note and every badge already paint from) instead of holding the
      switch unpainted until read() answers. This changes the panel's initial state, on the control
      the whole feature hangs from. An empty cache still holds the switch unpainted, so nothing is
      claimed before the daemon has been heard; the mount read still runs and replaces the seed; and
      the write path is untouched (P-05: never setEnabled(on), read back after every write). The
      one exposure: if the daemon's value changed after the cache loaded, the panel shows the cached
      value for one /config read (tens of milliseconds) before the read replaces it. A cached
      "off" was refused for this switch once (V4-R2-1, 77c00cd43); the difference now is the
      source, the app's shared snapshot rather than a new store of the panel's own.
  3. Text fields lose their resting border under the fill rule. They stay identifiable by fill,
    label or placeholder and the focus edge, and forced colours still draw borders
    (styles/fillTokens.browser.test.ts). The trade-off is recorded in design.md Part 9.
  4. R-30 runs the cold restore bind (Gate A) inside an opening scope.
    bind_provider_if_allowed now writes updated_at through a CASE: it stamps only outside an
    opening scope, and only when the provider or model_name differs from what the row records. Its
    WHERE id = ? AND (privacy_tier = 'public' OR ? = 1) is unchanged. The scope lives in
    crates/biorouter/src/session/activity.rs; a census test counts every door that enters it.
  5. The Windows app folder move is a data migration that has not run on Windows. Config and data
    move from the legacy author folder to %APPDATA%\UCSF\biorouter (cache
    %LOCALAPPDATA%\UCSF\biorouter), and the paths stored inside are rewritten. Only the daemon
    moves it, once, and never while another Biorouter process uses the old folder
    (crates/biorouter-mcp/src/app_folder/, ui/desktop/src/utils/appFolder.ts; commits
    eed608d6a, 2afec801a, 4830531f7, bce54c000). No CI job exercises the real Windows
    layout. Windows tester checklist:
    • Upgrade from a pre-move release. On a machine where an earlier release has chats
      (one of them private), BAAM extensions, a knowledge base, a schedule, a Crew connection with
      a vault and some tool permission grants, install this build and launch it. Everything is
      under %APPDATA%\UCSF\biorouter, and the legacy folder is gone (its parent only if empty).
      The daemon log names the move.
    • Extension resume. Reopen a chat that used an installed extension and run a turn; the
      extension starts from its new path.
    • Knowledge bases. Every base is listed and opens.
    • Crew vault. The vault unlocks after the move.
    • Held folder. On a machine with only the legacy folder, start biorouter session,
      then launch the app. The daemon log says the folder is in use, both use the legacy folder, and
      no UCSF\biorouter appears. Quit the session and relaunch: the folder moves.
    • Stale CLI copy. An older biorouter.exe in %LOCALAPPDATA%\Biorouter\bin resolves
      the legacy folder and can create it again. Check that the CLI dependency card offers the
      update, and that the updated CLI uses the new folder.
    • Both folders exist. The app shows a notice naming the older folder; "Show older folder"
      opens it, and "Do not show this again for this folder" silences it. The recovery steps are in
      docs/troubleshooting/common-problems-and-fixes.md, "Two folders after an update".
    • Redirected profile. With a redirected, UNC or subst %APPDATA%, the move and the
      path rewrite work and the Crew vault still unlocks.
  6. The private-environment filters lost the retired upstream's variable prefix (11fb8b649).
    A machine that still has that project's prefixed secrets in its environment no longer has them
    scrubbed from child processes. The owner approved this; it is security adjacent.
  7. The LICENSE appendix's copyright line for the retired publisher was removed (9bdb97464,
    owner decision, ruling 19). NOTICE keeps the attribution verbatim, and the repo guard allows
    only NOTICE.
  8. The sidebar draws only the rows near the viewport (2ba556332, baee980fa, d0bd8fad1,
    2ec504f4c). Rows outside the drawn range fold into placeholders of their exact height. The
    current chat, a focused row or header, an open row menu and a row being renamed stay drawn, and
    arrow keys bring an out-of-view line back first. Please review the keyboard paths and how a
    screen reader reads a list that is only partly drawn.
  9. The Ollama card names the configured server (cb527b765, ruling 30): with OLLAMA_HOST=0.0.0.0 the
    public note named the probe address 127.0.0.1 and so contradicted itself. It now names the configured host;
    the note's wording, role, test id and the public/private decision (the daemon's tier) are unchanged.
  10. The landing site deploys from main; merging publishes the website (not an app release).

🤖 Generated with Claude Code

…e: the group's opacity is gone, so each disabled control paints at its own 50% step (a field was 0.25, the dark number fields lost their fill) and the labels and InfoTips stay at full ink; a Chromium test reads the opacity product (0.5 fields, 1 labels, light and dark, on turns it away) and puts the old rule back to see 0.25 [R-27]
…r a keyboard and a pointer. V4-R3-1: the InfoTip's focus fill and 2px edge move off the 24px hit box (which starts 1px before the label's end, so the edge lay over the last letter: 'running', 'DEFAULT MODEL') onto a 20px ::before 2px inside it, under the glyph (z-index -1, isolated button); the OS ring under prefers-contrast/forced colours is drawn on the same 20px edge (button.br-info-tip outline-offset -4px !important) instead of 2px outside the hit box; hit box, glyph place (4px after the label) and line height unchanged. V4-R3-5: a SettingRow's value and control sit on the label's line (align-self flex-start, the control box = label line + 12px, the value centred on the label's line), so a status line ('Restart to apply', 'Checking for updates…') grows the row downward only and the switch just clicked stays put (was 8px down); one-line rows draw exactly as before; scoped by data-slot so hand-built two-line rows (Local models, Memory) keep a centred control. New settingRowStability.browser.test.tsx (Chromium, real main.css + Tailwind + the renderer's fontScale pass, standard and Large, light/dark/contrast/forced): focus changes no label pixel and does mark the tip; the clicked switch, the value and the button keep their place and rows below move by the growth; instrument cases put the old rules back and see the overlap and the 8px slide; against HEAD's main.css the 3 fix cases fail. tabFocus.test reads the LAST OS-ring media block (the InfoTip's own adjustment shares its head). main.css staged whole: only these hunks [R-27]
… an InfoTip is named by the label's words (plainTextOf, Field infoLabel), its focus mark is a 20px box inside the hit box, a SettingRow's value and control sit on the label's line, and the new settingRowStability browser test [R-27]
…p's are: the title never wraps and a chat too narrow for it ends it in an ellipsis with the row's chevron after it, outside the clipped words ('Ran LiteratureAg… >'), instead of a lone 'Ran' over 'LiteratureAgent >' that made the public row 40px beside the private chat's 20px. Only the affiliation badge may drop to a second line. The 320 and 340px chats keep their tool rows now (they were left out for being too tall). layout-stability.browser.test.mjs measures the title's line boxes, the ellipsis and the chevron at 12 widths, and is red against the previous stylesheet [R-27]
…toFocus field gives focus back to its opener: React focuses the field as the content commits, Radix's focus scope then finds focus inside and never fires onOpenAutoFocus, so the opener was never read and Escape left focus on body (WorkflowResourcePicker, and any later dialog). inputTrail records where each focus move came from (focusin relatedTarget, focusArrivedFrom); DialogContent and SheetContent read it when their content ref attaches with focus already inside (useReturnFocusToOpener.recordFocusAlreadyInside), through resolveDialogOpener so a menu pick still returns to the menu's trigger under the menus' key rule; read once per content node so a re-attached ref never takes a later move as the opener. dialogAutoFocusReturn.browser.test.tsx (Chromium, real keys and pointer): Radix's own content leaves body (instrument), Dialog/Sheet/ModalShell return to the opener on Escape, keyboard Cancel and after a pointer open, a key pick from a menu returns to the trigger, a Tab inside does not become the opener; 6 of 7 fail on the old primitives. Known red, by design: skills/skillsFocus.browser.test's instrument case (it asserts autoFocus leaves body); WS-SKILLS asked to invert or delete it [R-27]
… opens on an autoFocus field reads its opener when its content ref attaches [R-27]
… real owner of the provider form's labels (providerFieldName, through the exported sentenceCase) and says getUiNames is used by neither; sentenceCase stays exported with its signature [R-02]
…s a guard: with autoFocus back on the three fields, Escape and the keyboard Cancel still give focus back to Add, because DialogContent now reads its opener when its content mounts with focus already inside (218cfe0). It was red by design (it asserted BODY). Swapping the pre-218cfe015 ui/dialog.tsx into the bundle fails exactly this case with BODY and leaves the other seven green [R-27]
…ers return focus to Add now: MarketplaceDialog leaves its box alone until Radix asks, and since 218cfe0 the dialog primitive also reads the opener for a field that already holds focus; measured in Chromium that putting autoFocus back into MarketplaceDialog still lands Escape on Add, so the cases pin what a person sees and the instrument (primitive return removed) is what proves they can fail; no assertion changed [R-18]
…a.css relies on (.biorouter-message-tool, .br-transcript-row-line, the head's max-width against the line's content box) and the test that fails if one moves; reply to WS-TRANSCRIPT-2 [R-27]
…t per drawn row: a mounted menu root adds a capture keydown listener to the document and each key press makes every one add pointerdown and pointermove listeners, so 60 rows put 121 keydown listeners on the document and a switch after a scroll to 4,233 rows spent most of its time adding and removing them (production: switch 589 -> 85 ms, menu 279 -> 116 ms, submenu 181 -> 45 ms, WS-PERF's harness). The list has ONE right-click menu (ContextMenu around .br-sidebar-chat-groups, the row read from data-row=chat:<id>; a right-click or long press anywhere else opens nothing; the rename editor's li keeps the app's text menu; the row lights through contextMenuOpen); every row and folder header ⋯ is a LazyMenuButton, a plain aria-haspopup button until focus, a press or Enter/Space/ArrowDown arms it, focus kept on the armed node, released on blur with the menu shut. Handlers, copy, styles and testids unchanged. New RecentChats.menuRoots.test.tsx: the two listener cases fail on HEAD (121 vs < 5, checked by swapping HEAD's file in), the two behaviour cases pass on both. Doc paragraph on switch cost updated [R-28]
…te bucket once per calendar day, not once per chat: chatDateBucket builds an Intl.DateTimeFormat (about 25 us) for every chat older than 30 days, so a 5,542-chat store spent 115 ms in the grouping alone, most of the 0.7 s the reviewer saw between the list landing and the rows. Measured in Chromium over the real component (harness, 5,542 chats): 310 ms -> 185 ms from the list landing to the first row, 2,100 ms -> 910 ms under 4x CPU throttle; the 910 is 670 of React dev-mode work that does not grow with the list (50 chats: 670) and about 240 of list work. groupSessionsByDate (utils/dateUtils.ts) keys the bucket by local calendar day; dateUtils.test.ts holds it equal to groupByChatDate over 3,000 seeded instants (DST, year line, future, unreadable) at three values of now, counts the formatters built (at most one per day; fails against a per-row call), and pins the Older group. The sidebar still pays the per-row cost in chatDateBuckets.ts (WS-SIDEBAR's): requests/WS-SIDEBAR/WS-HISTORY-4.md carries the formatter-cache diff (3.7 ms for the same list). sidebar-chat-list.md says it [R-28]
… its kind once, in the glyph: the title goes through History's resolver (sidebarRowTitle -> historyRowTitle), so a scheduled run stored as 'Scheduled job: daily-meditation' reads 'daily-meditation' beside the clock and an app's chat stored as 'app:<id>' reads as the app's name; keyed on the kind, so a chat a person titled 'Scheduled job: x' keeps it. The row's accessible name, its ⋯ label, the rename field's label and the delete sentence use the shown title; Sort by Name sorts the shown title. Display only: the rename draft starts from the stored name, Export is titled with it (as History), a click hands it to the new tab, the Meditation job's prefix rule is untouched. Tests: RecentChats.test.tsx (row title and names, a person's 'Scheduled job:' chat kept, rename from the stored name, delete sentence; all fail against the previous RecentChats), sidebarChatView.test.ts (sidebarRowTitle cases; the name sort case fails on the stored-name key); the truncation case now expects the app's shown name. Doc: sidebar-chat-list.md (A row, name sort, test rows incl. menuRoots). Tab strip half forwarded to WS-MOTION (chatGroups owner) [R-27]
…its month formatter once per shape (with and without the year) instead of once per chat older than 30 days: a construction costs about 25 µs, and the sidebar groups every loaded chat by date on a switch, re-sort or regroup (WS-HISTORY measured groupByChatDate over 5,542 chats at 104 to 117 ms as shipped, 3.5 to 4 ms cached). Labels, keys, ranks and the en-US locale unchanged. New chatDateBuckets.test.ts case counts constructions with a subclass of Intl.DateTimeFormat over 3,000 old chats: at most 2 (3,000 on the previous file, checked by swapping it in), and both label shapes still read as a fresh formatter gives them [R-28]
…e with the Field error, not Chromium's themeless bubble: the Name Input carried the native required, so the browser blocked the submit before handleLocalSubmit ran and no danger edge or line appeared. The form is noValidate, no control has a native required, and both Name and Workflow file are aria-required (Field required for Name; on the Input for Workflow file, whose Field child is the row div). Tests: an empty submit shows 'Enter a name for this schedule.' with aria-invalid and an accessible description, the same for an empty Workflow file, and a wiring case pins noValidate and no required (the first and the wiring case fail with the old code put back) [R-27]
…leaves focus on the dialog box, so the switch wears no keyboard ring: useHandFocusOnReady hands focus to the first control after a pointer open only when it is a text field (restOnDialog records lastInputWasKey per dialog), a keyboard open hands focus on as before. Chromium test drives the real menu with mouse and keyboard and reads :focus-visible (fails on the previous dialogFocus.ts) [R-25]
… window: the composer mounts with a 128k fallback and isTokenLimitLoaded false, and the ring drew at once whenever the chat had tokens, so every open and tab switch showed a reading against a window nobody looked up (a 1M chat 10% full read '20% left · 102.4k of 128k'; one past 128k drew the empty ring and '0% left' for 700ms). useLoadedWindow (derived during render): no ring, no figure until a window loads for the chat; a re-read in the same chat (model change, app selection moved) holds the last LOADED window instead of blinking out; no hold across chats or when the re-read finds no model. The popover gauge reads the same window, and draws nothing alone against an unloaded one. Tests: ContextWindowIndicator.test (6 new: no ring/figure while loading then one reading at 82% of 1M, hold, no hold across chats or with no model, gauge); contextRingMotion.browser.test opens unloaded and asserts no arc and empty text in Chromium. Mutation-checked: old draw rule fails 5 jsdom + the Chromium case, no hold fails 1, hold across chats fails 1 [R-27]
…dalShell md (480px), pinned at the top (y=90 in a 900px window, and it stays there when the private-chat warning arrives) with the header rule, no 36px warning tile, default 32px footer buttons (words only: with their glyphs the three wrapped at 480px under Large text; Cancel secondary as in every footer, one coral button). The lead sentence, the list and both warnings are the body and the dialog's description; focus opens on the dialog box instead of the title's InfoTip; busy state is purpose required (no close, no Escape). ModalShell gains titleInfo: an InfoTip beside the title, outside the heading, so the dialog keeps the title as its name. Words, testids, userActionHeaders and the private warning unchanged. Tests: diagnosticsDialog.browser.test.tsx (4, real component, compiled main.css; all fail on the previous file via DIAGNOSTICS_SOURCE, the Large text case on a glyph variant), Diagnostics.test.tsx (+3), ModalShell.test.tsx (+2) [R-27]
… a dialog title goes in ModalShell's titleInfo (outside the heading that names the dialog); Report a problem is ModalShell md with a word-only footer and focus on the box; diagnosticsDialog.browser.test.tsx in the tests list [R-27]
…he shared ui/Select, not a native <select>: the OS chevron and OS popup were the only ones left in the app; same option strings, label still linked (for and inputId), data-testid on a wrapper, menu portalled, disabled while a start is in flight. Unit tests drive the combobox; a Chromium test measures the 32px trigger, the 16px chevron, the 32px rows with a trailing check and the disabled step, light and dark [R-25]
…s near the viewport: sliceSidebarLines folds the rows (and a Show more) outside a drawn range into gaps of their exact line count and says when a header is outside it, keeping the rows it is told to keep; sidebarLinesInView reads the first and last line in view from the drawn groups' boxes by binary search (about 20 boxes of 1,200 folder groups), never assuming a header or gap height; nextSidebarDrawRange keeps the same range while the view stays half an overscan inside it. Pure and tested (13 new cases); RecentChats takes them up through inbox WS-PERF-4 [R-28]
…may settle the footer ring: loadProviderDetails runs again on a model or binding change and was never cancelled, so a lookup for the app's model that finished after the chat's pinned model marked its window loaded and the ring drew a confident wrong reading. A request counter in a ref gates one settle(limit, loaded) in place of the seven setTokenLimit/setIsTokenLimitLoaded pairs (no-model, llamacpp, predefined, provider, pattern, default, error). New ChatInput.contextWindow.test.tsx drives the real footer and ring with held lookups: newest wins when the older finishes later, an older failure cannot restore the default window, in-order still settles; 3 of 4 fail without the guard [R-27]
…ws only the lines near the viewport: renderBudget still says how far down it reaches, and of those lines only the range in view plus 12 on each side are rows; the rest fold into .br-chat-row-gap placeholders of their exact height and a far folder header is an empty box of its height. The lines in view come from the drawn groups' boxes (sidebarLinesInView) on every scroll, committed before paint, and in a layout effect after anything that moves lines. Kept drawn whatever the range: the focused row, a row being renamed or just renamed, the row whose right-click menu is open. Home and End scroll to that end first. scrollRowIntoView shows a header only for a group's first line. The list's ContextMenu is modal={false} like its ⋯ twin: no pointer-events on body (about 50 ms of restyle each way with 2,100 rows drawn) and no scroll lock. WS-PERF measured, production, 2,100 lines in reach: 45 rows drawn instead of 2,100, right-click 16 to 26 ms (was 171 to 214), switch 21 to 35 ms (was 41 to 50). Tests: RecentChats.drawRange.test.tsx (3, fail on the previous file), a Chromium case in sidebarFill.browser.test.tsx (400 chats scrolled to the middle: rows on their lines, exact list height, no placeholder in view, fewer than 80 rows, body untouched by the menu, Home to s1). Applied as sent [R-28]
…try keeps the answer-actions reserve on every nested row, on purpose: the calls share one right edge, where freeing the upper ones would clip the bottom call's path 192px earlier than its siblings at rest. Pinned in Chromium with ExploredRow's DOM (nested rows share a right edge, the last clears the widest cluster, unmarked it lands on the chevron; fails if the upper rows are freed and if the reserve is removed); the trade-off is written next to the rule in message-meta.css [R-12]
…p shows historyRowTitle of the same source its glyph reads, so a sub-agent tab reads 'Reply with ALPHA' beside the robot glyph (was 'Subagent: Reply with ALPHA') and an app chat reads 'Living news router' (was app:living-news-router). Label, tooltip, drag ghost, close and rename-field names use the shown title; the rename draft and its rollback keep the stored name; the overflow menu shows the same title and its filter matches both spellings. Keyed on the kind: a plain chat titled 'Subagent: x' keeps it, and a scheduled run keeps 'Scheduled job:' because its tab draws no clock. New ChatTabStrip.shownTitle.test.tsx (9; 6 fail on the previous strip, the 3 guards pass on both) [R-27]
… Text size preference: the rows and the list's 2px gap scale with the type (vite-plugins/fontScale.mjs), so under Larger text a line is 34.5px, but the lines in view were read at a fixed 30px, which put them 15% further down the list than they were: from the middle of a 400-chat list on, the view showed one row and placeholder. measuredLinePitch reads the pitch off a drawn row (its height plus its list's row gap), standard 30px when none is drawn; .br-chat-row-gap scales its own 2px (a height is not a property the plugin scales; unscaled it drifted 0.3px a line, 56px by line 200). New Chromium case serves the stylesheet through the app's font-scale plugin with --app-font-scale 1.15 and scrolls to the middle and to 90%: rows in view on line N x 34.5 within 1px, no placeholder in view, list height exact within 1px. Fails with either half reverted (1 row in view; rows 56px off). The V1-R2-2 case shares the helpers [R-28]
…t change when only the rows near the viewport are drawn, with the list scrolled away under them by the wheel. ArrowUp and ArrowDown on a line out of view bring the list back to it first (scrollRowIntoView + the range before paint), so they reach the next line: they jumped to the first row drawn far down the list (s5 ArrowDown went to s189). The current chat is kept drawn, so the single Tab stop stays on it (Tab landed on a row in the middle). A row's or folder header's ⋯ menu opened with the pointer keeps its line drawn (Radix keeps focus off the trigger and the menu is not modal, so scrolling unmounted it and the menu vanished): onLineMenuChange on RecentChatRow and FolderHeader. A focused folder header or Show more is kept drawn like a focused row (it was folded into a placeholder and focus fell to the page): sliceSidebarLines takes keepLine(entry, 'header' | 'more'). Tests: two Chromium cases in sidebarFill.browser.test.tsx (each subcase fails with its piece removed: s189, Tab to s189, menu count 0, header focus null, s198), sliceSidebarLines keepLine unit case. Doc: sidebar-chat-list.md drawing paragraph (kept lines, the measured pitch, the arrow keys) and test rows [R-28]
…ontract: the comment names the Chromium case in transcript/messageActionsGeometry.browser.test.tsx that rebuilds it and the reserve message-meta.css puts on every nested row; comment only [R-12]
…y a vite run from the root writes (untracked public/ failed the clean-status step); ui/desktop/.gitignore only covered a run from ui/desktop [R-12]
…take the two wordings the workstreams asked for (the moves the pointer does not hold, the message actions' 26px rise), spec section 10 records owner messages 15 and 16 (R-29 to R-31), the plan and tracker add R-29 to R-31, the tracker sets each requirement's status from vision round 4 and logs 2026-10-10 and 2026-10-11, and the handoff states what is left (vision rounds 5 and 6, the round 4 lows, the final gate, the pull request) with four new traps [R-24]
…B in spec 3.19)): CI Rust / guards, Biorouter Copilot naming consistency: the V3-R2-6 comment in extensionDisplayName.ts named the removed capability in backticks with no legacy mark, which rule 1 of scripts/check-computer-use-naming.sh refuses (its one exemption is 'legacy' followed by whitespace and the phrase). Comment reworded to 'the legacy Computer Controller name' and reflowed; the script is untouched. Checked: the guard passes on this file (the other offender it reported, settings/config/configLabels.ts, is not this workstream's and is being edited by its owner), check-vendored-computer-use.sh passes, Prettier and ESLint clean, extensionDisplayName.test 14/14. Comment only: no behaviour, copy, privacy logic or main.css change [R-18]
…ndows-latest), app_folder::tests::only_the_daemon_moves_the_folder failed at tests.rs:1520 (run 38204107268, job 114663666501): the census found the one right caller (biorouter-server/src/main.rs in the Commands::Agent arm, callers.len()==1 held), but kept it as path.display().to_string() and compared with str::ends_with("biorouter-server/src/main.rs"), which never matches the backslash spelling Windows displays. The caller is now kept as the PathBuf and compared with the component-wise Path::ends_with, the same call the loop already uses to exclude app_folder/mod.rs (which the Windows run proves works there: mod.rs:529 carries the needle and was excluded). Same claim on all three OSes, no platform gate; the assertion is stricter, not looser: a partial component suffix (server/src/main.rs) now fails where the string match passed (mutation run red, restored). Tests: app_folder:: 31/31 on macOS; clippy -p biorouter-mcp --lib --tests -D warnings clean; rustfmt clean. No app code, copy, privacy logic or main.css change [R-18]
…er 'gives a hovered tab a fill that can be seen in both modes' failed on ubuntu (light chat hover vs selected 1.019679 < 1.02): over() painted the colours on a canvas and read back 8-bit pixels, and how 8 bits round the 5% hover ink over the #f7f7f5 band depends on the raster backend (Chrome 155 GPU canvas [237,236,234] 1.0217, CPU canvas [236,236,234] 1.0197, CI's exact value; the blend itself [236.75,236.5,234.35] 1.0247). over() now parses the computed colours (rgb/rgba, oklab, color(srgb)), converts oklab to gamma sRGB (Ottosson matrices) and composites source-over in float in gamma sRGB, so every bound measures the token on every runner; a new check holds the oklab conversion to the engine's own color-mix(in srgb) conversion within half an 8-bit step. Bounds unchanged (1.08, 1.04, 1.02, 1.04); exact minimum 1.0247. Reproduced CI's 1.019678664696824 on the old code with --disable-gpu and with willReadFrequently; fixed code 9/9 with and without --disable-gpu. Mutations: oklab L skewed 1% and b sign flipped each fail the engine check. Prettier, ESLint, tsc clean. No app code, copy, privacy logic or main.css change [R-27]
…-input.browser 'says the shortcut once' failed on the Linux runner (expected 'Search history · Ctrl+F' to be null, job 114663666236 at a7d0693): the fixture hard-coded History's Mac placeholder 'Search history… ⌘F', and FilterInput hides its hint only when the placeholder carries uiCopy.findShortcut(), which is Ctrl+F off a Mac, so on Linux the hint opened, correctly. The fixture now builds the placeholder as History does (historySearchPlaceholder(uiCopy.findShortcut()), SessionListView), and the case runs as darwin and as linux on every runner by giving the page window.electron.platform (isMacPlatform reads it before navigator): each arm asserts the platform took (aria-keyshortcuts Meta+F / Control+F), the exact placeholder ('Search history… ⌘F' / 'Search history… Ctrl+F'), no tooltip over History, and, so the null is not blind, the plain field's hint 'Filter · ⌘F' / 'Filter · Ctrl+F' on the same page. Mutation: FilterInput testing includes('⌘F') instead of the platform spelling fails the linux arm with CI's exact message, darwin passes. Tests: the file 7/7 (was 6). Prettier, ESLint, tsc filtered clean. Test only: no component, copy, privacy logic or main.css change [R-27]
…sAndRows.browser 'further in dark' failed on the GPU-less Linux runner (dark delta 0.026937 vs > 0.04, job 114663666236 at a7d0693): the row's wash, --overlay-hover (oklab ink at 5%) over the page ground, was read off a 1x1 screenshot, and 8-bit raster rounds it by backend. Reproduced in Chrome 155: GPU raster [38,38,35] (1.0834 to 1.1368, +0.0534), --disable-gpu [37,37,35] (1.0964 to 1.1233, +0.0269, CI's number); exact [37.85,37.65,35.25] (1.0871 to 1.1329, +0.0458). One step at #25/#26 moves the delta by 0.027, more than the 0.006 margin. The wash is now composited exactly in float from the computed colours (exactGround: the row's background over each ancestor's down to the first opaque one, oklab and rgb parsed, any other syntax or a background-image throws; this file's own copy of tabFill's helper), after every finite transition has settled; the badge and the strong probe stay screen pixels (opaque, exact on every backend). New instruments: the row holds the settled --overlay-hover, and the screen's wash is within one 8-bit step of the exact one (GPU and CPU both). Bounds unchanged: before < 1.1, now >= before, dark now - before > 0.04; light exact 1.0980 to 1.0986. Comment records the GPU/CPU/exact numbers and the caveat: on a CPU-rastered screen the dark step really is 0.027, and holding the claim there is a token change, not a test change. Mutations: a washed row's badge kept on the strong step fails (delta 0); compositing over nothing fails the screen instrument. Tests: the file 7/7 with GPU and with --disable-gpu; with filter-input.browser 14/14. Prettier, ESLint, tsc filtered clean. Test only: no component, token, privacy logic or main.css change [R-27]
…me from the coordinator's FR-R1-7 commit)): CI Frontend / Unit tests (vitest), bootSplash.browser 'is the wordmark the app draws' failed on ubuntu-24.04 with Google Chrome stable (run 38204107261, job 114663666236, at a7d0693): expected 0.1200000000000001 to be <= 0.006 at the viewBox close(). The splash's viewBox and rects are static numbers read off <BioRouterWordmark/> in Chrome on macOS (CoreText); Linux Chrome measures text through FreeType/Skia, and the component's viewBox x came out 0.12 units (about 0.04 px) off the markup's -7.21 (the float residue of the difference places it at the first of the four numbers). Gating only the two-decimal check would not have been enough: on macOS, moving the splash's x by +0.12 or -0.12 alone makes 167 and 48 pixels differ (bound 40), so the unpinned pixel comparison cannot hold where the measurement differs either. The case is split: (1) on every platform, face, weight, family, font-size, 160x49 box and two rects are equal, and the component given the splash's numbers (viewBox and rects pinned after React settles) rasterises to the same pixels, bound unchanged at 40; (2) it.runIf(process.platform === SPLASH_MEASURED_ON, 'darwin') keeps the old checks unchanged: two-decimal viewBox and rects within 0.006 and the unpinned pixel comparison below 40, with a comment recording the CI measurement and why. A skip shows as skipped on Linux. Measured with a scratch copy reading a mutated index.html and an overridable platform: the CI drift (x -7.09 or -7.33) as darwin fails case 2 with exactly the CI error, as linux passes case 1 and skips case 2; letter-spacing 1px (1109 px) and a swapped ink (769 px) fail case 1 on both; a moved coral bar or a halved navy bar fails only case 2 (pinned numbers, documented in the comment). Locally: bootSplash.browser 10/10, boot-splash 43, ESLint, Prettier, tsc clean. design.md's splash paragraph says which half runs where. Not verified on a Linux Chrome: case 1 on CI is the next run's to confirm. index.html's comment that the numbers are deterministic for Inter 600 is now known to hold only per platform (coordinator's markup, left as is). No app code, copy, privacy logic or main.css change [R-34]
…)): CI Frontend / Unit tests (vitest), toastYieldSelect.browser portalled case timed out at 30000ms on ubuntu with Google Chrome (job 114663666236, a7d0693). Cause, reproduced: the harness mounted the Select with menuIsOpen already true, and react-select hands its menu portal the control from a ref, so on that first render MenuPortal gets controlElement null, computes no position and renders nothing; only a second render fills it in. On macOS componentDidMount calls setState({ isAppleDevice: true }), which is that second render, so the case passed in under a second here; on Linux and Windows nothing renders it again and waitForSelector for the open menu waited out the budget. Measured: in a Linux container (Playwright noble, Chromium new headless, headless shell hidden so the launch order falls through as on CI) the old file failed the portalled case at 30005 ms with the dialog case green, exactly as CI; the same file with navigator.platform reported as MacIntel passed; on macOS with the platform reported as Linux the old file failed at 30003 ms. Fix: OpenedSelect mounts each Select closed and opens it after its first commit, as a person opens one; no Select in the app mounts open (grep), so app code is unchanged. Also instrumented as asked: every wait is bounded under the 30 s budget and named (newPage, emulateMedia, setContent, addScriptTag, each evaluate, close via bounded; both waitForSelector calls with timeout 10000 via waitFor; settle bounded at 5 s in the page and 8 s from node), and a failure appends a page report (visibility, focus, whether a frame still comes, toasts, Select menu states, unfinished finite animations with name, play state and target). Mutants checked the instrument: a paused animation fails 'settling after the toast' at 6.3 s naming the animation, no rAF fails it with 0 of 2 frames, a menu that never opens fails 'waiting for the open Select menu' at 11.4 s with Select menus [], a busy main thread fails at 11.3 s with the page did not answer. Assertions unchanged, budget unchanged, no retry. Tests: the file 2/2 on macOS; 5/5 runs in the Linux container idle and 5/5 under 6 spinning loops on 4 CPUs (portalled 0.7 to 1.1 s); alerts/** plus styles/toastLayer.test 120/120. ESLint, Prettier, tsc filtered clean. No app code, copy, privacy logic or main.css change [R-27]
…in the shared CopyField (ruling 25, a named exception to ruling 15 for frozen privacy UI; PR human-review list). The bare <pre> with no Copy is replaced by <CopyField className="mt-4" value={HOST_CONFIGURE_COMMAND} label={HOST_CONFIGURE_COMMAND} />, as every other command on first run; the shown text, test id, title, reason and reload line are unchanged. New HostManagedModelPanel.test.tsx (2): the command sits in a copy-field, no <pre> remains, 'Copy biorouter configure' writes exactly the command, the panel keeps its title, reason and reload line and holds one control. Mutation: the HEAD panel fails both. Tests: privacy + ProviderGuard + src/test + src/styles 66 files 1015/1015; controlsSourceGuard, emphasisWeight.sourceGuard, copy-field 33/33. tsc 0 errors; eslint --max-warnings 0 and prettier clean. No main.css change [R-18, R-34]
2a: useCostTracking.ts joins the WS-SUMMARY ownership row (3.17).
2b: the one logic exception in DependencySetupModal (hasInstaller, FR-S-7)
joins the class-only list, with the corrected note on where the # fallback
appears.
2c: biorouter-icon-entrance leaves the WS-TRANSCRIPT main.css family row.
2d: the boot splash is no longer frozen, and boot-splash.test.ts is no longer
unchanged (FR-R1-7).
2e: the Tools count and the folder line say when they appear (V2-R7-3).
2f: the raw DialogContent migration status (ruling 23 approved the disclosure
swap), the testId prop, and the two presentation-only edits made inside frozen
files (DeclassifySessionDialog, ModelAndProviderContext). [R-24]
…ops saying "laptop tier" (ruling 26, FR-R3-6). MODEL_CATALOG's gemma4-12b description now reads "Mid-size Gemma 4 model with a larger context window, for machines with 32 GB or more of GPU memory", the number its recommended_gpu_memory_gib already holds, so it no longer half contradicts the panel's Memory row on a large machine. One string, no behaviour change; no test or fixture pinned the old text (git grep). New unit test gemma4_12b_description_names_its_memory_not_a_tier: the description contains no "tier" and names "{recommended_gpu_memory_gib} GB" (the old string fails its first assertion). cargo test -p biorouter --lib providers::llamacpp 45/45; cargo fmt --check clean; cargo clippy -p biorouter --lib --tests nothing in llamacpp.rs [R-34]
… (ruling 27, V4-R7-2). Applied RULINGS-NEEDED-7-privacy-switch-seed.patch as written (git apply --check clean): PrivacyPanel's enabled and record initialisers read useConfig().config, the GET /config snapshot every privacy badge and the composer's off note paint from; an absent key (empty cache) keeps null, so the switch stays held and unpainted and nothing is claimed before the daemon is heard; the mount read still runs and replaces the seed; the P-05 write path (never setEnabled(on), read back after every write) is untouched; no copy, prop or test id change. PrivacyPanel.test gains the patch's four cases (first render on, first render off with the strip, empty cache holds, the read overrides a differing seed). tabSwitchFrames.browser now mounts the real Privacy panel (only the served statement is stubbed, copy null), its ConfigContext stub serves a config cache (loaded unless a page empties it), and Privacy tiers joins APP_SWITCHES: at rest with its value in the first commit and every frame on both visits, and counted in the constant column (6 switches from frame 1). Instruments: the first-visit case asserts Privacy tiers is painted from the cache while the four main-process switches are held; an empty cache holds it unpainted through the visit and the panel's read then paints it on; a bundle mutated back to the pre-ruling panel (seed replaced by null) holds it on both visits; the mutation helper now names its file. Named exception to ruling 15 in frozen privacy UI, on the PR's human-review list. Tests: tabSwitchFrames.browser 9/9 (was 7), settings/privacy 3 files 33/33, settings + privacy 140 files 1326/1326; tsc, eslint, prettier clean; check-no-goose-block clean. No CSS or token touched [R-27]
…acy/NonPrivateModelDisclosure.tsx, DR-17 requirement 3) renders through ModalShell size lg, purpose required, testId non-private-model-disclosure (on the role=dialog node, where it was), scrollBody, the same onOpenAutoFocus on the prose, in place of the raw DialogContent (560px, centred, no header rule, its own max-h-[50vh] scroller). Copy, props, the test id, the I understand/Continue relabel, busy disabling and the prose's ref/tabIndex -1/outline-none are unchanged; onOpenChange is a no-op because a required shell has no x and refuses Escape and the backdrop (the gate owns open). One placement choice beyond the recipe: the refused-acknowledgement notice (disclosure-ack-error) sits in the footer on its own row above the button, outside the scrolling body, as it sat outside the old scroller; as a body child it scrolled out of view at the app's minimum window (1048x600) and the larger text size while the button changed its word (measured). The prose takes py-3, as the other scrollBody dialogs' bodies do. Tests: NonPrivateModelDisclosure.required.test.tsx (8, jsdom) wraps the real Dialog to record every onOpenChange Radix raises, checks that probe on a default-purpose shell (Escape, backdrop and x each ask), then pins that Escape, a backdrop press and the absence of the x each hold on the disclosure, focus on the prose rather than the button, the anchor/size/test id and the notice outside the scroller; mutations purpose info and omitted fail 3, form fails 2 (Escape, x), no body focus fails 1, HEAD fails 2. nonPrivateModelDisclosure.browser.test.tsx (7, Chromium + compiled main.css + text-size pass): top 90, 640 wide, header and footer rules, title and button at the 16px gutter, no x, weights <= 600, focus on the prose with no ring and kept after Escape, still open after a backdrop press, PageDown scrolls the shell body from the prose at 1048x600 x1.15, the notice in view above Continue there, dark geometry equal; HEAD fails 4, notice-in-body fails the notice case. NonPrivateModelDisclosure.test.tsx 17/17 unchanged; gate, BaseChat privacy and dialog description guard green. Privacy UI presentation change for the PR human-review list [R-34]
…ume (load_model_and_extensions: true) answers with the metadata session it already read and skips the second, full conversation read, so opening a large chat no longer sends its transcript twice. read_resume_session takes include_transcript (= !load_model_and_extensions); the metadata read and refuse_subagent_unless_user still come first, session_reach still precedes both in resume_agent, and R-30's opening scope still wraps load_resumed_agent_extensions. The false (paint) response is the same full read as before. Confirmed the readers of the true answer read metadata only: restore_persisted_provider_if_missing / restore_provider_from_session (provider_name, model_config, id), restore_subagent_runtime_profile (session_type, id, extension_data, working_dir), load_extensions_from_session (working_dir, extension_data, id), ensureAgentLoaded (session.id). The true body now has conversation null and message_count from COUNT(*). One more reader found that section 8 did not list: the CLI's --shared-daemon --resume --history printed session.conversation from its resume(true) state; it now reads it through resume(false) when the state has none (no extra request against an older daemon). Tests: route test that the false body equals the store's full read and the true body is that row with conversation null; both bodies refused by session_reach (private chat) and refuse_subagent_unless_user (public child) with identical words and no agent minted; store-reader tests for both flags (refused: one metadata read; proven agent load: [false]); source guard that the metadata answer sits after the proof; CLI test; renderer test (chatStreamStore.resumeMetadata.test.ts, a proxy row recording reads) on the cold open, the LRU path and a queued child's reuse path. Mutations: ignoring the flag fails 2 route tests; spreading the phase-2 session in the store fails all 3 renderer tests. cargo test -p biorouter-server --lib -- routes::agent 79 ok, opening_is_not_activity 5 + 20 ok, -p biorouter --test privacy_guard_wiring 3 ok, -p biorouter-cli --lib -- shared_conversation 18 ok; clippy -D warnings on both crates, no new too_many_lines; rustfmt; tsc, ESLint, Prettier clean. No OpenAPI change [R-28]
…ec 2.6, V3-R7-1), DialogContent drops its p-6, gap-4 and sm:max-w-lg defaults now that the non-private model disclosure is a ModalShell (96820ea). Every remaining raw site sets its own: ModalShell (gap-0 p-0 and MODAL_SIZE, so its merged classes are unchanged) and nothing else reachable; the one other site, settings/tunnel/TunnelSection.tsx (frozen, unmounted, nothing imports it), sets only its width and loses its insets, which COORDINATOR-DELEGATE-3 accepted (no person can open it). A raw content now opens with no insets rather than as a near-miss dialog. Tests: dialog.test.tsx +2 (a raw content carries no inset, gap or sm width, keeps the max-w-[calc(100%-2rem)] clamp; a caller's own still applies); new ui/rawDialogContent.sourceGuard.test.ts (TS AST: every JSX use of the primitive's DialogContent under any import alias, comments and strings ignored) allows only ModalShell.tsx and the tunnel section, proves it is not vacuous (finds the shell once and the tunnel's two), and fails if anything imports the tunnel section. Mutations: defaults put back fails the dialog case (gap-4, p-6 named); the HEAD raw disclosure put back fails the guard naming privacy/NonPrivateModelDisclosure.tsx:66; restored by cmp. Suites: components/ui, privacy, ModalShell, schedule, DeclassifySessionDialog, ModelAndProviderContext, InstructionsEditor, the dialog description guard, 119 files / 1225 tests green (jsdom and Chromium). docs/desktop-ui/shared-controls.md says every dialog is a ModalShell, names the disclosure's move and the two new tests. ESLint, Prettier, tsc clean. No main.css change [R-34]
…the bare <strong>/<b> source guard is renderer-wide: ui/emphasisWeight.sourceGuard.test.ts now scans every non-test .tsx under components/ (was components/ui, ModalShell and the five class-only dialogs) and fails on a strong or b whose opening tag names no font-semibold, font-normal or font-medium, naming file, line and tag. It reads the TypeScript syntax tree, so a tag in a line, block or JSX comment or in a string is not an element and br/button/blockquote are not b (the old regex read text). Excluded, as the ruling says: settings/dictation and settings/tunnel (frozen, unmounted, 4 bare strongs no person can see); a third case fails if anything imports either folder. No main.css base rule. Passes on arrival (owners' class sweeps f822b2f, 8dbf27c, cc0c32e, e4a24ce, 757d4e0). Matcher case: 9 elements in a sample, 5 bare (plain, b, multi-line, spread-only, a look-alike class), comments and a string ignored. Not vacuous: more than 10 weighted emphasis elements seen across components. Mutations (impl/WS-PRIMITIVES-tools/r23disclosure/mutate-emphasis.sh): no exclusion names the 4 (ProviderInfo 22/31, TunnelSection 153/216); DisclosureProse's strong made bare names privacy/DisclosureProse.tsx:48; CopilotControl's names copilot/CopilotControl.tsx:334; a bare strong in a JSX comment stays green; restored by cmp. modalEmphasisWeight.browser 5/5. docs/desktop-ui/shared-controls.md says the guard's new reach. ESLint, Prettier, tsc clean. Test and docs only [R-27]
Semantic fixes made inside the merge (merge plan 2026-10-12, section 4):
- 4.1 routes/apps.rs opening_is_not_activity_app_tests: the test manifest
  sets app_uid: None, and load_page mints the socket token the way
  serve_index does, ws_token_for(app, manifest.app_uid.as_deref()) from the
  stored manifest, so #392's page-token gate admits the page (3/3 pass).
- 4.2 #391's CommandChip restyled to the composer chip idiom (no
  --motion-fast); its copy moved into COMPOSER_COPY.
- 4.5 app export goes through #392's IPC (window.electron.exportApp); the
  renderer's buildExportUrl/ExportOptions/ExportMode path is gone.
- 4.6 ChatInput.bugCommand.test no longer passes the removed
  disableAnimation prop.
- landing/docs.html regenerated from the merged docs/website/pages.

cargo check --workspace --all-targets and tsc --noEmit are clean.
… is checked in Chromium where it is drawn. The restyle itself landed in the merge bf904e2 (CommandChip: size-3.5 aria-hidden glyph, br-composer-chip-action on --dur-fast-min, no --motion-fast; CHIP_COMMANDS reads COMPOSER_COPY.chipCommands, chipCommandSpoken and removeChipCommand in COMPOSER_COPY); verified, not redone. New composer/commandChip.browser.test.tsx (11, real ComposerChips, MessageQueue, ResourceRefText, CommandChip/ResourceRefChip and MentionPopover with its daemon reads stubbed; main.css compiled by Tailwind + composer.css; bubble and edit card are UserMessage's class strings): in the composer row, a sent bubble and the edit box the command chip computes exactly as the ggplot reference chip beside it (height, fill, radius, type, name ink, 14px aria-hidden glyph, 16px action, transition = --dur-fast-min), comes first and stays inside its container; a sent /bug shows no /bug text and shares the prose's first line; a queued /bug is the chip; the slash menu's bug row (daemon row, Built-in badge, SquareSlash) matches the clear row's geometry and type. Light and dark; COMMAND_CHIP_SHOTS_DIR saves 2x screenshots (read: chips identical, menu row aligned). Mutation: #391's pre-merge CommandChip recipe put back fails the 6 chip cases and motion.test, menu and queue stay green; restored (cmp). ResourceRefChip.tsx: doc comment reflowed and points at the test, no code change. Checks: ChatInput.bugCommand 20, composerCommand, MentionPopover, ResourceRefChip, UserMessage.references, MessageQueue, composer/, motion, ChatInput.structure: 13 files / 194 passed; uiCopySpelling 6; tsc, eslint, prettier clean; no-goose guard clean [R-13]
…the restyle never covered follows the redesign's rules: the export dialog's app check face is ModalShell anatomy with a warning Note, both faces scroll their body, an unreadable app is a two-line row, the export toasts and the open-in-browser failure read from copy registries [R-15, R-27]

ExportAppDialog (applications/):
- The app check (lint) face: the consequence stays the visible subtitle; the advice ("Fix them in the chat that built the app, or export it as it is.") moves behind the title's InfoTip (principle 2); the findings sit in one warning Note (AlertTriangle) whose own eight-line ceiling and "Show more" replace the hand-rolled 8-item cap and "N more", so every finding is readable; no nested max-h scroller, the shell body is the one scroller (scrollBody).
- The face stays while Export anyway runs (button "Exporting…", disabled, neutral by ruling 22). It used to fall back to the mode choice under the pointer. Phase becomes running + lint state.
- Focus moves to the named findings list when it arrives (the button that asked turned disabled, which drops focus to body in Chromium).
- The choose face takes scrollBody too: a full export of an app reaching four knowledge bases, a skill and an extension clipped its last rows (Include Biorouter included) at the 1048x600 minimum window, with no scrollbar, because the shell hides overflow. Radios are disabled while an export runs, like the items. "Nothing to include" is a neutral Note (the empty-store line).

ApplicationsView: an unreadable app's row is two lines like every row (principle 4): the danger "Can’t be read" badge carries what it means as its InfoTip help, the second line is the parser's reason (mono, a machine string). Launch's failure uses failureSentence (one IPC unwrapper, same words). The export notes toast stays unclamped under the app's name, but each note is now a sentence (exportNotesMessage: a lower-case daemon clause is capitalised and stopped, words unchanged), so two daemon notes no longer run together into one unpunctuated line.

Copy: exportedMessage's three strings move into appsCopy (exportedTo, downloaded, exportCarries); buildChat's strings (already in appsCopy) take the registry's typographic apostrophe; the lint copy drops moreProblems. utils/openInBrowser.ts exports OPEN_IN_BROWSER_COPY ("Couldn’t open the app", the same sentence Built apps' Launch uses for the same failure, pinned by a test), and its three surfaces' tests (utils, MCPUIResourceRenderer, ArtifactViewer.openInBrowser) import it. WebPagePreview needed no change: its failure surface is this toast.

Tests: ExportAppDialog.test 9 -> 16 (the 9 #392 acceptance cases unchanged except the notes case, which now expects the daemon clause as a sentence; new: every finding in the warning Note with no inner scroller, advice behind the InfoTip, focus on the findings, the face stays while Export anyway runs, radios held while running, the neutral nothing-to-include Note, three notes as sentences unclamped); ApplicationsView.test broken-row case asserts two lines and the badge's description; new exportMessages.test (8); new builtAppsExport.browser.test (Chromium, real origin route-fulfilled, compiled main.css): rows equal height incl. the broken one, body scrolls and the last row is reachable at 1048x600 and 1440x900 light and dark with the footer rule, the lint Note is a wash, one scroller, findings at 12px, folds then expands, focus on the list, every text size on the 12/13/14/17/24 scale.
Mutations (impl/ws-views-a-tools/r392-restyle/mutate.py, each restored byte for byte): choose face without scrollBody, findings out of the Note, no focus move, the face falling back while running, raw note join, a third line on the broken row: each turns its jsdom and/or Chromium cases red.
Screenshots read (rows, full export at 1048 and 1440, lint face folded and expanded; light and dark).
Gates: applications + workflows + BioRouterHintsModal + the three openInBrowser surfaces + src/test copy guards + emphasis/raw-dialog guards + measures = 39 files / 490 tests green; tsc, eslint, prettier clean; no-goose-block clean. No main.css change.
…s again after the #392 merge: its toasts stub gains toastWarning, which ApplicationsView now imports for the export notes toast (a423670, called in ec0c0f3). Stub only, no assertion changed; 11/11 pass, including the restored-disabled instrument cases; prettier, eslint, tsc clean [R-27]
…ter/render.rs is in no workstream's ownership table; WS-BRAND owns the R-31 app-folder rule it now contradicts)): the exported-app launchers read the folder the daemon reads on Windows. run.ps1 and serve.mjs named the retired app folder (main's #392) and now apply rule 1 of app_folder (the older folder only while it is the only one), with the names filled from the app_folder constants; the README line and sdk-reference say the same; the launcher test gains the older-folder cases and the real presence check [R-31]
…r React.StrictMode: its mounted ref started true and only the cleanup wrote it, so the development renderer's StrictMode remount left it false for good and run() dropped every answer. The dialog sat on Exporting… after the folder was written and never showed the app check's Export anyway face. The ref now starts false and the effect sets it true. ExportAppDialog.test.tsx renders three cases under StrictMode (closes after exported, the lint face takes focus and Export anyway closes it with allowLintErrors and the retry token, closes when onExport throws) and the four ApplicationsView export cases mount the view under StrictMode. Mutation: the old ref put back fails those 7, the other 12 pass; restored (cmp). Production builds were not affected (StrictMode double effects are dev only); the same code is on main (a423670). Checks: applications/ + 6 importers 12 files / 270 passed, src/test + src/styles guards 48 files / 902 passed; tsc, eslint, prettier clean
…ys on the row at rest when the meta is a long description. The meta line was one truncate paragraph, so a long description pushed 'Build chat deleted' past the ellipsis (measured in the app: the note at x 1390 against a line ending at 1130). The line is now a flex row of two runs: the meta (min-w-0 truncate) gives way and the note (shrink-0, whitespace-pre keeps the ' · ' separator) never does; textContent is unchanged, the p carries data-testid app-row-meta. builtAppsExport.browser.test.tsx gains Renal Dosing Assistant (long description, deleted chat) and a case at 1048x600 and 1440x900, light and dark: the note is inside the line, unclipped and the element painted at its own centre, the description truncates with an ellipsis before it, the line reads 'description · Build chat deleted', the row keeps the other row's height; open() takes an apps list. Mutation: the one-line truncate p put back fails it (note right 1196 vs line right 787 at 1048); restored (cmp). Screenshots read: note visible light and dark at both widths. ApplicationsView.test.tsx's model and kb line case reads the line by its test id (the facts are now its first run). Checks: applications/ + 6 importers 12 files / 270 passed, src/test + src/styles guards 48 files / 902 passed; tsc, eslint, prettier clean
…'s output keeps its numbers whole. The output well re-voices markdown in 13px mono, where an outside marker is whole advances wide ("1. " = 23.4px), wider than the prose list's 1.5em gutter (19.5px + the item's 2px), so the markdown root's overflow-x cut the first digit ("1." read "l.", "10." read ")."), in any tool output with a list, not only the bug-report method. transcript-row.css now counts the well's list gutter in ch: 3 for one-digit markers and bullets (one text edge), 4 once a list's numbers reach 10 (ten items, a two-digit start, or a one-digit start whose items pass 9; a list continued after a code block keeps 3 while it stays one digit), 5 at 100 items. New toolOutputListMarkers.browser.test.tsx (4): the real ToolCallWithResponse bundled with its component stylesheets, main.css compiled by Tailwind + the text-size pass, rows opened and Show more pressed in Chromium; per item it reads the marker strip's pixels painted and with the root's clip lifted (flow-root, nothing moves) and requires the leftmost ink on or right of the clip edge, inside the well, with no ink lost; gutters pinned in ch. Cases: the bug-report method text from bug_report/mod.rs, twelve items, a list continued from 8, one continued from 3, a start at 12, bullets. Instrument: the rules taken out (the defect as found) cuts all five bug-report markers and all twelve; only the 3ch rule kept cuts 10-12, the continued 10 and the start at 12. Checks: 16 related files / 289 tests pass (TranscriptRow, ToolCallWithResponse x3, ToolCallRows, thinking, geometry browser tests, transcriptMarkdown, uiCopySpelling); tsc, eslint, prettier clean
… R-34)

The Ollama panel, the Llama Server memory warnings and the merged
memory module now follow the redesign rules. Checked in Chromium
with stubbed data in light and dark; no model was run.

- Every memory word comes from localModelsCopy and is written in GB.
- Ollama: the daemon's GiB paragraph about memory becomes the Llama
  rows' warning word, with an InfoTip in GB. The status line no
  longer repeats the model tag. A public server's status line starts
  with "Public", in place of a separate caption. The remote server
  note becomes one instruction with an InfoTip, after the picker.
  The privacy note text stays unchanged.
- Llama: the Windows VRAM note is now the Memory row's InfoTip. The
  Expected speed row drops the catalog's GiB memory clause, here and
  in the Settings info dialog. The confirmation strings move into
  llamaCopy.
- Switch models picker: the fit and state words match the Settings
  rows ("Fits this Mac", "Installed"). It no longer says
  "Recommended here" or "VRAM unknown".
- Local models: the delete and pull messages move into copy.ts. The
  Ollama delete confirm names the model by its display name.
… the probe address (R-34)

With OLLAMA_HOST=0.0.0.0 the public note read "treats the Ollama server
at http://127.0.0.1:11434 as public, not private: it counts only
localhost, 127.0.0.1 and ::1 as this computer", which contradicts
itself, and the status line said "Public · Ollama running" with no host.
The card handed every sentence the renderer's probe address, which turns
0.0.0.0 and localhost into 127.0.0.1 for the CSP. #393's card did the same.

- Every status line and the public note name the server the user
  configured (base_url without its trailing slash). Probes still go to
  the renderer address.
- A public server is always named in the status line. Only the default
  server on this computer stays unnamed.
- The note's words, role and test id are unchanged. Privacy UI: on the
  PR's human-review list.

Tests: two failing-then-passing cases (0.0.0.0, running and not
running) and one that the default server is not named.
… defaults, in GB

The page cited v1.92.1 for its defaults but named Qwen3.8 27B and
qwen3.8:27b as plain defaults, which 1.92.1 does not pick. The Llama Server
and Ollama defaults, the earlier catalog and the context window sizes now
say 'after v1.92.1' / 'in v1.92.1 and earlier', as the Versa API Azure row
already did, and memory is written in GB as the app writes it.

landing/scripts/provider-defaults.test.mjs reads each provider's default
from the published release's tag and from the tree, and fails when the page
states a changed default without the qualifier.
@Broccolito
Broccolito merged commit 2a795b5 into main Oct 12, 2026
18 checks passed
@Broccolito
Broccolito deleted the claude/crew-landing-redesign-91b46e branch October 12, 2026 19:03
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