A subagent window could host an xterm attached to a teammate's tmux pane,
but only for a teammate listed in teammatePanesByName, and the SSE
listeners that filled that map were removed in February. Since then the
branch could not be taken, and the code behind it was unreachable:
- initTeammateTerminal and the lazy dispose / restore pair that parked a
teammate terminal while its window was minimized or hidden;
- the two maps (teammatePanesByName, teammateTerminals) and every loop
over them: window close and reconnect cleanup, the live font-weight
save and the skin switch;
- the two early returns in renderSubagentWindowContent that protected a
terminal which could not exist.
Subagent windows render their activity list exactly as before, and a
teammate still gets its name and badge from the agent description. The
server side is untouched: TeamWatcher still holds respawn back while
teammates are active, and the teammate-pane routes remain.
Tests and docs that described "Agent Teams panes" now name what a font
or skin change really reaches: the main terminal, the tiles and the
split pane.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
- Nine one-off scripts from January and February that nothing references:
three Ralph wizard drivers that click the start-screen button removed in
1.50 (against the production port, with a personal case name), a respawn
monitor with a hardcoded session id, link-detection experiments for
patterns that no longer exist, and two screenshot and data helpers.
- @types/uuid: uuid 14 ships its own types and TypeScript already
resolves them from the package.
- config/knip.json now describes the workspace layout it runs in (its
top-level entry and project keys were being ignored), names the manual
scripts as entries, lists the system binaries tests shell out to, and
honours @knipignore. `npm run knip` reports nothing; CLAUDE.md says so.
- Two more unused tap helpers in the mobile test toolkit.
- Docs that pointed at files which no longer exist: the browser testing
guide (four deleted test files), the orchestrator panel's file name, two
spawn modules in the Ralph guide, the respawn mock utilities.
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 two commits of #803 that came after 1.51.0 was cut. The earlier commits (every model that fits, Muse Glimmer 30B and Ornith 1.0 9B) shipped in 1.51.0. No changeset: the next release names it.
Three corrections to the row added in 5396e33b, none of which changes where it is offered:
- Its peak. The run gave the process footprint's LIFETIME maximum, read at the end: 3.0 to
3.3 GB above what was held then. That is a floor, because a larger transient during the
earlier, longer prompts would not show in it. The row now takes the first measured row's
5 GB: 36.1 GB at 65,536 tokens (0.960 of a 48 GB Mac's estimate) and 39.0 GB at 131,072
(0.972 of what that Mac's runtime measures).
- What was compared. The two builds were not given the same prompt in OpenCode: the GGUF
build got a one-line task through the skill's sendmark (which adds its answer-file
sentence), the MLX build a plain one-line prompt. And the speeds to set side by side are
the two from the same 300-token request, 15.2 and 30.2.
- Runtime issue 18269: whether the memory fix its maintainer promised predates the pinned
runtime is not known, and the header says so.
Also a stale comment: the catalogue holds eleven models.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Run on a MacBook Pro (M5 Pro, 48 GB, runtime 0.40.15) on 2026-11-12, after the GGUF build
this branch lists turned out to be unusable in agent use there: an OpenCode turn of one
line showed no answer after 7 minutes, twice, although the runtime alone wrote 15 tokens
per second.
The MLX build of the same model (`muse-glimmer:30b-mlx`, 19.1 GB) on that Mac:
- 32.6 tokens per second for a 1,500-token answer. The tag carries a draft model, and the
runtime logs speculative decoding with 0.34 to 0.62 of the drafts accepted.
- A correct tool call in under 3 seconds, and the same OpenCode turn finished in 45 seconds.
- Memory, read from the runner's own log line after every request: 37,900 to 45,100 bytes
held per token (the GGUF build computes to 7,072), prefix snapshots that filled their
8 GiB during twelve separate prompts, and a process peak 3.0 to 3.3 GB above what stayed
held.
So it gets a row of its own, the second with measured figures. They put it at 34.6 GB for
65,536 tokens: offered from 48 GB up, never on the 32 and 36 GB Macs, which keep the GGUF
row untouched (it is still the pick of a 32 GB Mac with a Max chip: whether it should be
is the maintainer's decision). It is never the pick of a Mac whose disk has room for
Qwen3.6 35B-A3B, and it has no `tested` entry: one turn is not a test.
Also written down from the same session: the session of many prompts the Qwen3.6 35B-A3B
row owed (5.6 GiB of prefix snapshots after twelve prompts, still growing; its figures are
unchanged), and what the two open runtime issues on the MLX tag say against what ran.
Not done: the row was not installed through the window, and the measurement used a second
instance of the pinned runtime, not Codeman's.
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>
Seen on a MacBook (M5 Pro, 2026-11-12): with a worker of the codeman skill open on the
local model, Stop, Remove and a switch of models answered
Sessions are still using this model: . Close them first.
A session an agent starts has no name of its own (a quick-start without sessionName), and
the refusal listed session.name. The tab of such a session shows the last folder of its
working directory, else the first eight characters of its id, so the sentence now uses the
same label (sessionLabel() beside sessionNamesUsingEndpoint()).
The sentence has shipped since 1.50.0 for Stop and Remove. This branch adds it to Use and
to the install of another model, which is where a person with local workers open meets it.
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 review items of #777, finished here so the PR can land.
A statusline report fires the trigger in the middle of a turn, and the
command then waits for that turn to end. Until now it went out whatever
had happened meanwhile: with auto-clear at 140k, a report of 150k fired
while the session worked, Claude Code compacted by itself, the report
fell to 20k, the session went idle and /clear wiped the conversation.
Both actions now ask again at that moment and return unsent when the
context is no longer at the threshold, when the option was switched off,
or when its threshold was raised past the context. The skipped command
stamps nothing, so the next report is judged like any other, and the
autoCompact and autoClear events carry the size at the moment of sending.
With auto-resume on, a trigger that fired before a usage limit hit sent
/clear once the pane went idle, and auto-resume then typed continue into
an empty conversation. The checks start nothing from the limit message
until the resume prompt went out, and an action that waited through the
start of a pause returns unsent.
Also: the threshold cap never drops below the 1,000 minimum (a reported
window of 500 stored a threshold of 500), a test pins that a conversation
switch drops the report, and docs/api-reference.md names the new
contextWindowSize session field.
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>
A second scripted run, on dsh 0.2.2-rc.1 with a freshly installed
dsh-TUI 0.19.0 under a scratch home: the model name reaches the endpoint
unchanged, the first request is 6,983 tokens with 27 tools, and a tool
round trip completes.
Two things that version does differently are written down where a reader
of the harness table looks: a setup guide opens in every session until a
person has finished it once (and its cursor is the readiness mark), and
its transcript file was renamed, so `last-response` is empty for it.
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>
A pi session on a custom endpoint got a model list entry with only the
model id. pi then takes the model for a 128,000-token one (its footer
reads "/128k") and asks for 16,384 reply tokens, so on a 65,536 or
32,768 window it never compacts before the server's real window is full.
The entry now carries `contextWindow` (the full window) and `maxTokens`
(a quarter of it, 8,192 at most) whenever the endpoint's context length
is known: the same two figures the DeepSeek Harness patch already
carries, now from one helper. An unknown length writes neither, as
before.
Measured on pi 1.5.2: with both fields `pi --list-models` shows the
window, and the request carries `max_tokens: 8192`.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A job started from a model the person had stopped runs the runtime, and
the model it ends on is in use and running. The stored "stopped" flag
stayed set, so that model would not have started with the server again.
Both verbs now set it back once their job started, the way Start does.
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>
A 16 GB Mac got no model: the two 9B builds of the catalogue need 10.2 GB at
65,536 tokens, 0.987 of the estimated budget, just past the 0.98 a tight fit
may reach.
New row `ornith:9b-q4_k_m` (Ornith 1.0 9B, MIT), a fine-tune of Qwen3.5-9B for
coding agents whose GGUF build has no vision projector: 5.63 GB against 6.55 GB,
so 9.3 GB at 65,536 tokens, 0.898 of that budget. SWE-bench Verified 69.4,
SWE-bench Pro 42.9, Terminal-Bench 2.1 43.1 on Ornith's card. Its config.json
and its GGUF header are those of Qwen3.5-9B (no prediction layer), so it shares
the figures of the two other 9B rows. Never run on a Mac: `estimated`.
- What changes for a Mac: 16 GB goes from no model to `limited` with this row,
tight. That is the only pick that changes: the row is ranked under MiMo V2.6
9B, so every Mac that could already run a 9B keeps its pick and gains an
alternative. With the budget a 16 GB Mac mini measured (10.7 GB) the pick is
MiMo as before.
- Why under MiMo although its own card's numbers are higher: the two vendors'
tables put their common base 7 points apart, and runtime issue 16992 (open)
was filed on the 35B sibling, which has the same renderer and parser. The
35B stays set aside.
- The 16 GB rule itself is untouched: same budget estimate, same 0.98, same
eight context checkpoints.
- Ranks renumbered 1 to 10; the pins of test/local-model-recommend.test.ts and
the table of docs/local-models.md follow.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The "Run a local model" chooser showed the pick and four more models. It now
lists every catalogue model that fits the Mac, and the install accepts every
listed row.
- MAX_ALTERNATIVES goes from 4 to 11, above what the catalogue holds. It stays
a bound: test/local-model-catalog.test.ts fails for a catalogue that outgrows
it, so nothing drops off the list unseen.
- New row `muse-glimmer:30b-q4_k_m` (Meta, Apache-2.0): SWE-bench Verified 76.0,
SWE-bench Pro 51.2, Terminal-Bench 2.1 51.7 on Meta's card. Its K/V cache is
the smallest of the 27B class, so it is the first model of that class a 32 GB
Mac holds: 21.2 GB at 65,536 tokens, 0.970 of the estimated budget. Figures
computed from its config.json and its GGUF header by the llamacpp rule (13
full-attention layers, 39 sliding-window layers of 2,048 tokens, no
prediction layer); the sliding-window cache rule was read again in llama.cpp
b12111 (src/llama-kv-cache-iswa.cpp). Never run on a Mac: `estimated`.
- What changes for a Mac: 32 GB is no longer `limited`. On an M1 Max or M2 Max
the pick is Muse Glimmer; on a base or Pro chip the dense model is slow and
the pick stays MiMo V2.6 9B, now with the small-pick note. Every other pick
is unchanged.
- The "set aside" list was read again at its sources on 2026-11-10 and gained
ripple-preview:65b (every build requires runtime 0.40.16, one above the pin),
Ornith 1.0 (runtime issue 16992), nemotron-3.5-super:120b, qwen3.5:4b,
qwen3-coder-next and the MLX and DFlash tags of Muse Glimmer.
- The header and the guide now say that runtime issue 16992 has one diagnosed
cause in the tool parser every Qwen-family row shares (a tool argument that
quotes a whole tool call is cut off; the fix is runtime PR 17021, open).
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.