Skip to content

Release 107 feedback: clean desktop chat previews - #84

Open
alichherawalla wants to merge 416 commits into
mainfrom
release/107-feedback
Open

Release 107 feedback: clean desktop chat previews#84
alichherawalla wants to merge 416 commits into
mainfrom
release/107-feedback

Conversation

@alichherawalla

@alichherawalla alichherawalla commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • remove model protocol text from desktop conversation previews
  • add rendered regression coverage

Verification

  • focused desktop preview tests pass

Summary by CodeRabbit

  • New Features

    • Added Activity and Files shortcuts under Devices navigation.
    • Added transcription settings with language selection and improved voice-language support.
    • Transcription status now displays active and supported languages.
    • Added consistent, accessible side panels across model, settings, skills, and artifact views.
    • Transferred transcription models can now be registered and activated correctly.
    • Connector providers can supply verified tools for integrations.
  • Bug Fixes

    • Enhanced-prompt metadata now appears as clean prompt text in conversation lists.
    • Internal <think> and __LABEL: markers are hidden.
    • Unsafe links are blocked, while validated external links open securely.
    • Transcription language preferences now apply correctly with compatible fallbacks.
    • Malformed navigation URLs now fall back safely.

siddWednesday and others added 16 commits August 19, 2026 15:03
The desktop MCP server exposes the on-device ACTION tools (calendar,
reminders, contacts, messages_send, mail_send, open_url, web_task,
computer_task) so a paired phone can list + run them on this Mac. But the
gateway binds 0.0.0.0 (LAN-wide), so unauthenticated any device on the
network could trigger a mail_send or a computer-use task. The model/
inference tools are safe to leave open; the action tools are not.

Gate them: buildMcpServer(actionsAllowed) registers the action tools ONLY
when the request carries the desktop's action token, so an unpaired device
neither sees them in tools/list nor can call them. Model tools stay open.

- mcp-auth-logic.ts: constant-time Bearer check (timingSafeEqual), fail
  closed on blank/short/wrong/malformed. Electron-free, unit tested.
- mcp-auth.ts: token store (32 random bytes, persisted 0600 in userData,
  generated on first use) + isActionAuthorized(req); dev-only startup log
  so a device can be paired for testing (never logged in a packaged build).
- mcp-server.ts: handleMcpRequest builds the per-request server from
  isActionAuthorized(req); action tools register only in that branch.
- Source-level regression guard for the gate (buildMcpServer can't run in a
  unit test - it imports electron/native modules), plus the auth unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…airing

The desktop needs to hand a phone the details to reach its MCP action tools -
via a scannable QR or a shown code (both interchangeable). These are the pure,
tested pieces:

- lan-address.ts: pick a LAN IPv4 the phone can actually reach (skip loopback/
  internal/link-local, prefer 192.168 > 10 > 172.16-31). The gateway binds
  0.0.0.0; this just chooses what to advertise.
- pairing-payload.ts: the versioned pairing payload
  { t:'offgrid-mcp-pair', v:1, url, token, name? } + JSON encode. Same data the
  manual URL+token flow uses; the OGAM parser mirrors this contract.

IPC + the desktop "Pair a device" panel (QR + show-code) build on these next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A core Settings section that shows a phone how to run this Mac's MCP action
tools - scan the QR (fills in URL + token, nothing to type) or expand "Enter it
by hand" for the URL + Authorization header to copy. Same data either way, so
QR and code stay interchangeable.

- ipc `pairing:info`: composes lan-address + pairing-payload + the live gateway
  port + action token + host name into { mcpUrl, token, deviceName, lanIps, qr }.
  The token is the desktop's own secret shown on its own screen.
- preload: `pairing.info()` namespace.
- PairDevicePanel: QR (qrcode.react) + show-code fallback + a "no local network"
  guard; self-explanatory copy.
- Shared ui/CopyButton (phosphor), and migrated GatewayScreen's local copy button
  to it (DRY; drops its tabler copy icons).

