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>
Keep This Mac Awake lives in the app and ends when the app quits, and
closing the main window quit the app. So with a mode on, the red button
(or Cmd+W) followed by closing the lid let the MacBook sleep: the lid
request file was cleared and the helper released disablesleep at its
next run.
While a mode is on, closing the main window now hides the app the way
Cmd+H does (hide_application), so full screen and pop-outs come back as
they were on a click on the Dock icon. Cmd+Q and Quit from the Dock
still end it, through LoopDestroyed as before. With keep-awake off,
closing the window quits as it always did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner found the options confusing after the first test build. The
menu now reads Off / While Agents Work / Always, then on a MacBook
Only When Plugged In and With the Lid Closed…, then one status line.
- "Always While Codeman Is Open" is now "Always"; the status line says
"Awake: Codeman is open".
- "Only on Power Adapter" is now "Only When Plugged In".
- "Remove the Lid Helper…" is gone: the lid item is the helper.
Ticking it installs the helper behind the administrator prompt,
unticking it removes it behind another. The setting changes only
once the prompt's command ran, so a cancelled prompt leaves the item
as it was.
- Shorter status lines ("Awake: agents working, lid closed too",
"Paused: on battery", "Sleeps as usual: no agent working").
Config keys and values are unchanged, so a saved choice carries over.
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
Codeman > Keep This Mac Awake in the macOS app: While Agents Work (an
agent at work, plus 5 minutes after the last one stops) or Always While
Codeman Is Open. Off by default, and only for a Codeman on this Mac.
- Idle sleep needs no password: the app holds an NSProcessInfo activity
with IdleSystemSleepDisabled, which the kernel drops with the process.
While it only watches for work, an activity that still allows sleep
keeps App Nap from slowing its look at the server.
- On a MacBook, Only on Power Adapter (on by default) pauses it on
battery, and Also With the Lid Closed installs a small LaunchDaemon
behind macOS's administrator prompt. It sets pmset disablesleep only
while the app keeps a fresh request file with its live pid, only on
the power adapter, and undoes only what it set itself. The app writes
the script and plist as root from the copies built into it.
- A status line at the bottom of the submenu says what it does now.
This brings the macOS part of the keep-awake idea from PR #595 into the
app; the server-side setting and the Linux route are left out for now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 1.50.0 blink switched LEBacklightMode on and off. Every style was a lit
logo with short dark blips that differed only in how many there were, and
Working (Heartbeat) looked busier than Waiting for you (Wink), so urgency
read backwards. Each change was also a write to a setting corebrightnessd
keeps in root's preferences, and new users had to pick a style in three
submenus before anything happened.
Now one rule: the more the logo moves, the more it needs you.
- View > Blink Apple Logo is one switch, off until turned on. It shows
Breathe while agents work, Calm for a yellow tab and Alarm for a red one.
Each state can take another style under View > Apple Logo Blink Styles.
Picking a style turns the switch on, and turning it on with every state
off brings back those three.
- Breathe is new: a slow fade out and back in, every 6.3 s.
- The logo moves on the LED's own hardware fades (LEPWMFactorFadeDuration,
then DisplayLogicalState Blank or Unblank), as LogoLight measured them on
the real logo (MIT, reimplemented in Rust, not copied). A blink writes no
setting and leaves nothing behind a restart. LEBacklightMode is only read,
so a logo the user switched off stays off.
- The direction of a fade is tracked, because the daemon ignores a write in
the direction it already heads and the state can't be read back. When the
direction is unknown, the logo is turned round with a 1e9 s fade first.
- After every Blank the keyboards alone are told Unblank, and only while
the built-in display is awake (CGDisplayIsAsleep). They are told Blank when
it sleeps and Unblank when it wakes. When it sleeps, the fade duration
also goes back to automatic, as LogoLight does.
- A rise from exactly dark waits for a display frame, up to 0.5 s. Short
dark beats (up to 0.35 s) turn round before their fall ends, so they never
reach 0. Long ones time their light from when the rise starts.
- Every end puts the logo back: lit, with the automatic fade duration. A
new marker file (logo-fade) covers a killed child. 1.50.0's marker
(logo-blink) still switches back on a logo that version left off.
- While LogoLight runs, Codeman leaves the logo to it (process-name check),
and the app says so when the switch is turned on.
- Settings from 1.50.0 keep their styles, with the switch on if any was
chosen. All three off was that version's default, so it reads as
untouched.
The tests run the blink against a fake corebrightnessd that moves in
17.5 ms frames on the measured spring curve. It starts each fade 19 ms
after the write, ignores a write in the direction it already heads, and
holds a rise from exactly dark back by a configurable delay. They pin these
cases:
- dips never reach 0
- long dark beats keep their light
- no keyboard is told to light while the display sleeps
- every end leaves the automatic duration and a lit logo
- a logo switched off or driven by LogoLight is left alone
- old settings are migrated
An ignored probe (what_this_mac_answers) reads a Mac's displays,
processes, keyboards and logo without writing anything.
Not measured on the real logo, since there was no MacBook to test on:
- how the 0.12 to 0.36 s fades of the hard styles look
- whether a rise from a dip (about 7%) really skips the frame wait
- the keyboard during Alarm
- the keyboards through a real display sleep
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A delete button on each file row of the Files panel (tree and search
results) and a DELETE /api/sessions/:id/files?path= route that removes one
regular file from the session workspace, refused for remote (SSH)
sessions and behind the same sensitive-path, attachment-guard and .git
checks as the edit route.
Salvaged from #529.
The review items offered on #608, applied on top of the contributor's
zoom:
- `object-fit: scale-down` instead of `contain`, so a small image keeps
its own size instead of being stretched to fill the preview. The pan
clamp now uses the size the image is actually drawn at (natural size
fitted with scale-down) rather than the image box, which let a small
or letterboxed image be panned entirely out of view.
- Wheel zoom is proportional to deltaY (d3-zoom's scale: about 15% per
100px notch, lines and ctrl+wheel trackpad pinches scaled up), capped
at sqrt(2) per event, and a wheel event with no vertical delta zooms
nothing. A trackpad scroll no longer jumps to 600%.
- The zoom controls show only once the image has loaded; on an error
they are hidden and the gesture listeners are dropped with the image.
Both handlers now check the request sequence like the text branch, so
a late load or error from a replaced preview no longer resets the
current preview's zoom or paints its error over the current image.
- test/path-picker-image-zoom.test.ts drives the real listeners in
jsdom (focal point under the cursor and the pinch centre, pan clamp,
wheel scaling, error, close and stale events) in place of the
source-string checks in test/path-picker-ui.test.ts. 7 of its 10
tests fail against the contributor's commit alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The path picker's image preview gets zoom controls (out, fit/percentage,
in), a two-finger pinch that keeps the pinch centre in place, one-finger
pan while zoomed (clamped to the image bounds), double-click to toggle
2x, and wheel zoom on desktop. closePreview() removes every listener and
drops the zoom state.
Salvaged from #608.
- session-ui.js: no GET /api/settings on a tab return unless this device's cached syncCaseState is true
- system-routes.ts: caseSettings is merged only while syncCaseState is true (as stored or set by the same PUT)
- sync-case-state tests cover both; wiki notes the stored options stay when sync is turned off
The install window polls GET /api/local-model every second. A read of the
state file that was still out while the job wrote its last state and let
go of this.job came back with the old `installing`, and settling that with
no job in the process read it as an interrupted job: `paused` with Resume
for one tick before `failed`. Under CI load this made
test/local-model-install-e2e.test.ts fail on a different failure case per
run (master 86502b69, PR #790).
currentState() now reads again when the job ended during the read. The job
writes before it lets go, so the second read has the final state.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Folding a phone on the home screen left the welcome screen up when the page
had loaded wide (#702): the phone overview's breakpoint listener was wired
from its first show, which a wide-loaded page never reaches. Both home
screens' breakpoint listeners are now installed once from init() and act
only while a home surface is up.
A rail render queued just before the fold could also unhide the desktop
rail behind the phone overview; the debounced refresh now asks again when
it fires.
Tests: test/home-breakpoint-listeners.test.ts (gate) and
test/home-breakpoint-swap.browser.test.ts (browser suite: wide-first and
narrow-first across 599/600 and 767/768, plus a height-only resize).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Redact caseSettings for non-admins and never apply a shared map on a read-only page (CASE-SYNC-001)
- Send one case per save and merge it inside mutateSettingsFile, so devices cannot discard each other's cases; rapid saves keep their order (CASE-SYNC-002)
- Check HTTP status and the response body, keep the local choice, report an unsynced save and reuse the forbidden-write handling (CASE-SYNC-003)
- Let the server decide whether sync is on: refresh the cached switch, discard stale refreshes (CASE-SYNC-004)
- Carry shared options through case rename and drop them on delete or unlink
- Give the switch an accessible name; add server, client and two-browser tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5.3 MB for the universal app and 3.5 MB for the DMG, measured on the Mac
mini from this branch; the README still had the first build's 4.6 and 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behavior change: the connect page renders pixel-identical in eight
states (Chromium, before and after), posts the same messages, and passes
its jsdom and browser tests.
- policy.rs: the connect commands' camelCase fields come from serde's
rename_all_fields instead of three per-field renames; parse_ipc's
length and id checks are match guards like its other arms, with the
message cap named (MAX_MESSAGE_LEN); new_window reads its URL through
external_url, which is the same parse-and-http(s) check.
- logo.rs: the stubs for other platforms are one module, and Logo,
Marker and recover are private to the file.
- connect.html: answers() is the one test for a server that answers
(six copies), the running and needs-login rows of This Mac share their
branch, prepareWizard, needsAgent, newWait and openTerminal each
replace two or three copies, a finished install and a finished agent
share their path, host() is displayAddress(), state.clock is in the
state literal, three CSS rules that matched nothing are gone, and the
state comments say what wait and gui hold now.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behavior change.
- ProbeOutcome::version replaces two copies of the same match in setup.rs,
and classify_response reads the status JSON with serde_json's indexing
(same answer for every body, a missing key is Null).
- local_server::is_env_name is the one test for a variable name, used by
the login shell's environment and by ~/.codeman/.env.
- setup.rs has basename() for its two command-line scans; the install
scan no longer formats the script prefix once per word.
- agents.rs: refused() names the statuses that end the polling, and
answer() takes the body it already has instead of a closure.
- Small simplifications in gui_install.rs (Run::matches, follow, the
download error text) and a let-else in local_server::launch.
- Docs that no longer matched the code: probe.rs said the probe never runs
in a loop, but the This Mac tab re-asks while it waits for an install
(every 3 s) or for a starting server (every second); local_server.rs
covered only Start; FIND_TIMEOUT and exec named one caller of several.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No behavior change. What was written out two to four times now has one
name:
- AppWindow::new and AppWindow::hidden_for_download, window_builder,
placed_position (the corner tao places, which the frame save and the
pop-out cascade both read; cfg! instead of two attribute pairs),
file_name_of and UNNAMED_FILE for the download toast.
- page_sender, which in_background, attach_to_run and both install
starts use; plan_message and remote_install_failed for the connect
page's answers; open_external_raw for the two OpenExternal commands;
choosing() for the four phase checks.
- An accepted probe (Ok, or a password to change) runs one set of steps.
- macos::install runs inside build_webview, so no web view can be built
without the dialog and login handlers.
- ProbeDone no longer carries the Authorization header beside the
username and password it is made from; basic_auth makes it.
- Load::ConnectPage loses its manual flag, which was never true there.
- MIN_SIZE reads config's limits, the default username is
setup::DEFAULT_USERNAME, and the repository and wiki addresses live
in menu.rs.
- The module doc names the modules as they are now, and two doc comments
that had slid onto the wrong item are back on theirs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure moves, no function body changed (git diff --color-moved shows it):
- terminal_install.rs: the Terminal and SSH installs (the scripts, the
ssh arguments, open_in_terminal, copy_to_clipboard) and their tests,
out of setup.rs, which keeps one subject: the Codeman on this Mac.
- login.rs: choose_login, update_keychain and their types and tests,
out of main.rs.
- menu.rs: the menu bar, its ids and AppMenu, out of main.rs.
- policy.rs: loaded_on_commit and its test, a pure Phase decision like
the rest of policy.
What changed besides the moves: module paths, pub on the moved items,
imports, the module docs, and the plutil test's temporary folder name,
which no longer draws on the Terminal scripts' counter. main.rs goes from
2312 to 1881 lines and setup.rs from 2160 to 1625.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The feature table no longer says v0.1, a few paragraphs that had grown
past the wrap width are rewrapped, and the Gatekeeper limit now starts
with the signed and notarized release DMG, so the ad hoc steps read as
what they are: for a build of your own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ureq and wry already pull in base64 0.23 and dirs 7, so the app's own
base64 0.22 and dirs 6 were compiled beside them. The app now asks for the
versions already in the tree. base64 is declared as ureq declares it,
without the default SIMD engines, so the feature set compiled is the one
ureq had before. dirs 7 resolves config_dir and home_dir on macOS exactly
as 6 did (both go through dirs-sys 0.5).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Featured in Awesome AI Agents" badge pointed at a list with far fewer
stars than this repository, so it added nothing. The Download for Mac button
under the tagline is a fifth smaller in both READMEs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The only DMG link above the fold was a plain link in the sentence under the
install command, easy to read past. Both READMEs now open with a large
"Download for Mac" button right under the tagline, pointing at the latest
release's Codeman.dmg, with one line on what the app is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous commit counted 4 rollback copies of the recurrent state for every GGUF with a
prediction layer, the runtime's default `draft_num_predict`. A tag can set its own in its
params layer, and the runtime then passes that as `--spec-draft-n-max`, which is what
`need_n_rs_seq()` turns into rollback copies. Read on 2026-11-08:
- qwen3.6:35b-a3b-mtp-q4_k_m: draft_num_predict 2, so 2 copies (was counted as 4).
- qwen3.8:27b-mtp-q4_k_m: draft_num_predict 4.
- qwen3.8:27b-q8_0: none in its params, so the runtime's 4.
The 35B-A3B GGUF row's overhead drops by 132 MB: 0.971 instead of 0.976 on a 36 GB M3 Pro or
M3 Max, the same pick. test/local-model-catalog.test.ts now carries the drafted tokens per row
instead of a yes/no prediction layer.
Also states what was verified about llama.cpp's checkpoint default: 8 from b6800 to b8200,
32 from b8250 together with a checkpoint every 8,192 prompt tokens (common/common.h at those
tags), in place of an inferred reason. Checked in the b12111 source as well: the draft context
of a Qwen-family MTP model caches the prediction block alone (`mtp_on_hybrid_qwen`, layers at
or above n_layer()), and the main context leaves it out, so the row counts that layer once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The weekly check's report and docs ask for an optional `digest` on each catalogue row, but the
catalogue keeps no digest on purpose: test/local-model-catalog.test.ts ("holds no download
data") refuses a `sha256` anywhere in it. Say so where the digest contract is described, so a
maintainer reads it as a decision for the catalogue, not a quick schema change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The catalogue that names each row's runner (feat/local-model-catalog-v2: `runner` is `mlx` or
`llamacpp`, and a row's memory figures follow that runner's rule) needs the weekly check to
notice when a tag starts serving a Mac another runner: a tag that gains an mlx child, or a
list that loses it. The size check usually catches that, but not when the two builds come out
about the same size, and the figures are wrong either way.
classifyModel() now raises a `runner` finding when the row names a runner and the build a Mac
selects runs on another one. catalogueRunner() maps the registry's names into the catalogue's:
`mlx` stays `mlx`; `llamacpp`, and `ggml` (what Ollama calls a GGUF manifest that names no
runner), are `llamacpp`, because Ollama 0.40.15 serves every GGUF build through llama-server.
A catalogue without the field (the one on this branch today) is never held to a runner.
Run live on 2026-11-08 against the v2 catalogue's eight rows: exit 0, every runner matches.
Tests: the mapping, the finding (both directions, ggml, an unknown runner, a row without the
field), the report text, and runCheck against the fake registry with a plain GGUF manifest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The llama.cpp memory rule of the catalogue left out two allocations that the llama.cpp the
runtime bundles (b12111, the runtime's LLAMA_CPP_VERSION) makes for every GGUF row:
- Context checkpoints. For a model whose state cannot be rolled back (the recurrent state of
the Qwen-family rows, the sliding window of North Mini Code), llama-server keeps host copies
of that state, two or three per request, up to 32 per slot by default
(tools/server/server-context.cpp create_checkpoint). For the 27B that is 32 x 157 MB = 5 GB,
none of which was counted. The runtime now sets LLAMA_ARG_CTX_CHECKPOINTS=8 (read by
llama-server from its environment like LLAMA_ARG_CACHE_RAM; 8 was llama.cpp's own default
before it went to 32), and every llamacpp row counts 8 copies of its state.
- Multi-token prediction. The two -mtp tags and qwen3.8:27b-q8_0 carry a prediction layer
(nextn_predict_layers 1, read from the first bytes of each GGUF; the 9B tags and North Mini
Code have none). The runtime then always passes --spec-type draft-mtp --spec-draft-n-max 4,
so llama.cpp keeps 4 rollback copies of the recurrent state (need_n_rs_seq(),
llama-memory-recurrent.cpp) and the prediction layer's cache in the draft context's cache
type, f16 by default. The rows counted one state and that layer at q8_0.
What moves (test/local-model-recommend.test.ts, "every memory size", re-pinned):
- 16 GB: limited -> none. MiMo 9B at 65,536 tokens needs 10.2 GB, 0.987 of the 10.4 GB
estimate. With the budget the Mac mini measured (10.7 GB after the reserve) it is offered,
0.954, tight; that test is kept. With 4 checkpoints instead of 8 it would be 0.966 on the
estimate: this size sits on the edge, and only a run on such a Mac settles it.
- 18 GB: the same pick, 0.964 instead of 0.928.
- 36 GB: the same picks, 0.966 (27B on M4/M5 Max) and 0.976 (35B-A3B GGUF on M3).
- 48 GB on a Max chip: the 27B at 262,144 tokens is tight (0.853) instead of good.
- 32 GB with a measured budget of 0.74: North Mini Code at 131,072 tokens (was 262,144), the
27B at 65,536 on a Max chip (was 131,072).
- Every MLX row, and everything from 64 GB up, is unchanged.
test/local-model-catalog.test.ts recomputes the new rule from the architecture table and holds
the catalogue to LLAMA_ARG_CTX_CHECKPOINTS. The catalogue header, runtime.ts, the types,
docs/local-models.md, CLAUDE.md and architecture-invariants say the same. Read in the source,
not measured.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The migration's file comment and the invariants said a restored settings.json
stays as restored and that a refused start leaves the migration to the next
start. Both hold, but the first only once the migration has run: after a
first 1.50.0 start that found settings.json unparseable, no marker exists,
so an off saved between the fix and the next start is switched on with the
rest. That is the same "cannot tell a chosen off from the old default" trade
the release note already states, in a narrower window. Both texts now say
so. Comment and docs only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JSDoc on `agentSkillEnabled` in SettingsUpdateSchema said the skill is
injected into `<case>/.claude/skills/` on Claude session create and is
"SYNCED, default OFF ... so it is opt-in". Since this branch it is on when
absent, goes into whichever directory the session's CLI scans for skills,
and a false stored by 1.49.0 or earlier is switched on once at the first
1.50.0 start. Comment only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>