feat(desktop): harness-agnostic config bridge#887
Draft
wpfleger96 wants to merge 10 commits into
Draft
Conversation
66de98f to
60f95a2
Compare
de81ed1 to
0c44d4e
Compare
44cc07f to
678b871
Compare
0c44d4e to
ba28ea0
Compare
c392c34 to
9939a10
Compare
wpfleger96
pushed a commit
that referenced
this pull request
Jun 11, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jun 11, 2026
3228e7a to
139ada2
Compare
wpfleger96
pushed a commit
that referenced
this pull request
Jun 11, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jun 11, 2026
wpfleger96
pushed a commit
that referenced
this pull request
Jun 11, 2026
69e6bc8 to
fba912f
Compare
2357920 to
ac3a45e
Compare
Four-tier config bridge that reads agent configuration from config files (goose YAML, claude JSON, codex TOML), ACP session data, env vars, and Sprout-explicit overrides — surfacing a unified normalized config surface to the desktop UI regardless of runtime. Key changes: - Config bridge module with per-runtime file readers - ACP session config caching for post-spawn config visibility - AgentConfigPanel component with origin badges and tier provenance - Serde internally-tagged enums matching TypeScript discriminated unions - TOCTOU-safe write path with single lock scope - E2E mock handler for get_agent_config_surface with per-runtime fixtures and a Playwright screenshot spec covering 7 scenarios - Info icon + tooltip on read-only fields; override/strikethrough rendering for superseded config values Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…place badges with provenance sentences
Persona-linked agents had their inherited model/prompt/provider invisible
in the config panel, and the source indicators (checkmark/hourglass icons,
colored badges, footer) were undecipherable.
Phase 1: Resolve all three persona fields (prompt, model, provider) in a
single resolve_config_surface helper called by both get_agent_config_surface
(read) and write_agent_config_field (write). The helper injects resolved
values into the record where absent, calls the reader, then re-tags the
injected fields from BuzzExplicit to PersonaDefault. The re-tag is triple-
gated so a value the user set explicitly in Buzz is never re-tagged. Sharing
the helper keeps the read and write surfaces identical, so plan_config_write
never returns "field not available" for a persona-sourced field. Reader stays
untouched (pure tier-merge function).
Phase 2: Add AgentConfigPanel to ProfileSummaryView in the profile pop-out,
gated on isBot && isOwner && managedAgent defined.
Phase 3: Remove SourcesFooter and colored OriginBadge pills. Replace with
gray inline provenance sentences below each value ("Set in Buzz", "Inherited
from persona", "From environment variable", etc). No action clauses.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
ac3a45e to
69d2c4e
Compare
Picking a model on a persona-linked running agent now applies to the live instance instead of forcing a restart. The switch rides the existing `desired_model` lever plus the Interrupt requeue/invalidate machinery: a busy turn cancel-switch-requeues onto a fresh session under the new model; an idle session invalidates and re-applies on its next turn. The override is runtime-only — never persisted to `record.model`, gone on restart/respawn, so spawn resolution stays persona-wins. `ControlSignal` drops `Copy` to carry the owned model id; the post-match re-read is replaced by a `requeues()` predicate. A model absent from the agent's catalog surfaces an `unsupported_model` control_result (idle path guards pre-cancel; busy path validates on the re-created session post-cancel) so the picker rejects rather than silently no-opping. The desktop keys the override-active display off the ACP `current_model` diverging from the persona model (the harness-only `desired_model` is unreadable by the reader), shows the persona as a non-struck secondary, and folds the standalone Configuration block into the metadata card. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
… flag The runtime-override display keyed off `acp_model != persona_model`, which cannot distinguish a live ModelPicker pick from a session left stale after a mid-life persona edit. Editing a running persona-linked agent's model A->B false-positived as a live override — re-introducing the display-versus-reality bug this surface exists to kill. The harness now stamps a `model_overridden` flag into session_config_captured (true only when a SwitchModel control signal set desired_model, reset on spawn); the reader gates the override on that flag. Also fix multi-channel sendLiveSwitch: it resolved on the first control_result of any status, so a fast `sent` from one channel masked a later `unsupported_model` from another. Now any single rejection fails the pick immediately (fail-fast); success requires every channel to acknowledge. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The config-bridge provenance rows and ModelPicker origin/restart labels used hardcoded text-[11px]/text-[10px] literals. The px-text guard (added to main via the rem-token migration) forbids arbitrary font-size literals so text scales with Cmd +/- zoom. Swap all four to the text-2xs meta-text token (0.6875rem), the documented sibling for these decorative sub-labels. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The any-unsupported-fails-fast counting for a live model switch was locked inside the sendLiveSwitch useCallback in ModelPicker, verified by read but not unit-pinned. Extract the counting (remaining decrement, immediate unsupported reject, resolve-once, unsubscribe-once, timeout fallback) into a pure awaitLiveSwitchOutcome helper with the relay subscription, per-channel sends, and timeout scheduler injected. The component wires the real subscribeControlResults / window timer / dispatch; behavior is unchanged. The helper is node:test-drivable with synthetic frames and a manual clock. The masking-guard test fails against a first-ack-resolves variant and passes against the shipped fail-fast logic. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The interim settled===false checks used a single await Promise.resolve() drain, which the outcome.then callback outruns by one microtask tick, so the guard passed even against a first-ack-resolves variant. Drain five ticks before each interim check so it deterministically regresses an early resolve. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
A genuine-explicit agent (own record model, no persona) that live-switched mid-session rendered nowhere: resolve_config_surface passed no override baseline for an explicit record, so apply_runtime_override early-returned and the panel showed the stale record model as primary with the live model struck through — display contradicting reality. Carry the override baseline as a typed (value, origin) pair end-to-end so the secondary is tagged by its true source (BuzzExplicit for the record case, PersonaDefault for personas) instead of a hardcoded PersonaDefault. Build the record-model baseline only when model_overridden is true, so a persona edited mid-life still does not false-positive. On a live pick equal to the baseline, yield a clean single-value field rather than passing the pre-polluted base through (build_model_field independently sets an AcpConfigOption secondary for the record-plus-live-session case). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The live-acp-vs-record override is now exclusively apply_runtime_override's job, gated on model_overridden. build_model_field's acp-derived secondary predated that gate: with record_model=Some(X) and acp_model=Some(Y) it populated overridden_value=Some(Y) unconditionally, and that row passed straight through apply_runtime_override's !model_overridden early-return — surfacing a live override before any switch was applied. Collapse the secondary to express only the static record-vs-file precedence (a Buzz-explicit model shadowing a config-file model); drop all acp_model references from it. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The v4 screenshots captured the old origin-badge UI. The shipped surface
now renders inline provenance sentences ("Set in Buzz", "Inherited from
persona", "Live override (this session only)", etc.), a folded config panel,
and a non-struck persona baseline for runtime overrides.
Re-grounds the screenshot scenarios to the sentence-based render and adds a
multiOriginSurface fixture (one distinct origin per row) so the provenance-
sentences shot witnesses multiple distinct sentences in one frame instead of
duplicating the folded-panel capture.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Collaborator
Author
wpfleger96
pushed a commit
that referenced
this pull request
Jun 18, 2026
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.





Adds a harness-agnostic config bridge that reads agent configuration from all available sources and exposes it to the frontend with full provenance — where each value came from and how it can be written back.
Agent config was fragmented across four uncoordinated mechanisms with no single surface to see what a running agent would actually use. The silent failure mode was concrete: Wes's goose personas failed because
active_providerin~/.config/goose/config.yamlwas invisible to Sprout and overrode our injected model. The bridge surfaces that ambient config, establishes a clear precedence order, and routes writes to the cheapest live mechanism.config_bridgemodule with per-runtime config file readers: YAML for goose (~/.config/goose/config.yaml), JSON for Claude Code (~/.claude/settings.json+~/.claude.json), TOML for Codex (~/.codex/config.toml). 25 unit tests covering all formats, malformed files, and missing files.configOptionsfromsession/new> env vars > config file on disk. Pre-spawn surfaces tier 2 only; post-spawn adds ACP tiers.session_config_capturedobserver event emitted aftersession/new;put_agent_session_configTauri command populatesSessionConfigCacheinAppState.parse_modelshandles both the object shape ({currentModelId, availableModels}) and legacy array shape fromsession/new.get_agent_config_surfaceandwrite_agent_config_fieldTauri commands. Write mechanism is chosen per runtime and field:session/set_config_option(live, no restart) before env-var respawn. Single lock scope on the write path closes a TOCTOU race.AgentConfigPanelcomponent with origin badges (Sprout/ACP/ConfigFile/Env), collapsible advanced section, and sources footer showing all four tier statuses. Override/strikethrough rendering shows superseded config values inline — the overridden value appears crossed out next to the effective value, with both origin badges visible for at-a-glance provenance. Read-only fields display an info icon with tooltip rather than a lock icon.ModelPickershows provenance label when config surface data is available post-spawn (best-effort, non-blocking).codex.provider_lockedtofalse— Codex supports[model_providers.<id>]custom provider tables. Fixes serde enum tagging onConfigWriteMechanism,ConfigFieldType, andWriteConfigTargetto use#[serde(tag = "type")]matching the TypeScript discriminated union shapes (default external tagging made all write operations and TS type guards silently broken).Stack: #794 → this PR