Renderer needs a main restart to pick up the new IPC; verified via tsc + the
lan-address/pairing-payload unit tests. Visual/QR-scan check lands with the
mobile scanner (phase D).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mesh flow supersedes it: you pair once on the Devices tab and toggle
"Allow tools" per device, instead of a separate Settings panel that showed a
QR for the phone to scan. Removes PairDevicePanel, the pairing:info IPC handler
+ preload namespace, and the now-unused qrcode.react dep. The pairing-payload /
lan-address helpers stay - desktop-pro's mesh grant sender still imports them.
Dropped with the standalone Pair-a-device panel; the Devices screen now renders
the mesh pairing code as a QR (desktop-pro), so the dep returns for that use.
A small, curated set of starter prompts grouped by the capability each shows off
(browse the web, drive your Mac, remembers what you've seen, tools from your
phone). Tapping a preset will seed a real chat with its prompt so the agent asks
its own follow-ups - the demo and the "prompting is a conversation" lesson in one.
Data only, no pro logic; presets that need pro / a paired phone / capture history
are tagged via `requires` so the surface gates them. Invariants covered by tests.
Renders the preset catalog as capability-grouped cards (browse / drive your Mac /
remembers / phone), each with an icon, a blurb, and a plain requirement label when
it's gated so a card never dead-ends silently. Clicking a card calls onRun(preset)
so the host can seed a chat with the prompt. Placement-agnostic and brand-dense
(mono, emerald accent, Phosphor icons). Optional "request a capability" link points
at a form. Covered by a render/click test.
Wire ExploreSection into MemoryChat's empty state (non-image chat mode) so a
brand-new chat leads with the capability-grouped starter prompts. Tapping a
preset fires sendMessage(preset.prompt) directly - the prompt stays hidden and
the agent takes over asking its own follow-ups, teaching that prompting is a
conversation.

Adds REQUEST_FORM_URL (undefined for now) as the single place to point the
"Request a capability" link once the Google Form exists; until then the surface
hides the link rather than shipping a broken one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the demo presets discoverable to everyone, not just on a fresh chat's
empty state: a top-level "Explore" nav item opens ExploreScreen, which reuses
the same ExploreSection verbatim (one component, two placements).

Add a seed-prompt handoff so any surface can open a fresh chat that auto-sends
a preset: openTarget.seedPrompt resets to a new conversation and a small effect
below sendMessage fires it once the reset has settled, so the prompt lands in
the new empty chat. App's handleRunPreset drives it from the Explore screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Match the brand name shown in the sidebar and used across the app, per the
strict brand-copy rule, rather than the bare "Off Grid" shorthand.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A prompt like "best-reviewed places" failed with "Sorry, something went wrong":
the rag:chat retrieval built its FTS5 MATCH by joining tokenizeQuery's tokens
bare (`tokens.join(' OR ')`), and tokenizeQuery keeps hyphens - so `best-reviewed`
reached MATCH as invalid syntax and threw `SqliteError: no such column: reviewed`,
failing every FTS query in the turn (memory/message/summary/entity/fact). Common
words trip it too: e-commerce, back-end, co-founder.

