Repository navigation
Codex simplicity redesign of the desktop app and the website (R-01 to R-34) - #394
Merged
Merged
Conversation
…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.
…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.
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.
Branch
claude/crew-landing-redesign-91b46eintomain. Ata7d069304it is 879 commits on base30e1f0622, 1,358 files changed (160,308 insertions, 32,550 deletions). Every commit names therequirement 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 andhandoff.
By requirement:
stays on the active row rail, the focus edge, Send, the heatmap, status glyphs and lock badges.
shortened.
InfoTips, reachable by keyboard and namedfor screen readers.
wordmark), five sizes (12, 13, 14, 17, 24) in two visible weights, a 14px body, pinned by
styles/typeScale.test.ts.measured in Chromium.
them by Last activity, Created or Name, remembered per user.
menu (open in a new tab or window, diverge, export, copy ID, delete).
760px conversation where it fits, and becomes a 280px popover where it does not.
move with shared motion tokens, nothing animates during a window resize, and reduced motion is
respected.
with no loading flash for a tab already seen.
coral heatmap.
redesigned, not only Home.
monospace only for paths and code. The card is white with a hairline and the coral focus edge
(R-32).
regressions.
language: one page band, flat rows, filled controls.
look.
applies.
(
ModalShell, pinned at the top with a header rule, as in Crew) across the app.each have a glyph; a private chat adds a lock badge, so privacy is a shape, not only a colour.
utils/toolGlyph.ts) maps each built-in tool to arepresentative glyph; every external extension draws the generic extension glyph, and the
wrench is the last fallback.
landing/follows the same fills, type and motion, keeps all of its content,and keeps Join Slack in the header.
Verification.
fixed place 15px above the Tools chip whatever the list length or window height.
components/ui/slider.tsx, withrole="slider"and arrow, Home and End keys.--background-fillinstead of a 1px outline; lines stay only where they are structural and on floating surfaces.
3.1 s); the list now draws only the rows near the viewport.
deleted.
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.
tracked tree except NOTICE. A repo guard in
just check-everythingand inrust.yml'sguardsjob keeps it so. On Windows the app folder moves to
%APPDATA%\UCSF\biorouter.ground with its earlier hairline and the same coral focus edge, not a grey fill (owner message 17).
and yields room to staged sources and the paste box down to a 240px floor (owner message 17).
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 knownartifactCdnAssets.browserteardown hang): 1,006 testfiles, 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.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-fastwithBIOROUTER_DISABLE_KEYRING=true, asrust.ymlsets it: every target passed. Without the variable,tests/schedule_write_reach.rsfails its own precondition check; the file is unchanged frommain.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.
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 asent message moved the chat at once.
Integration with #391, #392 and #393
The three non-UI PRs merged into
mainfirst (#392 built apps as6eb1bcec0, #391 bug reporting asd2ab66f4f, #393 local inference ascf554fb5d), each green on every check, andmainstayed greenafter each. #390 was closed as superseded by this PR. This branch then took
mainin two merge commits,bf904e2e4(#391 and #392, 17 conflicts) andca7c64691(#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).routes/apps.rs' activity tests mint Fix built-app browser launch, chat links, exports and runtime isolation #392's per-page token the wayserve_indexdoes; export goes only through Fix built-app browser launch, chat links, exports and runtime isolation #392's IPC (window.electron.exportApp), with all three listsalways sent; one local-model memory module (Fix local inference prompt truncation, tool calls, and memory recovery #393's states, the redesign's GB wording); the inventory's delete
confirmation keeps Fix local inference prompt truncation, tool calls, and memory recovery #393's fix that names the right model; the composer's context window uses Fix local inference prompt truncation, tool calls, and memory recovery #393's
liveContextWindowinside the redesign's newest-request guard;landing/docs.htmlregenerated./bugcommand chip (composer, sent message, edit card, queue, mentionrow) and Report a problem's
/bughint; Fix built-app browser launch, chat links, exports and runtime isolation #392's Built apps rows, export dialog (lint phase included) and itsfailure surfaces; Fix local inference prompt truncation, tool calls, and memory recovery #393's Llama Server and Ollama cards, memory warnings, inventory and fit labels.
and Fix built-app browser launch, chat links, exports and runtime isolation #392 and no model runs for Fix local inference prompt truncation, tool calls, and memory recovery #393's screens: 15 of 18 Bug reporting: private chats, a docs and source check with a suspected cause, and /bug #391 features pass (the other three cannot be reached
in a sandbox without filing a real issue); 12 of 16 Fix built-app browser launch, chat links, exports and runtime isolation #392 features passed at first and the 4 failures (export
dialog not closing, its lint phase, the build-chat-deleted note, clipped list numbers in tool output) were
fixed and passed a live recheck; every Fix local inference prompt truncation, tool calls, and memory recovery #393 screen passes. Gates after each merge were green (1,062 test files
and 14,626 tests after the second, the full Rust suites and every guard).
address instead of the configured one (
cb527b765, see item 9 below), and the website's providers pageclaimed v1.92.1 already picks Fix local inference prompt truncation, tool calls, and memory recovery #393's local defaults (
3c0cb4227).Needs human review
MAX_SIDEBAR_SESSION_LIMITwent from 50 to2,000 (
crates/biorouter-server/src/routes/session.rs:53).aceb5d6e5: the sidebar's keyset listing walksidx_sessions_updated, and one page may carrythe whole list.
0403b8d63: the full list behind History andbiorouter session listreplaces itsJOIN messages ... GROUP BYwith anEXISTSprobe and a correlatedCOUNT(*).18de68b57: a tiedupdated_atbreaks byid ASC, as the sidebar does.6a4566b46:GET /sessions?limit=(History's first paint) reads one keyset page through thesame
keyset_listing_scopeas the sidebar; withoutlimitthe answer is byte identical. Theprivacy census counts are unchanged; only their descriptions name the shared scope.
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.
(
crates/biorouter-server/src/routes/agent.rs,resume_agent): whenload_model_and_extensionsis true the answer carries the metadata session and skips thesecond full read of the conversation, so opening a very large chat no longer sends its
transcript twice. The
falseresponse is byte for byte unchanged. Both gates(
session_reach,refuse_subagent_unless_user) are untouched, and the restore paths readmetadata only. A route test pins both bodies behind the same refusals, and a renderer test
pins that nothing reads
session.conversationfrom the second response. A performancechange; no privacy logic.
6bb7daced):privacy/HostManagedModelPanel.tsxand
privacy/CrossAffiliationAcceptCard.tsxswapped an outlined box for a fill and moved tothe shared type roles. No copy, props, logic or test id changed.
8f132acbb, first run FR-R1-14):privacy/FirstRunPrivacyNotice.tsx, the day-one notice on an upgraded install, renders throughModalShell(size md, top anchor, header rule, scrolling body, footer row) instead of a rawDialogContent, and its prose takestext-body. Copy, props, counting, the localStorageacknowledgement and every test id are unchanged; the one difference is that the
first-run-privacy-noticeid now sits on the body inside the shell, becauseModalShellcarries no test id. The count sentence is still the dialog's description.
ef7aa092e(first run FR-S-14) movesthe 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 thanthe browser's
bolder700. Nothing moves on screen (Arial draws 600 as bold); no copy, props,counting, test id or logic changed.
font-semiboldchanges 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) andprivacy/CrossAffiliationAcceptCard.tsx(the word "strict"). Report a problem (
ui/Diagnostics.tsx, not frozen) got the same for its twowarning leads. Before and after renders are byte-identical PNGs; no copy, props, logic or test id
changed.
51d145c72, vision round 6 V3-R6-2): where focus lands whenthe shared typed confirmation
ui/DangerousConfirmDialog.tsxopens. It reaches every privacysurface built on it: Knowledge's "Make public…" (
KbTierControl),DeclassifySessionDialogand 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
KbTierControllogic.docs/security/privacy-tiers.md§12.3 now says so (it said Cancel always holds initial focus).the one flow that marks a private chat public.
849d49e3a: the shared typed confirmationui/DangerousConfirmDialog.tsxis aModalShellsizesm(top anchor, header rule, 16pxinsets; the box is 400 wide, was 480), so Knowledge's "Make public…",
DeclassifySessionDialog'styped 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 ofDeclassifySessionDialog("Markingthis chat public…") is a
ModalShelltoo, instead of a centred rawDialogContent, so theflow 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).privacy/NonPrivateModelDisclosure.tsx, the DR-16 disclosurea person must answer the first time a chat runs on a model that is not private, is a
ModalShell(sizelg,purpose="required",testId="non-private-model-disclosure",scrolling body, top anchor, header rule) instead of a raw
DialogContent. It is the onedialog that cannot be dismissed (DR-17 requirement 3), so that property now comes from
purpose="required"rather than fromdismissible={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
purposestops beingrequired); focus lands on the prose, not on "Iunderstand"; and the dialog keeps its test id on the
role="dialog"node. Copy, props, theacknowledgement error banner and the
busydisabling are unchanged (96820ea11). Oneplacement: 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'sp-6,gap-4andsm:max-w-lgdefaults.privacy/HostManagedModelPanel.tsx, the first-run panel a browser user sees onbiorouter serve, drawsbiorouter configurewith the sharedCopyFieldinstead of a bare<pre>. Thecommand 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.
settings/privacy/PrivacyPanel.tsxseeds thePrivacy 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 controlthe 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). Theone exposure: if the daemon's value changed after the cache loaded, the panel shows the cached
value for one
/configread (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 thesource, the app's shared snapshot rather than a new store of the panel's own.
label or placeholder and the focus edge, and forced colours still draw borders
(
styles/fillTokens.browser.test.ts). The trade-off is recorded indesign.mdPart 9.bind_provider_if_allowednow writesupdated_atthrough aCASE: it stamps only outside anopening scope, and only when the provider or
model_namediffers from what the row records. ItsWHERE id = ? AND (privacy_tier = 'public' OR ? = 1)is unchanged. The scope lives incrates/biorouter/src/session/activity.rs; a census test counts every door that enters it.move from the legacy author folder to
%APPDATA%\UCSF\biorouter(cache%LOCALAPPDATA%\UCSF\biorouter), and the paths stored inside are rewritten. Only the daemonmoves it, once, and never while another Biorouter process uses the old folder
(
crates/biorouter-mcp/src/app_folder/,ui/desktop/src/utils/appFolder.ts; commitseed608d6a,2afec801a,4830531f7,bce54c000). No CI job exercises the real Windowslayout. Windows tester checklist:
(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 starts from its new path.
biorouter session,then launch the app. The daemon log says the folder is in use, both use the legacy folder, and
no
UCSF\biorouterappears. Quit the session and relaunch: the folder moves.biorouter.exein%LOCALAPPDATA%\Biorouter\binresolvesthe 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.
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".subst%APPDATA%, the move and thepath rewrite work and the Crew vault still unlocks.
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.
9bdb97464,owner decision, ruling 19). NOTICE keeps the attribution verbatim, and the repo guard allows
only NOTICE.
2ba556332,baee980fa,d0bd8fad1,2ec504f4c). Rows outside the drawn range fold into placeholders of their exact height. Thecurrent 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.
cb527b765, ruling 30): withOLLAMA_HOST=0.0.0.0thepublic note named the probe address
127.0.0.1and 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.
main; merging publishes the website (not an app release).🤖 Generated with Claude Code