Skip to content

2.2.0: your own endpoints, Agent settings, find, text size, smarter palette and UI polish - #6

Merged
ShadowfetchStudio merged 28 commits into
mainfrom
claude/adoring-allen-j9xho7
Oct 12, 2026
Merged

ShadowfetchStudio merged 28 commits into
mainfrom
claude/adoring-allen-j9xho7

Conversation

@ShadowfetchStudio

@ShadowfetchStudio ShadowfetchStudio commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

UI and feature improvements for 2.2.0. CHANGELOG.md has the full entry, under ## 2.2.0 (unreleased).

Features

  • Your own OpenAI-compatible endpoints (Settings › Accounts › Your own endpoints). Connect Ollama, LM Studio, a llama.cpp or vLLM server, xAI, OpenAI or any compatible API. Find models lists what the server offers; tick the models you want, then Test or Remove them.
    • New routes:
      • POST /api/providers/models lists a server's models and saves nothing.
      • POST /api/models/endpoints adds the chosen models and saves the key under a name chosen per service (api.deepseek.com → DEEPSEEK_API_KEY).
      • POST /api/models/remove removes a model; its saved key stays.
    • Only models added in Settings are listed (added_in: "settings"). The model: fallback in config.yaml, which the engine registers itself, never shows up.
    • Servers on this computer join On this computer; others are API rows that are off in offline mode.
    • A remote device never makes the desktop send a saved key to an address it typed.
  • Settings › Agent covers limits, the token budget and autonomy profile, tool timeout and output size, history shortening, retries, the stuck check, subagents, checkpoints and the commands that count as checks.
    • Each field validates its range inline and has Reset.
    • Only changed keys are saved, so a project's own values are never copied into the profile.
    • It shows and saves the profile's own values. When the open project uses another, the setting notes it ("This project uses 200."). GET/PUT /api/config return agent_profile and agent_project for this.
  • Find in conversation (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.
  • Search finds words in answers. GET /api/sessions?q= also matches task summaries and returns a match excerpt, which the sidebar and the Tasks tab show with the words marked.
  • Smarter command palette:
    • fuzzy, ranked matching;
    • jump to conversations and projects;
    • a Recent group;
    • full combobox/listbox ARIA.
  • Text size and zoom:
    • Ctrl+= / Ctrl+- / Ctrl+0 from 80% to 175%, saved as ui.zoom.
    • The desktop uses the webview's own zoom (core:webview:allow-set-webview-zoom added to the window's capability). Browsers use CSS zoom, with viewport units divided out at build time.
    • A phone or browser using remote access keeps its own size in its own storage, starting at 100%. It never writes ui.zoom, so the desktop's size never changes from a phone.
    • Theme and size preview live on the Appearance page.
    • The palette reaches all three themes.

Polish and fixes

  • Message times at the edge of each message.
  • Tool output: Show all / Show less and Copy.
  • Page title names the open conversation.
  • Composer: Enter picks the highlighted / command.
  • Toasts pause while pointed at or focused.
  • Screen readers: milestone announcements instead of streamed tokens, and a named log.
  • Drawer: proper tablist.
  • Reduced motion is respected by "Latest activity".
  • Automations: delete uses the app's own dialog.
  • Theme default: ui.theme now defaults to system, matching the window and onboarding.
  • Docs:
    • fixed the stale Custom model… reference in NATIVE_ROUTING.md;
    • the retry example matches the default of 3;
    • USER_GUIDE.md, API_CONTRACT.md, SUBAGENTS.md and others updated.

Engine fixes

  • Spending limits: choosing Stop at a limit could end the task as failed instead of cancelled. The answer woke the waiting task before the engine cancelled it. spend_gate now cancels the task itself when it sees Stop (engine/spending.rs).
  • Subagent settings are checked when saved. A subagents value that is misspelled, the wrong type or out of range is refused, with the allowed range, by PUT /api/config and Config::patch. Before, it was clamped, or the whole group silently fell back to defaults. Config::load stays lenient, so an existing config.yaml still starts the engine.
  • Settings › Agent showed project values as the profile's. GET /api/config is project-merged while PUT saves to the profile. The page now uses agent_profile and notes agent_project overrides; other pages are unchanged.
  • Stale-socket tests: they now wait until a dropped listener really refuses before asserting. Another test forking at bind time can briefly hold a copy of it. The sweep also counts a socket timestamped ahead of the clock as age zero.

Tests

  • Rust:
    • cargo fmt --check and clippy --all-targets -D warnings are clean.
    • New: tests/custom_endpoints.rs, using a fake OpenAI-compatible server.
    • New unit tests in endpoints.rs, models.rs, store.rs, control.rs and subagents.rs, plus route tests in config_keys.rs.
    • data_care is updated for the new theme default.
    • Locally, cargo test --workspace --no-fail-fast passes 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.
  • UI:
    • typecheck and Prettier are clean;
    • Vitest: 869/869;
    • Playwright: the full suite (136) passes once the drawer-tab selector updates in two specs and a smooth-scroll race in workspace.spec.ts were fixed in this PR.
    • New specs: endpoints, search, palette, agentSettings, find, appearance and transcriptPolish, with axe checks in both themes.

Notes for review

  • Left for a follow-up: autonomy_profile step caps are not enforced, and mcp.inline_tools is missing from the defaults.
  • The desktop title bar stays "ShadowCode"; only the page title changes. Forwarding it would need on_document_title_changed in src-tauri, and scripts/clean-host-gui-smoke.sh checks the exact window name.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fn7NbipAkYG7cYZxHk8xRu

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
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0f8b49a8-9310-43e4-b8ba-30aa2d45344f

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

claude added 16 commits October 12, 2026 14:34
…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
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
@ShadowfetchStudio ShadowfetchStudio changed the title Next version: your own endpoints, answer search, smarter palette and UI polish 2.2.0: your own endpoints, Agent settings, find, text size, smarter palette and UI polish Oct 12, 2026

Copy link
Copy Markdown
Owner Author

verify (aarch64) on 20a3ddb: one failure, control::tests::stale_sockets_are_swept_without_touching_live_ones (native/core/src/control.rs:597). The sweep returned [] where the test expected the dead socket.

I don't think this failure comes from this PR:

  • The PR doesn't change control.rs or anything it calls.
  • The same test passed on aarch64 for 4af4726 and e6f862a.

Likely cause: in sweep_stale_sockets, now.duration_since(modified).ok() treats a socket whose timestamp is even slightly later than SystemTime::now() as not old enough. That can happen with clock adjustment or file timestamp granularity on the runner. The test calls the sweep with Duration::ZERO right after binding, so it is the case most exposed to this race.

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
@ShadowfetchStudio
ShadowfetchStudio marked this pull request as ready for review October 12, 2026 16:18
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

Copy link
Copy Markdown
Owner Author

verify (aarch64) on 7bb969f failed one test: process_stress::a_detached_pipe_cannot_hang_completion (native/core/tests/process_stress.rs:136, assert!(result.truncated)).

I don't think this comes from this PR:

  • The PR doesn't change the process runner or this test.
  • The test passed on aarch64 in every earlier run of this PR.
  • It depends on timing: a setsid child must still hold the output pipe when the shell exits after sleep .1.

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. 7bb969f is already superseded by 79927ff, whose CI is running, so I'm not re-running the old job. If it fails again on the current head, I'll treat it as real and fix it.


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
@ShadowfetchStudio
ShadowfetchStudio merged commit 232778a into main Oct 12, 2026
9 checks passed
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.

2 participants