The repo already had the safe pattern (search-ranking.ts `ftsExpr` quotes each
term so punctuation can't be a syntax error); rag:chat just didn't use it. Add
`ftsMatchExpression` next to tokenizeQuery in the pure, testable ipc-query-logic
module: quote each token as an FTS5 phrase literal and OR-join (preserving the
any-term recall), with a quoted-phrase fallback when nothing tokenises so that
path can't throw either. Route rag:chat through it.

Regression test runs the built expression against a REAL FTS5 table (node:sqlite):
the old bare form throws `no such column`, the fixed form matches, and hyphen /
colon / quote-heavy inputs never throw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… clause

The preset "Play something upbeat and set the volume to about 30%" read as
broken in testing: there is no system-volume tool at all, so the model honestly
refused that half; and "play something upbeat" is too vague for the local model
to route (orchestrator logged plan=[none] for the "music" follow-up), so it
disclaimed instead of acting.

The capability itself works - a concrete "play <x> on Spotify" routes straight
to computer_task and queues the native action (verified: orchestrator logged
plan=[computer_task] and queued it awaiting approval). So make the preset match
that proven shape ("Play some jazz on Spotify") and drop the unsupported volume
promise, so the "Drive your Mac" demo actually drives the app.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pro too

The inline approval card (ActionGateDock, mounted in the chat) was only fed when
no pro queue was listening - so in a pro build the gate always went to the pro
queue (a notification) and the in-chat Approve/Reject card never appeared. That
was flagged in the code as "pro queue wins until the migration".

Do the migration: gateHost now parks AND emits the inline card whenever a human
is needed (pro queue accepted it, or an inline surface is registered). The
notification and the in-chat card are two views of the ONE engine gate -
whichever the user acts on calls resolveActionGate for the same actionId
(idempotent), and the other view settles on the outcome broadcast. A
chat-initiated computer-use task is now approvable right where it was asked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add registerApprovalModeProvider / computerApprovalMode to gate-host so the user's
"Auto-approve vs Ask every time" choice governs the gate. gateHost short-circuits
to approve when the mode is 'auto' (the task still journals and its outcome shows
in chat); 'ask' (the default when no provider is registered) parks for approval as
before. Pro owns the persisted setting + the Sync-sharing toggle and registers the
provider - core just exposes the seam, so the free build has no computer-use logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d + mesh)

The inline gate surface was a single slot, so only one consumer could render a
parked gate. Mobile approvals need a second: pro forwarding the gate to paired
phones over the mesh, alongside the desktop chat card. Make it a Set of
subscribers - registerInlineGateSurface adds/removes, gateHost fans each parked
gate to all of them. Each surface resolves the ONE engine gate via
resolveActionGate; the first verdict wins, the rest are no-ops.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your trial has ended. Reactivate Greptile to resume code reviews.

@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The change updates transcription language handling, adds transcription settings, standardizes side panels, adds Devices subroutes, sanitizes Markdown links, supports provider-owned MCP tools, and updates tests and repository wiring.

Changes

Application feature updates

