Skip to content

Fix desktop settings, local diagnostics, and built app validation - #396

Merged
Broccolito merged 26 commits into
mainfrom
claude/final-fixes-2026-10-12
Oct 13, 2026
Merged

Broccolito merged 26 commits into
mainfrom
claude/final-fixes-2026-10-12

Conversation

@Broccolito

@Broccolito Broccolito commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Settings deep links could leave the first rows hidden beneath the top bar, extension failure toasts could cover the page's error actions, and the diagnostics report showed broken meditation runs and local models printing tool calls without running them. Exported apps also started a second service and could be reported successful despite a broken page. This PR fixes those paths, moves Crew into Settings > Chat > Capabilities, reuses the existing service where supported, and checks app artifacts by default.

Desktop behavior

  • Settings and provider setup have 24px of headroom. The ScrollArea viewport owns its positioned content, its outer root stays at zero, and the fade disappears at the top after section links and wheel scrolling.
  • While the Extensions page's failure notice is visible, it replaces every duplicate failure toast, including distinct failures from other chats. Notice actions also clear adjacent toasts.
  • Development launches show Biorouter's package version in Settings, About and update checks.
  • Tools popover switches have accessible names and each row is announced once.
  • Crew appears with the other capabilities. It is absent from the Extensions page and Tools popover, and existing on/off choices persist. The self-test workflow now checks its placement and persistence.

