The wizard's only entry point was the "Start Ralph Loop" button on the
start screen, removed in 1.50.0, so none of this could be opened or
triggered from the app any more:
- ralph-wizard.js (the wizard, plan generation flow and the floating plan
subagent windows) and the #ralphWizardModal markup;
- the "Plan Wizard Agents" section of the monitor panel with its cancel
button, and the five plan:* SSE handlers that only fed these surfaces;
- the wizard branches in the subagent window code: connection lines to
the wizard and to plan agents, window placement beside the wizard, and
the wizard cleanup on reconnect. The ordinary path (agent window to its
parent tab) is unchanged, only un-nested;
- the wizard's state on the app object and its z-index constant;
- its styles: 67 classes, three keyframes and a second identical
@keyframes spin.
What stays: the Ralph panel, the Ralph tab in session options, the plan
routes (/api/generate-plan, /api/generate-plan-detailed,
/api/cancel-plan-generation, the ralph-wizard file routes) and the
plan:* SSE events, which are part of the public API. A client that
starts plan generation over the API still gets its events; the page no
longer draws anything for them.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
toggleMonitorDetach / toggleSubagentsDetach and their two drag setups
were the same 97 lines twice, differing only in three element ids. They
now call _togglePanelDetach(panelId, headerId, detachBtnId) and
_setupPanelDrag(panelId, headerId); the two public names stay, since
index.html calls them.
Checked in a browser: each panel detaches, drags by its header inside
the viewport, docks back, and after a second detach one drag moves it
exactly once (the header's previous listeners are still replaced).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The frontend JS is outside ESLint and knip, so this pass used an AST scan
(every method, function and top-level constant in src/web/public, counted
against identifiers, strings and index.html) plus a selector scan of both
stylesheets. Each hit was confirmed by text search before it went.
JavaScript
- The ten run<Cli>() one-line wrappers and the quickStart() alias: run()
has dispatched to _runCliMode(mode) since the launcher consolidation and
nothing called the wrappers except tests, which now call the launcher.
- The Agent Teams panel code that lost its event listeners in February:
the team tasks panel, the teammate pane window, rebuildTeammateMap and
the three maps that nothing filled any more.
- Unreferenced methods: redockSession, refreshOrchestratorStatus,
toggleSubagentPanel, minimizeSubagentWindow,
openAllActiveSubagentWindows, pickBetterPath, getTodoIcon, deletePreset,
closeRunSummary, saveSessionOptions, getAgentParentSessionId,
_isUltracodeAgentMinimized.
- Six unused constants and a few unused locals (an unused frame-time
computation in the touch scroll loop among them).
CSS
- 62 classes and one id that no markup, script or test names: the old
settings-item / settings-section layout, the pre-redesign respawn and
context-settings rows, the existing-wizard card and others. 116 rules
and about 770 lines. A dead class inside an :is() list only loses that
alternative; the rule stays.
Checked in a browser against an isolated instance: no console errors on
desktop or phone, a shell session runs, and 96 screenshots (desktop,
tablet and phone; settings, session options, add case, panels) are
pixel-identical before and after, apart from clock times.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Found with knip (exports, types, class members) and confirmed by a text
search of src, test, scripts and packages before each removal.
- Per-CLI get<Cli>NotFoundMessage() (nine of them) with their message
constants: missingCliMessage() builds that text from the registry now.
- Registry leftovers: renderCliCommand, asCliId, cliIds, isCliRunnable,
createCliResolverForTest, profileExists, and two name lists that only
repeated schema enums.
- A task-description cache in Session that was written on every chunk of
terminal output (one regex pass per line) and never read since the
subagent watcher takes descriptions from transcripts. The cache module,
its pattern and both parse methods go with it.
- Session.parentAgentId / childAgentIds: never set by anything, so the two
optional SessionState fields were never on the wire.
- Unreferenced methods on TmuxManager (pipe-pane pair, syncMouseMode),
RespawnController, StateStore.recoverFromBackup (load() already falls
back to the .bak file), TeamWatcher, SseStreamManager and the AI
checkers' getErrorVerdict.
- updateCaseEnvVars: the settings.local.json env path that envOverrides
replaced.
- Re-exports in the utils, cli-registry, routes and prompts barrels that
no importer used, and unused test helpers (MockStateStore, the mobile
throttle helpers and others).
Eight mobile tests imported the WebServer type from a path that does not
exist (../src instead of ../../src); type-only, so nothing failed.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The viewer looked for a Claude transcript under ~/.claude/projects only, and
for the conversation after a /clear in ~/.claude/history.jsonl only. A session
started with its own CLAUDE_CONFIG_DIR (a second account, #255), or any session
on a server that exports one, writes both elsewhere, so the eye button showed
"No response yet" or a terminal dump, and codeman agent read and the Discord
bridge's last message came back empty. This is the third part of the report
in issue 701; the other two are a design choice and a feature request.
lastResponseFor() now asks claudeConversationDirs() for the folder the
session's claude writes to: its own override, then the server's, then
~/.claude, through claudeProjectsDir(), which the relaunch pin and the Edit
history panel already use. The rename's /resume title takes the same lookup
when no hook has named the transcript yet.
- Local sessions only. In a Docker case or on a remote host the override
names a folder over there, and the same path on this host is someone else's.
- A relocated folder is a path somebody chose, so it is probed bounded first
(the rule from issue 516). Anything but a directory reads as no transcript,
never as a reason to look under ~/.claude, where a file of the same name is
another account's conversation.
- The default folder is not probed, so that path is as before.
Not changed: the resume pre-validation at session create and the host-wide
history scans still read the default folder.
Tests: five route cases for the viewer (own folder, the server's, no fallback,
/clear through the relocated history, Docker left alone) and one for the
rename. Each part of the change fails one of them when reverted.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The button's aria-label replaced its visible text as the accessible name with
a sentence that does not contain "Copy Markdown" (WCAG 2.5.3, Label in Name),
so a voice-control user could not say what they see. The label is gone and the
sentence stays as the title.
No test reached the line that keeps the brief view's answer when the full view
holds no answer of its own: the existing case returns earlier, on an empty
list. A full view that holds only a prompt reaches it.
The PR's own changeset is folded into the 1.51.0 one.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The viewer renders the agent's answer, so selecting it loses the source:
headings, list markers and code fences do not survive. The only source copy
was the per-code-block button.
Copy Markdown copies the raw source of the newest answer the viewer holds:
every row of its turn, or the last response block when the reader emits no
turns. Tool and status blocks share the assistant role and are left out. The
copied text is recorded where the views render, so a reopened viewer never
copies an earlier answer, and an empty viewer warns instead of copying.
The feedback helper replaces the inline copy of the same logic in the
code-block button. On phones the header title now truncates so the longer
action row stays on screen.
Seen on a MacBook (M5 Pro, 2026-11-12): pressing Use on a model in "Models on this Mac"
ran the switch and then showed the screen that ends an install, "X is ready on this Mac"
with Start a session alone. The list was gone, so deleting the model just switched away
from meant closing the window and opening it again.
That screen is right after a download: the person installed a model and wants a session.
A switch is pressed IN the list by someone who is tidying it. A switch is a job without a
`model` step, so the window now tells the two apart from the job it just watched and ends a
switch on the whole installed screen: the list with "In use" on the other row, Add a model,
Stop and Remove.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Until #809 a display:none rule in mobile.css and its KNOWN_PHONE_HIDDEN
entry kept the File Viewer button off phones, and the gate checked both.
With that rule gone, the handheld default in getDefaultSettings() and the
html.mobile-init startup rule are the whole lock, and only the browser
test saw them, which npm test does not run.
The policy test now loads the real getDefaultSettings() on a handheld and
on a desktop, checks that every reader of the setting goes through the
device default, and that mobile.css keeps the startup hide.
Docs: architecture-invariants, src/web/CLAUDE.md and the wiki say the
button is off on a phone until it is switched on there, and that a window
narrower than 600px on a device that is not a handheld now shows it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Head 6c2e257d (DodgyBadger). The phone half of #759: the button stays off
by default on a phone and shows once the setting is turned on.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Conflict in skills/codeman/SKILL.md (and its plugin mirror): the list of
modes with neither stop nor blocked gained jcode here and the omp
exception from #798 on the release branch; both are kept.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The four review items of #797, applied at merge.
The editor's name rule lowercases, the data dir's .env pattern did not:
.codeman/.ENV passed both, and on a case-insensitive filesystem (the macOS
default) that is ~/.codeman/.env, the login file codeman attach reads. A
session whose workspace is the home folder could open and save it. The
pattern is case-insensitive now, pinned in the blocklist table and on the
three editor routes.
A save writes a new file and renames it over the old one, so the file took
the server's own group: a 0640 .env owned you:www-data came back you:you
and the web server lost read access. The save now puts the old user and
group back where the account may (best effort), and the wiki says mode
and group instead of permissions.
Tests: an .env in an outside worktree is refused on edit=1 and PUT too,
now that the editor's own gate lets dotenv through; the comment on why
.env.example is blocked for downloads and attachments is back.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The four edits promised in the review of #798:
- An omp alert could clear while omp's dialog was still up. omp items got
the same pane `capture` as Claude's, which the inbox re-reads with
Claude's numbered-dialog parser. omp's dialog has no numbered rows, but an
agent's plan above it does, so the list was parsed as the item's options,
and once it scrolled out of the frame the item was resolved as answered in
the terminal. An approval-only item now takes the frame once as `context`
and gets no `capture`, like a screen-read item (web/screen-blocked.ts), so
the module's `elicitation_complete` is the only thing that clears it. A
route test puts a numbered list above an omp-shaped dialog and fails on
the old code at both steps (options parsed, alert cleared).
- The reboot-restore install is dropped: the restore plan skips every CLI
whose transcript is not `claude-jsonl` (omp's is `omp-jsonl`), so it never
ran, and it sat after the start, when omp has already scanned its hooks.
- The docs that say which CLIs raise `blocked` and open alert-only items now
name omp: api-reference, CLAUDE.md, architecture-invariants, an entry for
`approvalHookFile` in cli-registry, and the skill with its plugin mirror
(verbs.md listed omp among the modes where `blocked` is a 400, one line
above the paragraph that says it works).
- Two comments: the schema's file-name rule is a pattern, not a closed list,
and the marker check takes the marker anywhere in the file.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Selecting a chip stored it for the device at once. Someone who clicked
"DeepSeek" (Needs setup) or "Pi" (Not installed) just to see what it
says then got a window that reopened on that chip: "Install Pi", or no
main button at all, with "Start a session" out of sight.
The device now remembers the agent a session was STARTED with, an
opening of the window selects it only while it can be started, and a
selection made in one opening does not carry into the next. Otherwise
the server's default agent is selected, as before.
Also: a real-browser test for the chips (they wrap inside the window at
1440, 390 and 320 px, the install command included, one tab stop), and
one sentence in the API reference: a readiness mark says the composer is
there, not that nothing else is (DeepSeek Harness's setup guide draws
the same glyph).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The two edits promised in the review of #799:
- The main pane's Ctrl+V branch hands the keydown to the paste trap, and
nothing tested that call: with it reverted to `_handleImagePaste()` every
test still passed and Firefox would paste twice again. A source pin now
fails on that, like the Ctrl+C one in terminal-copy-clean.test.ts.
- architecture-invariants and src/web/CLAUDE.md described the key's own paste
as reaching the trap in every browser. Since #799 that is Chromium and
WebKit only: in Firefox the trap cancels the keydown.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- docs/local-models.md: the four agents with their first request, whether
the window installs them and what was seen where (a real model, a
scripted endpoint, Linux, not the pinned runtime); what differs for Pi
and for DeepSeek Harness; installing one more agent beside a model.
- docs/api-reference.md: `POST /api/local-model/agent`, `install.harnesses`,
the `install-agent` action, and `install.cli` as the default agent.
- docs/architecture-invariants.md, CLAUDE.md, src/web/CLAUDE.md: the
rules: the agent is chosen per session, the agent install is not a
job, a launcher is never installable, a readiness mark is a measured
one, and the window's one builder for the agent chips.
- docs/custom-model-endpoints.md: Pi's context window.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The installed screen gets one more block, "Start a session with": a chip
per coding agent the model in use can be run with, as a radio group, the
server's default selected. A chip that is not on the Mac says so.
The main button follows the selected chip:
- an agent that can be started: "Start a session", with that agent;
- an agent that is not on the Mac: its literal install command above the
button, and "Install <agent>". The click is the consent; the server
runs its own stock command, and the window shows "Installing" on the
chip while the model stays ready and another agent can be started;
- an agent that needs a setup of its own (DeepSeek Harness without a
terminal profile): one line that says where, nothing to press.
A failed install shows the server's sentence under the chip with the
installer's last words folded away; the model's own headline is
untouched. On a stopped model Start stays the one main button. The
choice is remembered on the device. A server that lists no agents gets
the screen it had.
Rendered in Chromium at 1440, 390 and 320 px on a dark and a light
skin: the chips wrap, nothing leaves the window.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
An installed model knew one coding agent: the one it was installed with.
The launch check never asked about the agent, only about the endpoint
and the model, so nothing stopped a second one. What was missing is the
list, and a way to get an agent that is not on the Mac yet.
The install view gains `harnesses`: every catalogue harness the model in
use can be run with, each with how it stands (`ready`, `installable`,
`installing`, `setup-needed`), the literal install command, a readiness
mark for scripts, and the actions the server accepts for it. `cli` stays
the default agent, and names one that can be started whenever one can,
so a client that reads only `cli` never starts an agent that is gone.
`POST /api/local-model/agent { cli }` installs one more agent beside an
installed model. It is not a job and not an event of the reducer: the
state stays `installed` whatever the installer does, so a failed npm
install never blocks a session on a model that works. Same install slot
and same compiled stock command as the `agent` step, one at a time, the
outcome in memory only.
The catalogue:
- `pi` is no longer gated. Its two open questions were answered by a run
against a real model (the `name:tag` id reaches the runtime unchanged;
its first request is 2,096 tokens), and Codeman now passes it the
window. Its minimum is 65536 like the others: nothing below was run,
and no model row offers less, so no Mac's pick changes.
- `deepseek` is new. It is a launcher: its install command leaves it
with nothing to launch, so it is never reported installable (read off
`discovery.launcherProfile`, not the id) and a Mac is offered it only
where it was already set up. Its tool round trip was seen on a
scripted endpoint only; the header says how to gate it again.
- a row may carry `readyMark`; `opencode` has the measured one.
A switch no longer refuses for the agent a model was checked with when
another agent that can run it is on the Mac.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The guide, the API reference, the invariants and the two CLAUDE files
describe the library with one model in use: the state file's list, the
three verbs and their refusals, the two cached listings of the view, and
the window's list with its row actions.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The local model window could hold one model. A second install was refused
(409 already-installed), and a model a Remove kept could never be freed
from the window again. The store can now hold several models, and exactly
one of them is the model in use: the one the endpoint record lists, the
runtime is started for and a session may be launched on.
Server
- LocalModelStateFile.library lists every model that passed the check
(id, label, context, digest, size, coding agent). The service writes
the entry when verify passes; the reducer carries it and remove drops
it. An installed file of an earlier build counts its one model.
- POST /api/local-model/install is allowed from installed: it adds a
model, and the earlier one stays. A replaced job no longer deletes a
model of the library, only an unchecked or partial download.
- POST /api/local-model/use switches to a checked model: a job of the
two steps verify and connect. The stored digest is compared before
anything is loaded (model-changed otherwise). Nothing is downloaded.
- DELETE /api/local-model/models deletes one model by its name, through
the runtime's own delete, never by a path, never the model in use.
- Adding and switching answer 409 sessions-open while a live session
uses the endpoint. Until such a job ends installed, a session start is
refused with the new code job-unfinished.
- The view carries library (the state's entries joined with what the
running runtime lists: ready, changed, missing, unverified) and store
(the size of the blob folder). Both are cached and refreshed in the
background, never read on the one-second poll.
- With nothing installed, remove is offered while the store still holds
models an earlier Remove kept.
Window
- The installed screen lists the models on this Mac with the context
each was checked with, its state and its size on disk, with Use and
Delete per row (Delete asks once, in the row) and Add a model.
- In the chooser a model that is on this Mac already says so and is
switched to without a download or a confirm screen.
- A paused or failed job that has a checked model beside it shows the
way back to that model.
Every route is admin only in multi-user mode, like the existing ones.
Not run on a Mac: the tests use fakes and the fake runtime, and the
listing, store size and delete were checked against a real Ollama 0.13.0
on Linux, not the pinned 0.40.15 build for macOS.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review follow-up. omp declared `hooks: 'always'`, which every consumer reads
as Claude's full hook set, so an omp session picked up behaviour it cannot
back. omp goes back to `hooks: 'none'`; `capabilities.approvalHookFile` is now
a separate, narrower capability with its own predicate,
`approvalHookAvailableForMode()` (local, non-adopted sessions only).
- An explicit `stop` wait on omp is a 400 again; `blocked` still works.
- omp inbox items are alert-only (`answerInTerminal`, no `approvalId` on the
push), and only `permission_prompt` / `elicitation_complete` are accepted for
an approval-only session.
- The module is installed from POST /api/quick-start too (new and existing
cases, not Docker or remote) and on reboot-restore, after the launch
pre-flight on both create paths. Claude's settings.local.json block and the
Docker CLAUDE.md no longer land in omp workspaces.
- The generated module passes Bun's `tls: { rejectUnauthorized: false }` for an
https CODEMAN_API_URL (checked against a `--https` server), clears the alert
only when the last pending toolCallId resolves, sends reports in order,
carries omp's policy `reason` as `message`, and imports HookAPI from
`@oh-my-pi/pi-coding-agent/extensibility/hooks`.
- Every path component and the target file are lstat-checked.
- docs/omp-integration.md, verbs.md and the stock.ts comments updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ctrl+V in Firefox still pasted the clipboard twice some of the time.
Right-click → Paste never did.
`_handleImagePaste()` focuses a hidden contenteditable trap, calls
`document.execCommand('paste')`, and reads the clipboard from the paste
event that lands on the trap. Firefox dispatches that event synchronously,
inside the `execCommand` call. It then delivers the key's own paste as a
separate task, about 10 ms later in Playwright's Firefox. The one-shot flag
from b87bc687 drops that second event, but only if it still reaches the
trap. The trap removes itself and refocuses the terminal on a
`setTimeout(0)` queued by the first event, and that timer races the second
paste. When the timer wins, the second paste lands on xterm's textarea,
xterm's own `handlePasteEvent` sends the clipboard again, and the flag
never sees it.
The trap now cancels the keydown when its paste listener has already run
by the time `execCommand` returns, so Firefox never issues the second
paste. Both callers pass the keydown as `target.keyEvent`: the main pane's
key handler in terminal-ui.js and a tile's in terminal-tile.js. Chromium
and WebKit refuse the command and dispatch nothing from it, so their
keydown is left alone, because its default action delivers their only
paste event.
Measured in Playwright's Firefox with the trap removed before the key's
paste arrived: the paste reached xterm's textarea and the text went in
twice. With the keydown cancelled, the key's paste never fired. End to end
on an isolated instance with a shell session, 20 Ctrl+V presses gave 20
pastes in both Firefox and Chromium.
test/image-paste-trap.test.ts gains a harness option in which
`execCommand` delivers the paste from inside the call, as Firefox does.
New cases cover the keydown being cancelled for the main pane and for a
tile, being left alone when `execCommand` delivers nothing, and a caller
that passes no keydown. The two cancel cases fail without this change.
test/terminal-tile-input.test.ts now expects the tile to pass its keydown
along with its terminal and session.
Docs: the Terminal paste section of docs/architecture-invariants.md and
the paste-trap entry in src/web/CLAUDE.md now describe the race and the
rule. The section's old claim that the key's default action alone always
reaches the trap is replaced, since in Firefox it can miss it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An omp pane asking for tool approval raised no tab alert, no desktop
notification and no Approvals Inbox item. omp inherited hooks:'none' from
agentDefaults(), so hooksAvailableForMode() answered false and
hook-event-routes never opened an item; there was nothing to resolve either.
omp is one of the few non-claude CLIs with real approval events
(tool_approval_requested / tool_approval_resolved), so Codeman can install a
hook module into <workspace>/.claude/hooks/pre/ - the directory omp scans -
on session create. It forwards them as permission_prompt and
elicitation_complete, the names the server already routes on, so the existing
alert, notification and inbox pipeline works unchanged.
The file name comes from the new capabilities.approvalHookFile field rather
than a branch on the CLI id (cli-registry-no-id-branching). stop/idle_prompt
are deliberately NOT reported: omp's idle stays screen-read via workDetect,
which the agent skill's verbs.md now says explicitly.
The plain preview already serves a workspace dotenv file in full to the same user, so refusing the edit protected nothing. The edit gate now passes allowDotenv to the sensitive-path check, which skips only the two dotenv patterns; every other credential pattern, a dotenv file inside a blocked tree and Codeman's own data-dir login file stay blocked. Downloads and attachments are unchanged.
- docs/api-reference.md and the voice-stream.ts overview now say a
transcript frame is the current utterance so far and a final frame
ends it, as the relay's own comment and the invariants doc do.
- VoiceInput.cleanup() hides the Send button before stop(true), so the
button stays up for the words stop(true) salvaged and inserted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_onWebSpeechEnd counted every run that ended without a result toward the
two-retry cap, however long the run lasted. Under 30 s or Manual, each
no-speech window of the engine is one silent run, so the third ended the
recording at about 18 s of silence, before the deadline, and Manual
stopped on its own. Both recognition.start() sites now stamp the run start;
an empty run counts toward the cap only when it ended within a second of
starting (a recognizer that cannot listen), and any other run resets it.
The 250 ms backoff is unchanged.
The vm harness in test/voice-silence-timeout.test.ts now passes the test's
Date into the context, whose own Date ignores fake timers.
Also from review: the VoiceInput stubs of the four settings-ui.js tests
carry SILENCE_TIMEOUT_SECONDS; Settings-Reference lists Stop after silence;
the touchscreen Linux fallback leaves out ChromeOS and says a touch Linux
laptop still matches; the dead _webSpeechStream teardown is gone; the two
voice tests use vitest expect instead of node:assert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The floating log viewer reconnected every few seconds when its tail-file
stream ended with a startup error (path refused, file missing, too large,
stream limit, tail spawn failure). It now closes the EventSource on an
`error` message that arrives before `connected`; an established stream
keeps its reconnect.
Squashed from #720 (johoja12). Maintainer edits while landing, as the
review said: the onerror close is dropped (it stopped the browser's own
retry after a transport failure, leaving the window on "Connecting"),
the test's check for a button nothing renders is gone, and the stubbed
stream sets `retry: 200` so a reconnect, if one came, shows within the
shorter wait.
Co-authored-by: Codeman maintainer <kay@stutz.science>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- session-ui.js: drop the answer when the case-sync chain moved during the read; settings-ui.js calls _applyServerCaseSettings with ?.
- i18n.js: zh-CN for the failed case-options save toast, case name passed through
- tests: regression for the stale read, zh-CN toast row
- sync-case-state browser and stale-refresh tests use lastPickedCase (the stale test now fails without its guard)
- saveLastUsedCase runs on the case-sync chain, and the follow is skipped while this device's own pick was not stored
- _caseStateSyncOn tolerates a harness without loadAppSettingsFromStorage
- lastPickedCase added to the config-backup volatile-key test and docs; rename rollback tests cover caseSettings and lastPickedCase
- wiki row moved to System
- follow lastPickedCase (written by saveLastUsedCase while sync is on), not lastUsedCase, so a quick-start or an agent's spawned worker no longer moves other devices
- close the open case-options popover when the follow or an import changes what it shows; follow through selectQuickStartCase(save:false)
- failed save reuses _noteSettingsWriteResult and says the choice lasts until the next sync
- rename undo renames the shared entry back instead of restoring the whole map; lastPickedCase follows a rename
- caseSettings and lastPickedCase added to the redaction, rename and backup-volatile lists and the docs
The fixes#529's review asked for, each with a test.
- No inline handler. The button quoted the path inside
onclick="...decodeURIComponent('<path>')", and encodeURIComponent leaves
' alone, so a file named x'-alert(1)-'.txt ran script in Codeman's
origin and it's.txt was a syntax error. It is now bound in the existing
.file-tree-item loops from the row's escaped data-path.
- Symbolic links are refused, never followed. validateSessionFilePath()
returns the realpath, so deleting AGENTS.md -> CLAUDE.md deleted
CLAUDE.md and left the link dangling. The path as named is lstat'ed,
a link answers 400, and the inode is compared again before the unlink.
- The edit route's confinement whole: validateLocalSessionFilePath()
behind withGuardedOutsideWorktree(), so an absolute path in a followed
outside worktree obeys the same rules, and the sensitive, blocked and
.git checks run on both the realpath and the path as named.
- Adopted sessions are refused like remote ones (the /tmp exception of
the older file surfaces is not copied to a new one); Docker workspaces
are allowed, as on the edit route. The workspace is probed bounded
first (#516). Errors are thrown with their status.
- Deleting from search results re-runs the search, the row buttons stay
visible on touch screens, the confirm and toasts have zh-CN text, and
no button is drawn for remote or adopted sessions.
- Docs: CLAUDE.md, architecture-invariants, api-reference and the
Working With Files wiki page.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>