Layer / File(s) Summary
Transcription language and model flow
src/main/transcription/select.ts, src/main/ipc.ts, src/preload/index.ts, src/main/models-manager.ts, src/main/transcription/__tests__/select.test.ts, src/main/models/__tests__/*, src/main/__tests__/rag-empty-memory.dbtest.ts
Configured STT language is resolved against model support. Active metadata includes language choices and installed transcription entries. Downloaded model variants are checked during activation.
Settings language controls
src/renderer/src/components/SettingsPanel.tsx, src/renderer/src/lib/model-settings-panel.ts, src/renderer/src/components/__tests__/SettingsPanel.speech-language.integration.test.tsx, src/renderer/src/components/__tests__/ModelsScreen.settings-navigation.integration.test.tsx
SettingsPanel adds Kokoro voice-language selection, transcription-language persistence, failure recovery, and a transcription tab.
Shared side-panel migration
src/renderer/src/components/SidePanel.tsx, src/renderer/src/components/{ArtifactCanvas,ModelPicker,ModelsScreen,SettingsPanel,SkillsPanel}.tsx, src/renderer/src/components/__tests__/ModelPicker.dismiss.integration.test.tsx
SidePanel provides shared modal structure, dismissal behavior, accessibility, and focus management. Existing panels use the shared component.
Device navigation and link handling
src/renderer/src/App.tsx, src/renderer/src/components/CommandPalette.tsx, src/renderer/src/lib/paletteScreens.ts, src/renderer/src/components/ChatMarkdown.tsx, src/renderer/src/__tests__/App.navigation.integration.test.tsx, src/renderer/src/components/__tests__/*
Activity and Files retain Devices subroutes through palette navigation and URLs. Malformed paths fall back to root views. Markdown links open only sanitized external URLs.
Provider-owned MCP tools
src/main/bootstrap/hookRegistry.ts, src/main/mcp.ts, src/main/mcp-oauth.ts, src/shared/mcp-oauth-callback.ts, pro
Provider hooks can supply verified connector tools. Generic MCP discovery remains the fallback. OAuth uses shared callback configuration.

Estimated code review effort: 4 (Complex) | ~60 minutes

Merge Risk: 🟡 Moderate · up to f84f1

This PR changes desktop preview behavior alongside connector and navigation flows, but the current head still permits broken focus wrapping, can leave the Electron SQLite dependency unusable after a failed restoration, and can crash route parsing for malformed device URLs. Merge should wait for these bounded correctness and runtime issues to be fixed or explicitly accepted.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main objective: cleaning desktop chat previews based on Release 107 feedback.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch release/107-feedback

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

siddWednesday and others added 11 commits August 23, 2026 12:57
The Explore surface now renders each capability as a bordered panel (icon
tile + teaches line) holding a dense card grid, adapting to its container
(two panel columns on the Explore screen, one in the chat empty state).
Preset titles become short capability labels - the prompt-phrased titles
("Find me a flight to book", "What did I work on today?") no longer leak
the seeded prompt onto the card. Robust ungated presets get a ready
marker; the Explore screen gains a real page header with catalog-computed
meta and hides the section's compact intro via showIntro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Boots the free build on a fresh profile, walks the Explore screen and the
chat empty state, and asserts the capability panels render with card
labels while the seeded prompt never appears on the surface. Screenshots
captured for evidence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every preset now carries its own Phosphor icon in the catalog SSOT
(AirplaneTilt, SpotifyLogo, Crop, ...) so both placements show the same
mark; the card leads with the icon and warms it emerald on hover. Each
panel header gains a small runs-count meta. Tests cover the icon
invariant, the per-card rendering, and the panel counts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…app quits

The agent WebContentsView runs with backgroundThrottling off (so the agent can
work while the user does other things), and it was only ever hidden - bounds set
to zero - never muted or torn down. So a web_task that started a video kept its
audio playing after the pane closed, the window was hidden, or the app was
"quit" (on macOS a window close leaves the process alive).

- Mute the view's audio whenever it's hidden, unmute when shown, so closing the
  browser pane actually silences it.
- React to the window too (setRegion only fires while the pane is mounted): mute
  on window hide, fully dispose (remove + close the WebContents) on window close.
- disposeBrowserHost() on before-quit stops media immediately rather than
  whenever the process finally exits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the amber "this model can't ground" warning (VisionGrounderNudge) - it added
noise on every non-grounder computer-use run. Removed the render + its import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card sat on screen while the action ran and the outcome made its way back.
resolve() now drops the pending card immediately on approve/reject/edit; the
outcome still lands as its own row, and an edit re-gates as a fresh pending event.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ip notch

The left-edge resize handle was covered by the native WebContentsView below the
header, so it was only grabbable in a thin strip. Inset the web region (ml-4) to
give the handle a real full-height gutter, and swap the faint bar for a centered
DotsSixVertical grip so it clearly reads as draggable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The live step narration for a web task sat in a feed under the browser pane.
Move it into the chat turn: MemoryChat subscribes to browser:onStep + onTaskState
and renders the recent steps in the streaming assistant turn (a new task resets
them, and they fall away when the turn ends). WatchedBrowserPane drops its step
feed + summary and keeps just the goal, live browser view, and takeover prompt.
Computer-use task steps are unchanged (they stay in their own small modal).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card labels/blurbs are unchanged; the actual prompt each preset SENDS is now
defined - it names the site or app and states the task plainly, so it routes
cleanly and reads sensibly, e.g. find-flight now sends "Go to skyscanner.com and
help me find a flight to book. I will give you the route, dates, and budget when
you ask." Named targets: Skyscanner, Google Maps, Google Shopping, Spotify,
Preview, Mail; the memory/phone prompts point at what was captured on the Mac.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolutions:
- packages/models: accept main's de-vendoring (now file:../shared/packages/models,
  where the grounder work already landed).
- pro: adopt main's submodule model (.gitmodules); pointer bumped to the synced
  desktop-pro feature commit in a follow-up once desktop-pro is synced.
- MemoryChat.tsx: took main's rewrite, re-grafted the 12 chat-approvals surgical
  changes (ExploreSection seed handoff, ActionGateDock/VisionSupervisorOverlay,
  self-contained WebTaskStepFeed for the streaming turn); dropped the MessageNudge
  cutoff change (superseded by main's ResponseCutoffNotice).
- ipc.ts: main's superset chat-stream-state imports.
- tools.ts: widen the planning-path return to main's plural imageRequests contract.
- vitest.db.config.ts + release.yml: union of both sides' exclusions / build steps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… by the sync

- pro: bump submodule pointer to the synced desktop-pro feat/mesh-tool-grant (4b882d8).
- docs: restore the directory name (an earlier commit accidentally renamed docs/ ->
  docshi/); main carries docs/, and the coverage ledger + release checklist read docs/.
- WatchedBrowserPane: guard the browser IPC method calls (onTaskState?./onTakeover?.)
  so App integration tests (and a partial preload) don't crash on mount when the
  optional browser API isn't wired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the presence seam the floating task card needs. `useTaskWorkspaceOpen`
answers "does the user want the workspace open" and rightly survives navigation;
its other readers depend on that. "Is a docked surface painting right now" is a
different question, and only the surface can answer it, so it registers while
mounted.

A count, not a boolean: two workspaces exist (the chat panel and the Tasks
route) and they can hand over in either order.
There is one native WebContentsView for the window, and more than one HTML
surface can host it. Its position was decided by whichever surface called
set-region last, so a handover depended on an ordering React does not guarantee:
the arriving surface mounts before the departing one cleans up, so the departing
surface's release landed last and cleared the arriving one's claim.

The floating card was left painting nothing, and since its size never changed
afterwards nothing re-reported - blank until a manual resize. A cleared region
also hides the native view, and a hidden view captures nothing, so a running task
died with "the browser returned an empty screenshot".

Surfaces now report under an owner key and main paints the highest priority one
present (docked outranks floating). The two messages commute, so no ordering can
strand the page. Every PiP failure this week came from that race.
…capture failed

Two problems in the capture loop, one of which was silently killing tasks.

The viewport check demanded 1920x1200 within a flat 2px tolerance. But the CSS
viewport is bounds/zoom, so the smallest expressible change is 1 device pixel =
1/zoom CSS pixels. The floating card runs at the 0.25 zoom floor, where that is
FOUR CSS pixels: a 3px layout rounding reported 1908x1192, was rejected forever,
and every capture spun to the deadline the moment the card took over. The docked
pane sits near zoom 1, where the same rounding stayed under 2px - which is why
only the card broke. The tolerance is now a device-pixel budget converted at the
current zoom.

The loop also has four distinct exit conditions and reported them as one of two
sentences, so a failure could not be told apart - and the commonest one never
reaches capturePage at all, yet still said "empty screenshot". It now names the
blocker and the numbers behind it. That diagnostic is what found the bug above.
…control by view id

Enter never submitted. Chromium needs a virtual key code to treat a key as that
key, and the two dispatch paths disagreed: pressKey carried a three-entry map
with proper codes, while a vision `press` went through dispatchKeyPhase, which
merely capitalized whatever it was given and sent no code at all. So the model
saying "press return" produced key:'Return' with no keycode - a keystroke the
page could not act on, reported as a success. One table now serves both paths,
with the aliases the model actually uses (return, esc, up, del), and an
unrecognised name fails loudly instead of being title-cased into a key that does
not exist.

The chat control compared the view against 'chat', but the chat view's id is
'memory-chat' - the route is /chat, the view is not. The comparison never
matched, so the control showed on the very screen it exists to hide from. The id
is exported now, so a hand-written literal cannot drift from it again.
jsdom implements no scrolling, so Element.scrollIntoView is absent and calling it
throws. Real browsers always have it. Stubbed in the shared setup rather than
guarded at each call site, so product code is not shaped around a gap in the test
environment.
The desktop recorded nothing about generation performance - no rate, no token
counts, no time to first token - so there was no way to answer "why did that
take so long" except by guessing.

Three pieces. parseSseLine used to drop any chunk without choices, which is
exactly the final chunk carrying usage and timings, so the numbers were being
thrown away before anything could read them. The local request now asks for
usage. The accumulator times the stream itself, counting the first reasoning
token as the first token - for a thinking model the alternative reports the
whole thinking pass as latency.

Rates prefer the server's own measurement, which times decode alone; our wall
clock includes queueing and prefill and understates the rate on a cold prompt.
Every field is optional, and a server that reports nothing produces no numbers
rather than a row of zeros - "0 tok/s" is a lie, an absent rate is the truth.

Carried to the renderer through the same context blob that already persists
reasoning and cutoff, so no schema or IPC change. Display comes next.
Records how long the screen capture took and how long the model call took, and
carries both to the step detail.

Measured at the graph level, around the two calls, so it covers the whole cost of
each phase rather than the part any one layer can see. The capture loop in
particular retries against an unsettled page for up to six seconds without any
single call looking slow.
Renders the measured numbers - prefill rate, decode rate, time to first token,
token count, total - as one muted line under an assistant message, and persists
them in the same context blob that already carries reasoning and cutoff, so they
survive a reload.

Only the fields the run produced. A server that reports no token counts shows a
time and nothing else, because a zero rate would be a lie.

The metrics module moved from main/llm to shared: the renderer needs the same
shape it renders, and the IPC contract cannot import from main. Its inputs are
typed structurally so shared stays independent of the SSE layer.

Unlike mobile this is not behind a preference. The desktop chat has no
display-settings surface to hang a toggle on, and adding one for a single muted
line is not worth the drift - say the word and it becomes a setting.
Mobile keeps these numbers behind a switch and so should we - they are for when
you go looking, not a permanent fixture under every answer. Settings -> Model ->
Generation details.

It is a DISPLAY preference, so it rides saveSetting rather than LlmSettings:
nothing here reaches the engine, and putting it in the engine's parameter block
would fire a reconfigure on every flip. It travels to the message row through
MessageRowState, the bag that already carries the other display state, so no new
prop drilling.

The integration test found a real gap: metrics were persisted but never set on
the LIVE message, so the numbers only appeared after a reload. They are on both
now. The test drives the real SettingsPanel and the real chat, and covers the
third case too - a run that measured nothing prints nothing, never "0 tok/s".
modelInput was the serialized prompt, history included, stored per step and
re-read on every tasks:list poll. Measured on a real profile: 10 MB across 50
tasks, 1.27 MB in the worst single task, of which modelInput was 73%.

It is a debugging echo of what we just sent. Nothing needs it after the request,
and its only reader was a details block that duplicated the tool-call JSON
already shown beside it. Removed at every producer, from the stored shape, and
from the UI.

sanitizeComputerUseStepDetail now takes a LOOSE input rather than the stored
shape: it is the boundary that decides what may be persisted, so it has to be
able to receive a field and refuse it. Its test now passes modelInput carrying a
secret and asserts the whole field is dropped - the redaction guarantee holds by
removal instead of by scrubbing.
@sonarqubecloud

Copy link
Copy Markdown

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.

3 participants