Diagnostics 20261006_2

  • Daily Meditation upgrades every previously shipped meditation workflow. The old soul-writer requirement resolves to update-soul, and missing-skill errors explain the remedy.
  • A scheduled run that fails before its first message leaves no empty chat. An aborted turn is a failure and does not advance the meditation cursor.
  • Tool calls written as JSON text are recovered when unambiguous, otherwise the user is asked or told that nothing ran. A real Qwen reply with invalid string escapes is detected for the notice without repairing or executing its arguments. The notice retains its user-only audience in live streaming and stored history.
  • An Ollama idle timeout names OLLAMA_TIMEOUT (or a custom provider's timeout setting) and is sent once, including when response headers arrive before the body stalls. Provider and agent fixtures replay the reporter's replies; a raw TCP regression covers the body stall.

Built apps and #386

  • An app's agent.extensions entry resolves an enabled installed extension using the running machine's configuration. Disabled, platform and frontend entries are not loaded. The manifest reference is documented.
  • Exported shell and Node launchers prefer the verified service for the same configuration profile on macOS/Linux, using its existing private socket protocol. Explicit service configuration keeps precedence; standalone fallback remains when no usable service is available. A reused service stays running on launcher exit. Windows retains explicit configuration and standalone fallback because this build has no secure shared-service discovery transport.
  • A default browser-free page check runs for build, launch, serving and export. It checks the entry, declared local script/style files and JavaScript structure without Node, browser, network or app-code execution. Proven defects produce actionable errors and block success; static passes explicitly leave runtime behavior unverified. Existing lint warnings and the person-only export lint override remain distinct.
  • A regression uses the reporter's exact fallback-bundler input. The other fixed items were delivered by Fix built-app browser launch, chat links, exports and runtime isolation #392; the closing verdict on Biorouter apps (Agent Drafter): five issues found while building and running an app #386 records the implemented follow-ups and remaining platform/runtime limits.

Installed extension behavior

This expands what a built app may arm to include extensions the person installed and enabled on the running machine. It uses that machine's command and credentials; the app does not supply them or override a disabled switch. Tool dispatch remains subject to the existing permission and privacy gates.

Validation

Native Electron validation passed 60 Settings deep-link/headroom cases, 54 real-window geometry cases, 8 first-run/provider setup cases and 4 Crew placement/persistence cases, across light/dark themes and actual OS window sizes. Native fullscreen and preview-driven window growth also passed. No viewport or device emulation was used. Brief 51–82ms sidebar commit costs remain measured on the development artifact with 10,854 seeded chats; the audit found no resize-triggered full-history sort/load.

On a fresh daemon, an HTTP response whose body stalled after headers produced actionable OLLAMA_TIMEOUT guidance with exactly one request. The captured malformed Qwen call produced exactly one user-only notice in both streaming and persisted history, with no tool execution or corrective retry.

Reporter-model replays used an isolated Ollama port and synthetic data. Qwen's bounded narrow replay recovered and executed a valid text call, but did not complete the clinical extraction task. Llama used native /api/chat, num_ctx=131072, truncate:false, and the full prompt; it returned prose and call examples without executing tools. Its scheduled Meditation run migrated the legacy workflow, created a nonempty chat, cleared the previous error, and advanced the scheduler cursor, but did not ingest the soul. These model completion limits remain; ordinary prose is deliberately excluded from tool-call recovery.

The focused app-authoring suite passed 386 tests (0 failed, 2 ignored), including per-operation service identity, exactly-once payload imports, page rejection and preservation of the previous bundle. The complete just check-everything gate passed again on clean head cbb6600f8574106a246836d89fa0209350163e4f, including Clippy, UI lint, OpenAPI regeneration and every consistency/registry guard. A reproduced UI fixture race was corrected by waiting for the visible dialog overlay before clicking outside; all six focused cases and three additional runs pass with the original assertions and timeouts. Independent source and live-evidence review is clear. The complete UI suite passed with exit 0: 1,069 files, 14,735 tests passed and 13 skipped; formatting also passed. All four local library suites passed: core 5,059 (3 ignored), MCP 1,849 (6 ignored), server 998 (1 ignored), and CLI 789. Diagnostics integration (11), privacy wiring (3), and self-test isolation (5) also passed. Every suite exited zero. All normal CI workflows are green on the exact head: Rust on Ubuntu/macOS/Windows, Frontend, Apps smoke, and commit-message check.

Fresh final-head daemon checks passed 57/57 assertions with 46 saved HTTP/MCP responses, including hard-defect refusal despite a lint override, staged-output preservation and repair. Actual macOS exported launchers passed 30/30 service reuse and scoped-page assertions; full payload imports are covered by both generated launcher lifecycle fixtures. A bounded run of the tracked apps workflow exercised seven actual MCP subcalls through a deterministic provider fixture. Its separate browser-smoke phase was unavailable and is not claimed passed.

The previously deferred CLI error-turn exit status and the fixed prompt size on 32k-window models remain outside this change. No release is published.

…n, not Electron's, in a dev launch

A launch that hands Electron .vite/build/main.js directly loads no package.json, so
app.getVersion() answered Electron's own version (39.8.10): the Version row showed it,
and the GitHub fallback updater compared release tags against it, so a dev launch was
never offered an update. utils/biorouterVersion.ts replaces that one answer with the
package.json version Vite inlines at build time; main.ts adopts it into Electron at
startup (so electron-updater's currentVersion agrees) and answers get-app-version, the
About panel, the Help menu label, the daemon version match, the dependency report and
both updater paths through it. Packaged builds keep what app.getVersion() reports.
…by their rows and each row is still announced once

The per-chat switches in the Extensions, Skills and Knowledge lists had no
accessible name, unlike every switch on the Extensions page. Each switch is
now named by the row's visible name through aria-labelledby (the base's
name, not the knowledge list's state-saying label). The wrapper keeps it out
of the accessibility tree; the switch no longer carries its own aria-hidden,
which would leave it with no name at all.

Naming it exposed a Chromium quirk: a name built from an option's content
read the hidden switch's name in again (a focusable button under
aria-hidden, and the switch's ::after), so every row said its name twice.
jsdom and Playwright's aria snapshot did not show it; Chromium's own
accessibility tree did. The option is now named by reference to its title
and note (or by its label), so nothing inside it reaches its name. The row
click still goes through the switch.

Tests query each switch by role and name in all three lists, and a Chromium
test reads the real accessibility tree for one node per row.
…chine's own settings, not as a built-in server (#386)

An app's agent.extensions names an extension and nothing else. Every name
that was not a platform extension was armed as one of Biorouter's own
servers, so an installed stdio extension the drafter's catalog offered
(ucsfomopagent) failed on every connect with "Unknown builtin extension".
A name that is not built in now resolves against the extensions configured
on the machine running the app, with that machine's command and
credentials, so the manifest stays portable. An entry the person turned
off, a platform or frontend entry, and a name nothing is installed under
are not armed, and the daemon log says which. The SDK reference says how
to declare one.
The literal { omop_rules: OMOP_RULES, required_outputs: Object.keys(patient) }
keeps its values and builds, and the same literal across lines and the
local modules ./prevent and ./preventData are refused by line with nothing
written. The behaviour landed in a423670; this holds it to the reported
input rather than to a look-alike.
…he Extensions page's load-failure actions

At 1440 the sticky '1 of 2 extensions loaded' report (1144 to 1424) covered the
right 32px of the load-failure notice's Copy error and all of its dismiss mark.
The stack has nowhere to go there that covers nothing (under the notice is the
list's first switch, beside the actions is the error), so the page's actions step
aside instead: a group marked data-toast-aside moves left while a toast stands
beside it, out from under it and 16px clear, never closer than 8px to the title
before it, as a translate (main.css 'toast clears the corner'), so nothing else
moves or rewraps. toastClearance measures it against the stack's target box,
gives it back when the toast goes or the watcher stops, follows scrolls, and
never moves it from under a resting pointer. The notice lays its title, actions
and error out in its own grid (the Note's slot cannot see the title), keeping
the error wrapped before the actions and the note 60px tall. Tests: a Chromium
case with the real notice in the real reading column, the real report and
container (fails before: Copy error and the X under the toast), hover hold, 1048;
unit cases for the step, its cap and the watcher; source pins.
…of the failure toast

The live check of a671173 found the rest of the same defect: once the
load-failure notice was dismissed, the sticky '1 of 2 extensions loaded'
stood over the list's first switch, at 1440 and at 1048 wide. The page
already says what the toast says, with the error and its actions, so while
the notice is mounted it is the announcement: a failure recorded then is
shown there and counted announced, no toast is raised for it, and a failure
toast already standing (the grouped report or the single one) is taken down
when the notice shows a failure. Progress and clean-run reports are left
alone. Leaving the page does not re-announce a failure it showed.

Tests: the reporting rules with a notice on screen and after it closes, the
withdrawal of each failure toast (and only those), and the notice's presence
and withdrawal on mount and on a failure recorded while it is open.
… an extension

Crew ships with Biorouter (PLATFORM_EXTENSIONS["crew"]) and the daemon already
classified it as a capability, but the desktop's CAPABILITIES list left it out,
so it still showed on the Extensions page, in its count, in the Tools popover
and in the load toast's denominator. It is now a capability row in
Settings > Chat > Capabilities, labelled Crew, with the same switch: the row
writes the same crew entry in config.yaml the Extensions page did, so a saved
on or off carries over with no migration. A chat connected with /crew and a
Crew task still get the tools from their grant, and only Revoke ends access.

- capabilities.ts: add the crew entry (default on) and its InfoTip.
- Tools popover: drop the Crew row lock (crewLocked, lockedByCrew,
  accessCopy.extensionLocked, isCrewExtensionName), dead once the row is gone.
- /ext: palette: Crew is listed as a capability; its row reads
  "Crew tools (advanced)".
- Rust: display name "Crew" for /ext: messages, crew in the capability name
  table and defaults test, and a crew_is_a_capability test.
- CLI status line: count extensions with ExtensionConfig::is_capability instead
  of a stale six-key list that counted Crew and seven other capabilities.
- Docs: Crew listed with the capabilities; landing/docs.html regenerated.
…uter shipped (diagnostics 20261006_2)

The stock-copy list held two hashes that match no Meditation text ever
committed, so startup never upgraded an install. A v1.85.2 copy kept
requiring soul-writer, the skill v1.85.3 renamed to update-soul, and since
v1.90.0 made workflow skills required the job failed every night.

The list now holds the hash of every text that shipped, and a test reads each
text back from tests/fixtures/meditation/ and hashes it. A pin on the current
text's hash fails when the text changes without its outgoing hash being
recorded. The reporter's exact bytes upgrade in both the library and the
schedule's copy, with the job's bookkeeping kept.
…issing skill says what to do (diagnostics 20261006_2)

A Meditation copy the person edited is not upgraded at startup, and named
soul-writer, a skill startup deletes and nothing can install. The old name
now resolves to the skill it became, unless a skill or bundle by that name is
installed. A workflow skill that really is missing is refused with the next
step: add it under Skills, or remove it from the workflow's skills.
…mpty chat (diagnostics 20261006_2)

The required-skill check runs after a scheduled run makes its chat, so each
night's Daily Meditation failure left one empty 'Scheduled job' row in the
store, hidden from History. Setup after the chat is made (the bind, the
workflow's skills and its own setup) now deletes that chat when it fails, and
the run's error still stands.
…Meditation's cursor stays put (diagnostics 20261006_2)

The agent ends a turn that did not do its work (the model call failed, the
loop guard stopped it) with a 'Ran into this error' message and a TurnAborted
event, and the stream then ends normally. The scheduler ignored the event and
recorded a success: last_error cleared and last_run, Daily Meditation's Chat
Recall cursor, moved past a day nobody digested. Found re-running the
reporter's Daily Meditation once its skill check passed, against a local model
that was not running. TurnAborted now fails the run with its code and message.
…or says nothing ran (diagnostics 20261006_2)

The reporter's llama3.1:8b, in a chat with Code Execution on, answered with
calls written as JSON, and the person read the JSON as the answer with no word
that nothing had happened.

- Two call objects in one reply (reply 11: workspace_read_conversation then
  workspace_read_panel) were neither a call nor an attempt, so the turn ended
  on the JSON. attempted_calls now reads the shapes recovery reads, a run or
  an array of objects, and Ollama's one corrective retry names every tool.
- A reply that is nothing but calls nothing can run (reply 1 text_editor,
  reply 7 shell, reply 15 agent_drafter: tools that chat did not offer) gets
  one line after it, for the person only, saying nothing ran and what may
  help. The agent adds it, so it covers every provider; the model never sees
  it, and the call is never run from text.

The reporter's exact replies are fixtures, and a unit test classifies each:
nothing runs from text, 9 and 11 are asked about, 1, 7 and 15 are noticed,
and the answers (3, 5, 13, 17, 19) are left alone.
…is sent once (diagnostics 20261006_2)

Ollama sends nothing while it reads a prompt. On the reporter's Intel Mac
(about 25 prompt tokens a second) the whole first prompt of about 37,000
tokens outlasts the 600-second bound, and the person read 'Request failed:
error sending request for url (.../api/chat) (timeout)'. The provider already
refuses to re-send a timeout, since the server is still reading the first
copy, but the agent classed it as a network blip and sent the whole prompt
again: twenty minutes for a sentence that names neither the bound nor the
prompt.

A timeout before the reply begins is now ProviderError::IdleTimeout, a
sentence naming the seconds waited, the prompt's rough size in tokens and the
setting to raise (OLLAMA_TIMEOUT, or a custom provider's timeout_seconds). It
is classed as network for exit codes and wire frames, and the agent never
retries it.
…the agent

The reporter's exact replies, served from a mock Ollama with the usage the
bundle measured, through the real provider: the cut first turn (164,005
bytes answered from 2,046 + 4 prompt tokens) is refused and the request asks
for the window it needs; qwen2.5-coder at its 32,768 trained window refuses the
reporter's prompt size; replies 9 and 11 are asked about once, naming the real
tools; replies 1, 7 and 15 in the Code Execution chat are shown and never run;
reply 7 with Developer offered is asked about; the answers are left alone; a
first prompt slower than the idle bound names the setting and is sent once.
Through the agent, reply 7 gets the line saying nothing ran, stored for the
person only and kept out of the model's next request.
…ed about, a first-byte timeout names its setting (diagnostics 20261006_2)
…nder the band, with 24px of headroom [R-17 R-34]

Opening Settings at a section (the update notice's "update", "usage",
"project", even "privacy") left the first lines under the 44px band after
the wheel reached the top, on every tab, until Settings was reopened.

The ScrollArea viewport was not positioned, so absolutely positioned
content without a positioned box of its own (every InfoTip's sr-only
description) was placed against the overflow:hidden ROOT, past the
viewport's clip, and gave the root up to ~1,600px of overflow. A wheel
cannot scroll a hidden box; scrollIntoView can, so the deep link scrolled
the root as well, and nothing scrolled it back.

- main.css: `.br-scroll-viewport { position: relative }`, so that content
  belongs to the viewport and the root has nothing to scroll.
- ui/scroll-area.tsx: the root is put back at 0 whenever anything scrolls
  it; `data-scrolled` (the top fade's hook) counts a whole pixel, so a
  sub-pixel rest at the top keeps no fade.
- `--page-headroom: 24px` and `.br-page-headroom`: one clear space under the
  band for Settings (was py-5, 20px), the first-run setup page and the
  provider page (were py-6), taller than the 10px fade.

Tests: settings/pageHeadroom.browser.test.tsx mounts the real SettingsView
and ProviderGuard first-run page in Chromium under main.css compiled by
Tailwind, wheels/flings back to the top on every tab and after deep links
at 1048x600 and 1440x900, and asserts scrollTop 0 (viewport and root),
data-scrolled false, no mask, and the first row >= 24px under the band.
Its instrument removes both halves of the fix and sees the defect return
(each half alone holds). Run against HEAD's code it fails 9/9 (20px
headroom; root scrolled 174/411px on landing). Unit tests for the root
guard and the threshold; settingsScrollFade.test.ts pins the token.
…nly notice streams as user-only (diagnostics 20261006_2)

categorize_tool_requests rebuilt the message the client is shown live with
default metadata. The notice that nothing ran was stored for the person only
but streamed as visible to the model, so a client holding the live copy held
the wrong one. The rebuilt message now carries the reply's own metadata; the
agent replay of diagnostics 20261006_2 asserts the live notice is user-only.
@Broccolito Broccolito changed the title Small UI fixes from the final check, and the last item of #386 Fix desktop settings, local diagnostics, and built app validation Oct 13, 2026
@Broccolito
Broccolito merged commit 989dc74 into main Oct 13, 2026
18 checks passed
@Broccolito
Broccolito deleted the claude/final-fixes-2026-10-12 branch October 13, 2026 01:47
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