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 response viewer, the rename's /resume title and the Discord bridge's last message read a local session's own Claude config folder (part of #701). No changeset: the next release names it.
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>
Seen on the Mac mini while checking the release tip: 1 failure in 21 runs of
cargo test, at the line that takes the claim file right after its first
holder dropped it (Busy instead of Yes).
The lock is a flock, which lives as long as the last copy of the open file.
The test binary runs its tests on threads, and a test that starts a process
at that moment hands the new process a copy of every open file until its
exec. Measured there with a throwaway stress test: with a second thread
starting /usr/bin/true in a loop the lock was still held right after the
drop in 12 of 28,000 rounds and free within 2 ms every time; with no process
starts, 0 of 28,000.
Test only. The app does not depend on the lock being free at once: the
child asks for the file again every second, and nothing it holds is copied
into another process, since it starts none.
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>
Seen on a MacBook (M5 Pro, 2026-11-12), in a real Claude Code session: the lead started a
Pi worker and an OpenCode worker on the local model in ITS OWN case, sent both their tasks
at once, and reported afterwards that one written answer was lost. Both prompts had named
the same file, codeman-answer-<token>.md, and the second worker's summary replaced the
first one's. The code the workers wrote was not affected.
The token was the second plus $RANDOM. Claude Code runs zsh on a Mac, and zsh hands every
subshell the same $RANDOM until the parent shell reads it (measured there: four subshells,
one number, where bash drew four). The token line is itself a subshell, so under zsh the
"random" part never changed, and two calls in one second shared a token. Workers in their
own cases never noticed: same name, different folders, and the marker is matched per pane.
The token now ends in six characters of the worker's id. The test makes $RANDOM a plain
variable that always reads the same, which is what zsh does, and runs two calls at once.
The reference gets one sentence for hand-built markers in a fan-out.
No new preamble version: 1.51.0 is not released yet.
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.
The desktop app's paragraph already explains the lid logo. LogoLight is the app for the rest of the time (off, dimmer, effects), steps aside while Codeman blinks it (#802), and now installs with brew install --cask ark0n/tap/logolight. Both READMEs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
One changeset for everything since 1.50.1, in the shape the release page is
built from. It replaces the two that came in with #650 and the lid change.
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>
POST /api/local-model/use, DELETE /api/local-model/models and
POST /api/local-model/agent are experimental like the routes beside them,
and so are the install object's library, store and harnesses lists.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The Mac app keeps a closed MacBook awake through the kernel's lid-sleep
switch, held by a child process of the app, instead of the root helper that
1.50.1 installed behind an administrator prompt. The old helper is no longer
installed or used; Keep This Mac Awake offers to remove it while its files
exist.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Keep This Mac Awake > With the Lid Closed is a plain switch now. 1.50.1
installed a root LaunchDaemon behind the administrator prompt, which set
`pmset disablesleep` while the app asked for it. The app now turns off lid
sleep in the kernel's power management root itself
(`kPMSetClamshellSleepState`, a call any process may make), the method
LogoLight ships as Lit with Lid Closed.
That switch has no owner (read from Apple's sources of the kernel and of
powerd): the kernel keeps it after the process that set it has gone and
honours it on battery, macOS's power daemon and LogoLight write the same
bit, and clearing it under a closed lid puts the Mac to sleep at once. On an
Apple silicon Mac it is also what keeps a docked MacBook awake with its lid
closed, set by the power daemon once, when a display comes. So a child
process holds it (`src/lid_awake.rs`, this binary with `--keep-awake-lid`):
- it sets the switch again every second and holds two assertions with it
(system sleep and idle sleep), the three LogoLight measured together;
- it reads the power source itself and holds nothing on battery;
- it writes nothing at all while an external display is connected: a zero
from here would put a docked Mac to sleep, or leave it without its
closed-lid mode until the display is plugged in again. What the child set
before the display came is cleared once the display has been gone for
five seconds, and the child outlives the app for that if it has to;
- it lets go when the pipe from the app closes (quit, crash, Force Quit) and
when the app has not written for a minute (a hang), the assertions first,
then the switch;
- it ignores SIGHUP, SIGINT and SIGTERM, so the pipe is its one way out;
- it leaves the switch alone at its end while LogoLight holds it for itself
(LogoLight sets it once and would not notice it gone).
One child at a time holds a lock on `lid-awake` next to `desktop.json`, and
the file is non-empty while the switch may be set. A child that was killed
leaves it marked and unlocked: the app turns lid sleep back on when it sees a
child end and at its next launch, and the next child takes the mark over.
The status line says what came of it: the kernel took the call, the lid is
left to macOS at a display, on battery, refused, or that the Mac slept under
the lid all the same and until when. A display has its own wording so a Mac
that sleeps there never reads as covered.
The helper of 1.50.1 is no longer installed or used. Its request file is
never written and is deleted at launch, so a helper that is still there does
nothing. While it is installed the menu has Remove the Old Lid Helper…, the
one item left behind the administrator prompt. `macos/keep-awake-lid.sh` and
its vitest suite go with the install path.
Checked on a Mac mini (macOS 27.2, no lid, no display): 276 tests, clippy and
fmt; the kernel takes the call from an unprivileged process; the real child
holds its assertions, lets go on a closed pipe and after a minute of silence,
ignores SIGTERM, waits for a child before it, writes nothing when told a
display is connected, and a SIGKILLed child is replaced by the app and its
mark undone. 34 deliberate breaks of these rules each fail a test. Not run
yet: the app itself with this change (the mini has no session to open it
in), a real closed lid, the menu items, and the removal of the old helper.
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>