Repository navigation
2.2.0: your own endpoints, Agent settings, find, text size, smarter palette and UI polish - #6
Conversation
The engine could already run any OpenAI-compatible model registered through `shadowcode models --use` or config.yaml, but the desktop had no way to add one and the picker never listed them. Settings › Accounts gains a "Your own endpoints" card: choose Ollama, LM Studio, a llama.cpp or vLLM server, xAI or any OpenAI-compatible API, find its models, tick the ones you want, then test or remove them. - POST /api/providers/models lists one server's models (Ollama's /api/tags, otherwise /models) with the typed key or, on this computer's window only, the saved one. Nothing is saved; addresses with credentials, a query or a fragment are refused and a bare host gains /v1. - POST /api/models/endpoints registers the chosen models and saves a typed key under a per-service name (api.deepseek.com uses DEEPSEEK_API_KEY), so two services never overwrite each other's key. - POST /api/models/remove removes a model added this way; keys stay. - GET /api/picker lists these models (`custom: true`): loopback servers with the models on this computer, others as API rows that are off in offline mode and ask for their key if a saved key goes missing. Tasks on them run ShadowCode's own agent loop through the registry. docs/NATIVE_ROUTING.md pointed at a "Custom model…" control that does not exist; it now names the card. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The window, onboarding and Settings › Appearance all treat a missing theme as "system", but the engine's default (and config.example.yaml) said "light", and a first-run save without a theme stored "light". A profile that never chose a theme therefore opened light on a dark desktop. The default is now "system" everywhere; saved choices are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
Task search (the sidebar and the Tasks tab) matched titles, folders and
prompts only, so a conversation was hard to find again by what the
agent said, and a result never showed why it matched.
GET /api/sessions?q= now also matches each task's answer (its summary).
A result whose title doesn't contain the words carries `match`: an
excerpt of the newest prompt or answer that matched, on one line, with
the match's character offsets. The sidebar and the Tasks tab show it
under the title ("Answer · …the TOML reader…") with the words marked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The command palette (Ctrl+K) matched commands by plain substring, could not reach a conversation or project by name, forgot what was used last, and its field had no combobox wiring for screen readers. - lib/fuzzy.ts: a pure matcher. The query's letters must appear in order (case, accents and spaces ignored); a dynamic-programming search picks the best alignment, scoring word starts (including camelCase), runs of consecutive letters and the text's first letter higher, and returns the score and matched positions. "rev ch" finds "Review changes", "cmp" finds "Compare models on this task". Ties keep the given order. - Palette.tsx (moved out of overlays.tsx, still exported from there): with a query, commands, titled conversations (up to 8, project folder beside them) and recent projects (up to 5) are listed in labelled groups, the group holding the best match first so Enter runs what the query fits best. Matches far below the best one (letters scattered across unrelated words) are left out. Matched letters are bold and accent-coloured; options are named by their label alone, with the shortcut or project as their description. - With nothing typed, a Recent group shows the last five commands run from the palette (shadow:palette-recent via readStore/writeStore, tolerating blocked or malformed storage), then the other commands. - ARIA: the field is a combobox with aria-expanded, aria-controls and aria-activedescendant over a grouped listbox of options with aria-selected. Arrows, Page Up/Down and Home/End move (Shift+Home/End still select text), Enter runs, Escape closes as before; the active option scrolls into view, and the pointer moves it without taking focus from the field. - A conversation chosen in the palette opens through the same function as the sidebar's. Every existing palette entry, its order and its behaviour are unchanged. Tests: unit tests for the matcher, component tests for the palette and its conversation/project entries, and e2e/palette.spec.ts (fuzzy search with an axe pass in both themes, opening a conversation, recents across a reload, keyboard and pointer). The fake engine gains a `conversations` option to seed titled conversations. The user guide describes the palette. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
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. Comment |
…and checks
The agent, subagents, checkpoints and verification groups had no UI, and
the guides told people to edit config.yaml for them. Settings › Agent now
edits every key of those groups: steps, token budget and autonomy profile,
tool time limit and output size, parallel reads, history shortening and
summaries, provider retries and backoff, fix attempts, the stuck check,
the subagent switch and limits, checkpoint switches and sizes, and the
list of check commands.
Defaults, types and ranges mirror config.rs, subagents.rs, checkpoint.rs
and verification.rs. Each field says what it does, its range and default,
has a Reset, and is checked as you type (aria-invalid, described by its
error); Save waits until every value is valid. A save the engine refuses
shows its reason above Save and marks the setting it names, so onSave
takes an optional { rethrow } that the other pages do not use.
The page sends only the keys that changed. GET /api/config includes a
trusted project's own agent values, so sending whole groups would copy a
project's choices into the profile. The subagents group is absent until
saved; the page then uses the engine's defaults. The autonomy profile caps
the token budget, and the page says so when a larger budget would have no
effect.
The palette gets "Agent · limits, retries, subagents and checks", the
polish spec checks the page in both themes, and the user guide,
SUBAGENTS.md, SUBSCRIPTIONS.md, VERIFICATION_RECEIPTS.md and
config.example.yaml point to the page while keeping the YAML keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
There was no way to find text in a conversation: the transcript draws only its latest 150 rows and older history is paged in, so even a browser find could not reach most of a long conversation. Ctrl+F (also "Find in conversation" in the command palette) opens a compact find bar at the top of the stage. It searches the whole conversation's prompts, answers and notes, ignoring case: the latest messages, and every saved history page, read one page at a time in the background while a query is open (the same 128-event pages "Older messages" reads). Answers are matched as they read on screen (Markdown markers left out), and the bar shows "3 of 12" or "No matches" in a polite live region. Enter / Shift+Enter and F3 / Shift+F3 step through matches, wrapping. A match outside what is drawn is brought into view: its saved page opens as "Older messages" would (keeping Newer messages working), the row window grows down to its row, and it scrolls to the middle of the view, smoothly unless reduced motion is asked for. Every drawn match is highlighted with the CSS Custom Highlight API and the current one distinctly, in theme token colours; where the webview lacks the API (older WebKitGTK) the match's row is outlined instead. Escape closes the bar and returns focus; another conversation, Review or Compare closes it. A code editor's own find and the terminal keep Ctrl+F. Matching lives in a pure module (lib/find.ts) and the drawn-text side in lib/findHighlight.ts. Each item's searchable text is worked out once (items are replaced, never changed), each saved page is matched once per query, and the latest messages are searched again at most every 300 ms while an answer streams. Matches are kept as per-row counts, so a one-letter query in a long conversation stays small; at most 2,000 drawn matches are highlighted at once. useConversation gains history.show (open a saved page directly), history.base and liveItems; TranscriptRows marks searchable rows with a display: contents wrapper and takes a row to keep drawn. The performance fixture can load a larger latest window, which the new e2e spec uses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The interface had no zoom: sizes are fixed pixels, Ctrl+= and Ctrl+- did nothing, the theme on Settings › Appearance only showed after Save, and the palette's light/dark toggle could never return to System. Text size (`ui.zoom`, 80–175%) steps with Ctrl+= / Ctrl++ (Shift and the keypad too), Ctrl+- and Ctrl+0, under the shortcut rules that already exist: a terminal keeps the keys (Ctrl+- is readline's undo) and dialogs own theirs. A small status at the top right says the new size; each press replaces it rather than stacking notifications. The size applies at once and is saved once the keys stop, through the same deep-merged `ui` config save as the Appearance page, and mirrored in browser storage like the theme so a reload opens at the right size before the config is read. The desktop zooms its webview (`setZoom`, which needs core:webview:allow-set-webview-zoom in the main capability), so layout, media queries and the terminal's and editor's pointer maths behave as under browser zoom. A plain browser (remote access, the test suite) cannot set its own zoom and gets CSS `zoom` on the root, which needed three corrections, each covered by the new Playwright spec: - Chromium scales `vh`/`vw` inside a zoomed root by the zoom, so Settings and the terminal grew past the window. A small PostCSS step divides every viewport length by a `--zoom` property that is only set in this mode (it divides by 1 on the desktop). - xterm.js maps the pointer with window coordinates over cell sizes it measured unzoomed, so selections landed on the wrong cells. The terminal undoes the zoom on its own box and scales its font instead. - The conversation menu placed at the pointer divides by the zoom. CodeMirror already handles a scaled editor; the spec checks a click. Settings › Appearance gains a Text size control (the house segmented control, with Reset to 100%). Theme and text size now preview as they are picked; leaving the page or Settings without saving restores the saved ones, and only the saved values are remembered for the next start. The palette offers Theme: same as the system / light / dark (marking the one in use) and Zoom in, Zoom out and Reset zoom with their keys. The keys are in the shortcuts dialog and the user guide has an Appearance section. The Playwright fake can keep the `ui` group across a reload (keepUi) so the spec can check persistence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
…itle Prompts and answers now show when they were sent. The time comes from the `ts` of the event that recorded the message (user.message, or agent.started for older records; an answer's latest model.stream / model.delta, or agent.completed), so stored history, live streams and forks all read the same, and a message without a recorded time shows none. It sits small and muted beside the message actions: "14:32", "Yesterday 14:32", the date and time earlier this year, only the date before that, in the viewer's locale, inside <time dateTime> with the full date and time as a tooltip. A message within five minutes of the last time shown (same day) keeps its time quiet until it is hovered or focused, so a quick exchange shows it once. Formatting and the quiet rule are pure helpers (lib/messageTime) with an injectable now and locale-stable tests; times re-render at midnight via useWallClock. Tool output in the activity timeline and on hook cards was cut at 4,000 characters with no way to see the rest. It now offers "Show all (N lines)" / "Show less" and a Copy button in the box's corner with the code-block feedback (a shared useCopyFeedback hook). Copy always copies the whole output; the expanded view is bounded to the last 2,000 lines and 200,000 characters and says what it leaves out, so huge output cannot stall the window. The scrolling box is focusable for keyboard users (axe scrollable-region-focusable). The document title is "<conversation> — ShadowCode" (with "● " while a task runs, plain "ShadowCode" with no conversation). Nothing in the UI, e2e or scripts depends on the exact document title; the desktop window title is set by src-tauri and does not follow document.title. The fake engine records event times with a fraction of a second as the engine does, and can run a search with many matches (searchHits) for a long tool output. New e2e spec: transcriptPolish.spec.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
With the / menu open, Enter sent the half-typed text ("/rev") instead of
the command the menu highlighted; only Tab picked it, unlike the @ menu
where Enter and Tab both pick. Enter now picks too, and the next Enter
(with the menu closed) sends. Click, Tab and Enter share one pick, which
adds a trailing space only for commands that take arguments.
Matching was prefix-only, so "/view" found nothing. Names that start with
the text still come first, then names and then descriptions that contain
it (lib/slashCommands.ts). The field's aria-controls and
aria-activedescendant now point at the / menu and its highlighted row, as
they already did for the @ menu, which gets an id for aria-controls.
The fake engine lists a few built-in commands so the e2e suite can drive
the menu. The user guide describes the ranking and keys.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
Deleting an automation was the one destructive action still asking with window.confirm, which looks foreign in the desktop window and has no danger styling. It now uses ConfirmDialog like deleting a task or discarding a change: "Delete this automation?", what goes and that the conversations it created stay, and Cancel / Delete automation. A "Automation deleted" notification confirms it, as Run now confirms a start. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
Notifications closed after 5, 8 or 12 seconds whatever the reader was doing, so one could vanish while being read or just as Tab reached its Undo. useToasts now keeps each notification's countdown and `hold` pauses it; the stack holds a notification while the pointer is over it or focus is inside it, and resumes it with the time it had left (at least two seconds) once both have gone. One pushed out by newer notifications while held finishes its countdown. The stack was click-through with only its buttons taking the pointer, so hovering a notification's text never reached it. Each notification now takes the pointer; the gaps between them stay click-through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
"Latest activity" scrolled with scrollTo and behavior "smooth". The stylesheet's reduced-motion rule sets scroll-behavior: auto, but a script's own behavior overrides it, so the conversation still glided for people who asked the system to reduce motion. lib/motion.ts answers "auto" for them and "smooth" otherwise; it was the only smooth scroll in script (the others already jump). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The drawer's tabs were toggle buttons (aria-pressed) in a group, so a screen reader announced seven pressed/not pressed buttons and Tab stopped on each. They are now a tablist of tabs with aria-selected and aria-controls, labelling a single tabpanel, with one tab stop (the selected tab). Left/Right wrap around and Home/End jump to the ends; they only move focus, and Enter or Space opens the focused tab, so passing over Terminal or Preview does not start a shell or a preview. Nothing changes visually: the tabs keep their buttons and the "on" class. e2e specs that found the tabs as buttons now ask for tabs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The whole status bar was a polite live region, so every allowance and
context figure change was read out; now only its state (Ready, Working,
Paused…) is live. The conversation had no role or name, and the running
timeline was a status region whose ticking clock was re-read every
second. The conversation is now a log named "Conversation" with
aria-live off (streaming answers would be read word by word), and the
timeline is no longer live.
Instead one visually hidden polite region (shell/Milestones.tsx) says
each milestone once: "Answer finished", "Task failed", "Task stopped"
(also "Task stopped at the plan limit", "Task interrupted") as the open
conversation's task ends, and "Approval needed: npm test" when a new
approval arrives. It watches the conversation's job (set by
useConversation as job.done arrives) and the feed's approvals, says
nothing for a finished task that is opened, puts each message in its own
node so a repeat is read again, and clears after ten seconds. It sits
outside the conversation so Compare and Review hear it too.
An approval card is named for its action ("Approval needed: npm test",
from approvalSummary) and described by its question, so several cards
tell themselves apart. The user guide says what a screen reader hears.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The command palette's input is now a combobox and its entries options (Palette: fuzzy search…), so the appearance spec looks them up by those roles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
- data_care: a reset profile starts with the default theme, which is now "system". - palette and appearance specs: the drawer's panels are tabs (aria-selected) since the drawer became a tab list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
"Latest activity" disappears once the view is near the bottom, while a smooth scroll may still be gliding; scrolling to the top then raced it and the button never came back on a busy runner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The picker listed every registry row for a compatible server that had a key name, which also matched the `model:` fallback in config.yaml (the engine registers it on its own, so it appeared as soon as GET /api/models ran). Models added in Settings › Accounts are now marked `added_in: "settings"`, and only those are endpoint rows the picker lists and Remove accepts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
|
I don't think this failure comes from this PR:
Likely cause: in Proposed patch (not pushed here, to keep this PR scoped): let old_enough = before
.modified()
.ok()
.map(|m| now.duration_since(m).unwrap_or(Duration::ZERO))
.is_some_and(|age| age >= min_age);A timestamp in the future then counts as age zero. A real minimum age still leaves such a socket alone, and only a zero minimum age, as in this test, removes it. GitHub refused the re-run while the x86_64 job in the same run is still going; I'll re-run the failed job once when the run finishes. Generated by Claude Code |
`sweep_stale_sockets` measured age with `now.duration_since(mtime).ok()`, so a socket whose time was even slightly ahead of `SystemTime::now()` (a clock step, timestamp granularity) was never old enough, even with a zero minimum age. That made `stale_sockets_are_swept_without_touching_live_ones` fail now and then on the aarch64 runner. Such a socket now counts as age zero: any real minimum age still leaves it alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
`decide_spending` records Stop and wakes the task waiting at the limit before it cancels the task. When the task woke first, `spend_gate` bailed with the stop summary while the cancel token was still unset, so the task finished as "failed" instead of "cancelled" (seen as a 120 s timeout in `headless_clients_see_the_card_answer_it_and_get_an_estimate` on the aarch64 runner). The gate now cancels the task itself when it sees Stop, so either order ends the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
A `subagents:` value with a typo, the wrong type or out of range was silently clamped, or made the whole group fall back to defaults. Saving one through Settings (PUT /api/config) or `Config::patch` now refuses it with the allowed range. Loading stays lenient, so a config.yaml written before this still starts the engine and is clamped as before. Also notes the spending Stop fix in the changelog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
|
I don't think this comes from this PR:
It is the third different timing-sensitive test to fail once on the aarch64 runner. The other two, the stale socket sweep and the spending Stop status, are fixed in this PR. No fix for this one exists yet. Generated by Claude Code |
The stale-socket tests drop a listener and expect connecting to its path to be refused at once. When another test in the same binary forks at that moment, its child holds a copy of the listener until it execs, so the socket still accepts for a short while: the sweep skipped it (CI saw `[]`), or `socket_refuses` returned false. The tests now wait, up to 10 seconds, until the dropped sockets refuse before asserting. Production is unaffected: a socket held that briefly is swept on the next pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
GET /api/config returns the configuration merged with the open project's `.shadow/config/config.yaml`, so the Agent page showed a trusted project's own `agent` values as if they were the profile's, and measured changes against them, while PUT saves to the profile. Both config routes now also return `agent_profile` (the profile's saved `agent` values) and `agent_project` (the keys the open project changes, with the values it uses). The Agent page shows and diffs against the profile's values, and notes "This project uses N." under a setting the project overrides; checkbox rows now show their note too. Other pages are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
`ui.zoom` is shared config, so zooming on a phone over remote access changed the desktop's text size, and a phone opened at the desktop's 150%. A browser on the remote transport now keeps its size in its own storage, starting at 100%: zoom keys apply and remember it there without saving, and Settings › Appearance saves theme and notifications but hands the size to the browser. The desktop and the test bridge are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
The worktree mutation lock is a non-blocking flock. When any thread forks (a git command, a check) just as the previous owner closes the lock, the child holds a copy of that descriptor until it execs, and the next owner was refused with "Another process owns worktree cleanup or mutation" (seen in `uncertain_keep_survives_restart_and_blocks_mutation_ and_cleanup` on the aarch64 runner). A busy lock is now retried for up to half a second before it counts as held by another owner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
`a_detached_pipe_cannot_hang_completion` started a `setsid` child and exited after a fixed 0.1 s. On a slow runner the child had not called setsid() yet, so the runner's group kill took it too, the pipe closed, and the output was not marked truncated. The shell now exits once the child has written its pid, which it does after setsid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu
UI and feature improvements for 2.2.0.
CHANGELOG.mdhas the full entry, under## 2.2.0 (unreleased).Features
POST /api/providers/modelslists a server's models and saves nothing.POST /api/models/endpointsadds the chosen models and saves the key under a name chosen per service (api.deepseek.com→DEEPSEEK_API_KEY).POST /api/models/removeremoves a model; its saved key stays.added_in: "settings"). Themodel:fallback inconfig.yaml, which the engine registers itself, never shows up.GET/PUT /api/configreturnagent_profileandagent_projectfor this.Ctrl+F) searches the whole conversation, including older pages that aren't loaded. It highlights matches with the CSS Custom Highlight API, or outlines the row where that's missing.GET /api/sessions?q=also matches task summaries and returns amatchexcerpt, which the sidebar and the Tasks tab show with the words marked.Ctrl+=/Ctrl+-/Ctrl+0from 80% to 175%, saved asui.zoom.core:webview:allow-set-webview-zoomadded to the window's capability). Browsers use CSSzoom, with viewport units divided out at build time.ui.zoom, so the desktop's size never changes from a phone.Polish and fixes
Enterpicks the highlighted/command.ui.themenow defaults tosystem, matching the window and onboarding.NATIVE_ROUTING.md;USER_GUIDE.md,API_CONTRACT.md,SUBAGENTS.mdand others updated.Engine fixes
spend_gatenow cancels the task itself when it sees Stop (engine/spending.rs).subagentsvalue that is misspelled, the wrong type or out of range is refused, with the allowed range, byPUT /api/configandConfig::patch. Before, it was clamped, or the whole group silently fell back to defaults.Config::loadstays lenient, so an existingconfig.yamlstill starts the engine.GET /api/configis project-merged whilePUTsaves to the profile. The page now usesagent_profileand notesagent_projectoverrides; other pages are unchanged.Tests
cargo fmt --checkandclippy --all-targets -D warningsare clean.tests/custom_endpoints.rs, using a fake OpenAI-compatible server.endpoints.rs,models.rs,store.rs,control.rsandsubagents.rs, plus route tests inconfig_keys.rs.data_careis updated for the new theme default.cargo test --workspace --no-fail-fastpasses except for 13 tests that need a non-root user or a fresh engine binary. They fail with "Run without root/DAC override" or "a different version" in this root container.workspace.spec.tswere fixed in this PR.Notes for review
autonomy_profilestep caps are not enforced, andmcp.inline_toolsis missing from the defaults.on_document_title_changedinsrc-tauri, andscripts/clean-host-gui-smoke.shchecks the exact window name.🤖 Generated with Claude Code
https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu