Commit Graph
2792 Commits
Author SHA1 Message Date
Codeman maintainer 8dc81d0089 Merge #792: feat(files): delete a file from the Files panel (from #529) 2026-11-09 07:57:02 +01:00
Codeman maintainer 5e80c0a1ca Merge #791: feat(path-picker): pinch, pan and wheel zoom for the image preview (from #608) 2026-11-09 07:57:01 +01:00
Codeman maintainer 20cd2eccc9 Merge #790: fix(home): swap the home surface on a fold whatever width the page loaded at 2026-11-09 07:57:00 +01:00
Codeman maintainer 078bd74bd1 fix(files): the review's fixes for the Files panel delete
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>
2026-11-09 03:09:46 +01:00
shenlvkang-collab dfe1af2e5f feat(files): delete a file from the Files panel
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.
2026-11-09 03:09:46 +01:00
Codeman maintainer 572ff202bb fix(path-picker): finish the image preview zoom from #608
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>
2026-11-09 03:09:45 +01:00
shenlvkang-collab ba6bff388a feat(path-picker): pinch, pan and wheel zoom for the image preview
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.
2026-11-09 03:09:45 +01:00
Codeman maintainer f62c1f7e2f fix(local-model): a view read while the job ends no longer shows paused
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>
2026-11-09 02:51:07 +01:00
Codeman maintainer 7602b6db21 fix(home): swap the home surface on a fold whatever width the page loaded at
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>
2026-11-09 02:29:51 +01:00
Codeman maintainer 236fecf6b9 chore: version packages
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:11:06 +01:00
Codeman maintainer 8a54ded78b Merge #784: a local model catalogue for every Mac memory size, and its weekly check 2026-11-08 11:10:13 +01:00
Codeman maintainer 8fa2884f48 Merge #787: turn the Codeman agent skill on by default 2026-11-08 10:41:11 +01:00
Codeman maintainer 83fb523979 fix(local-model): rollback copies follow each tag's own draft length
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>
2026-11-08 10:38:12 +01:00
Codeman maintainer 2cd3cbd6a0 fix(local-model): count what llama-server keeps beside the K/V cache, and bound its checkpoints
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>
2026-11-08 10:26:57 +01:00
Codeman maintainer 235db69c1e fix(agent-skill): the migration's log line says it switched the setting on
It stores true rather than clearing the key, so the line now says what happened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 10:11:36 +01:00
Codeman maintainer cda4184e42 docs(agent-skill): say what the migration does after a refused first start
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>
2026-11-08 10:09:34 +01:00
Codeman maintainer f1cfe61950 docs(agent-skill): the settings schema comment still said default off
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>
2026-11-08 10:09:30 +01:00
Codeman maintainer bc9c3c1639 docs(local-model): two catalogue header claims made exact
The Q8 27B row counts a prediction layer its GGUF may or may not carry
(not checked, careful side), and the macOS notice on the runtime's page is
about Laguna XS 2.1; Laguna S 2.1 is set aside for its open runtime issues.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:39:48 +01:00
Codeman maintainer 97f10a1033 docs(local-model): the long-session caveat of the tested pair, and what the check proves
The 48 GB row now says the test was a short task and that MLX may keep up
to 8 GiB more in a long session, which brings the pair to the edge of the
budget. The check after the download says what it proves for each runner,
from the runtime's source: for a GGUF build both of its figures are the
buffers llama-server reports allocating, so the check covers the whole
load whatever the cache type; for an MLX build only the weights. And the
docs say where a new row's architecture goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:39:27 +01:00
Codeman maintainer edfc2ed667 docs(agent-skill): say what the setting UI test pins, not that it proves the deleted-key shape
The migration's file comment said test/agent-skill-setting-ui.test.ts proves
both shapes. It pins the shipped one (a stored true wins on a device holding the
old false); the deleted-key shape was measured with the same harness and is not
kept as a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:38:15 +01:00
Codeman maintainer 989fcd778a feat(local-model): a catalogue for every Mac from 8 to 512 GB, each row with its own figures
The catalogue had seven Qwen-family rows that all carried the memory
figures measured on one model (Qwen3.6 35B-A3B on the MLX runner), so a
9B or a dense 27B was judged with figures that do not describe it, two
rows had the wrong Mac download size, and every Mac below 32 GB got
"none".

Now eight rows, each with the runner a Mac really uses for its tag (a new
`runner` field: `mlx` or `llamacpp`) and memory figures of its own:

- the 35B-A3B MLX row keeps its measured figures and the tested pair;
- every llama.cpp row is computed from its model's config.json: a q8_0
  K/V cache for the whole context (what the runtime now sets), its
  recurrent or sliding-window state, 1.0 GB of runner and the 1 GiB host
  prompt cache;
- the one other MLX row (Ornith 1.5 122B) is computed the same way,
  scaled by what the measured row showed, plus the runner's 8 GiB of
  prefix snapshots.

Sizes are the registry's Mac pull totals (2026-11-08, lowercase ids
verified there). What each memory size gets: 8 GB none; 16 to 32 GB a 9B
(limited); 36 GB Qwen3.8 27B or the 35B-A3B as GGUF; 48 GB the tested
pair (or the 27B on a Max chip); 64 and 96 GB the 27B or the 35B-A3B;
128 to 512 GB Ornith 1.5 122B. Nothing but the one row was measured.

The catalogue test recomputes every computed figure from a table of
architectures and lets two rows share figures only when they share the
architecture and the runner. The rule tests of recommend() now run on a
frozen copy of the 2026-11-06 catalogue, so a weekly catalogue update
changes only the per-memory table and the real-Mac tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:33:48 +01:00
Codeman maintainer f60b63c005 feat(local-model): q8_0 K/V cache, flash attention and a bounded prompt cache for llama.cpp
runtimeEnv() now also sets OLLAMA_KV_CACHE_TYPE=q8_0, OLLAMA_FLASH_ATTENTION=1
and LLAMA_ARG_CACHE_RAM=1024. Read in the v0.40.15 source of the pinned
runtime: the first becomes --cache-type-k/-v of llama-server (17/32 of the
f16 cache), the second --flash-attn on (a quantized V cache needs it, and
"auto" or "off" would fail such a load), and llama-server inherits the third
from ollama serve's environment, which bounds its host prompt cache to
1 GiB instead of letting it grow by gigabytes in a session. The MLX runner
reads none of them.

The catalogue's llama.cpp rows are computed for these values, so the two
change together. Not yet measured on a Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:18:51 +01:00
Codeman maintainer 55e0f0e7e4 feat(agent-skill): switch a stored off from before 1.50.0 on, once per data dir
Every App Settings Save up to 1.49.0 wrote `agentSkillEnabled: false`, chosen
or not (the switch loaded an absent key as off and Save sends the whole form),
so the new default never reached an install that had ever saved. On its first
start the server now replaces a stored false with true, once per data dir, and
logs one line when it does. It cannot tell a deliberate off from the old
default; that is accepted and stated in the release notes. An off set after it
ran stays off.

It stores true rather than deleting the key: the page's merge overwrites only
the synced keys the server has, so every device that saved on 1.49 kept its own
false, showed the switch off and wrote false back on its next Save (measured in
the JSDOM harness). True wins on every device, including a browser still on a
cached settings-ui.js. For a server with no key the page now drops the device's
copy as well, so the switch shows the default.

The "done" marker is its own file, `agent-skill-default-migrated.json` in the
data dir: it is not a setting, so GET /api/settings never returns it, no Save
can send it back (the strict schema would refuse it) or drop it (which would
re-arm the migration), and a config-backup restore leaves it alone. It is
checked before anything is read and written only after the settings write
landed. The migration runs in WebServer.start() before the first settings read
and before listen (skipped in test mode, like the other boot-time writes),
writes only through mutateSettingsFile(), drops the 2 s settings cache, and
leaves a settings.json that does not parse alone with no marker, so the next
start after the fix runs it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:15:53 +01:00
Codeman maintainer 54c02990ba Revert the skill default flip on this branch: it ships in its own pull request
This reverts commit 0aa34da8, which made the codeman agent skill default on here as well. That change lives in feat/agent-skill-default-on, together with the self-ignoring copy and the reference-folder symlink check it needs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 09:00:59 +01:00
Codeman maintainer cd2265f8d0 docs(agent-skill): name GitHub Copilot among the CLIs that get no skill copy
Copilot's registry entry takes no agent skill, like Pi and a plain shell, but
the changeset, the App Settings help text, the Settings Reference and
CLAUDE.md named only Pi (and the shell). The Agent CLIs wiki page still said
no CLI but Claude gets the bundled skill; every CLI except Pi and Copilot
gets it, in `.agents/skills/`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:35:38 +01:00
Codeman maintainer ddcdd5d974 fix(agent-skill): refuse a symlinked reference folder inside the skill copy
Install and uninstall checked only the copy and its parent for symlinks, so a
`reference` folder that is a link carried every write and unlink of
`reference/<name>.md` to wherever it pointed. Measured: install overwrote a
`history.md` outside the workspace and uninstall deleted it. A cloned
repository can commit that link, and a Docker case's container can plant it
in the bind-mounted workspace the host injects into. With the skill on by
default, both now ran without the user asking.

Both paths now return 'symlink' for a linked folder inside the copy, before
the first write or unlink, and the refusal messages say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:33:02 +01:00
Codeman maintainer 0aa34da80f feat(settings): the codeman agent skill is on by default
agentSkillEnabled now reads an absent key as ON, the way
workspaceHooksEnabled does: only an explicit false turns it off. A new
session therefore finds the codeman skill in its folder without a
setting first, and with it the verbs that hand work to other CLIs and to
the local model on this Mac. Settings shows the switch on for the same
absent key.

The injection itself is unchanged: add-only, marker-owned, never through
a symlink, never for remote sessions or a plain shell. Turning the
setting off still removes nothing already written.

The quick-start test now pins the new default (injected on an absent
key, nothing after an explicit false), and the docs that said "default
off" say when that changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 08:13:51 +01:00
Codeman maintainer b3f0ba9e29 feat(agent-skill): turn the Codeman agent skill on by default
An absent `agentSkillEnabled` now means on, on the server
(`getAgentSkillEnabled()` reads `!== false`, like `workspaceHooksEnabled`)
and on the page (the App Settings switch loads `!== false`, so a Save of any
other setting no longer writes the old default back). Only an explicit false
turns it off. A partial settings PUT that omits the key leaves it as stored,
since the route merges the request into the file.

The switch's help text says it is on by default, that the copy stays out of
git, and that turning it off removes no copy already written. The shared
route-test mock now defaults to on too, so route tests see what a user sees.

Tests: quick-start against a real server (absent key injects, a partial PUT
keeps it on, an explicit false stays off through a later partial PUT), the
real openAppSettings/saveAppSettings in JSDOM (absent shows on, explicit false
shows off, a Save on a never-touched install sends true and passes the
schema), and the plain create route at the mock's default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:42:56 +01:00
Codeman maintainer ba9d311cb7 feat(agent-skill): make the injected skill copy ignore itself in git
Every Codeman-managed copy of the agent skill (`.claude/skills/codeman/`,
`.agents/skills/codeman/`, and the user-level copy) now carries a `.gitignore`
of `*`, written once with O_EXCL before any skill file, the way the prompt
uploads folder does. A user's repository no longer lists the copy as
untracked, and Codeman never touches the repository's own ignore files.

A file already at that name is never overwritten, a copy injected before this
gains the file on its next install (reported as refreshed), and uninstall
removes it only when it is the last thing left and holds exactly `*`, so a
user's extra files in the folder stay ignored. Claude Code 2.1.319 runs
`git check-ignore` only on directories at or above `.claude/skills`, never on
a skill folder, so the skill still loads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:35:03 +01:00
Codeman maintainer 028bd87494 fix(files): no outside worktree for an adopted session, and say what the routes re-check (#780)
outsideWorktreeOptions() now returns undefined for an adopted session as it does for a remote or Docker one: session.remote and session.docker are undefined on an adopted session, so it needs its own test. An adopted pane gets no paneCwd today (paneExitApplies), so nothing changes in practice; the rule stays true if that ever changes.

The live-worktree.ts @fileoverview said the routes re-check only the realpath of an outside worktree. They also judge the attachment guard rules again on every request (isOutsideWorktreeRootRefused() through withGuardedOutsideWorktree()), which is what makes a confinement or blocked-tree change apply at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:08:43 +01:00
Codeman maintainer e8a63ede00 Merge feat/local-model-recommendation: recommend and install a local coding model on this Mac (experimental)
Conflicts resolved as on test/desktop-macbook: config/test-suites.ts keeps both browser tests (desktop-connect-page and local-model-ui), and the CLAUDE.md state-files line keeps #778's cli-installed entry with the local model's local-model.json and local-model/ entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:06:46 +01:00
Codeman maintainer 2522ac1727 Merge chore/remove-ralph-start-button: remove the "Start Ralph Loop" button from the start screen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:06:01 +01:00
Codeman maintainer 2f9cc7aabc Merge #780 feat(files): follow the agent into a worktree outside the session folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:05:56 +01:00
Codeman maintainer e6c79c8e8c Merge #779 fix(macos): never run Apple's placeholder git, which opens the developer tools window
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:05:49 +01:00
Codeman maintainer 98c4a3d386 Merge #778 fix(cli): a running server notices a CLI the installer just added
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:05:41 +01:00
Codeman maintainer 49e1dda23b Merge #782 fix(approvals): the list leaves a dialog too new to judge
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:04:59 +01:00
Codeman maintainer a192fb7253 Merge #763 feat(desktop): native macOS app that runs, installs or connects to Codeman
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:04:57 +01:00
Codeman maintainer 324aa75896 fix(approvals): the list leaves a dialog too new to judge
`GET /api/approvals` re-reads the pane of every pending dialog and resolves
one whose frame shows no dialog, either because its options vanished or
because the frame shows a turn running. `notePrompt` deliberately waits 3 s
before making that same judgement, since the Notification hook can land
before Ink paints the dialog and the working line is still on screen then.
The list did not wait. That rarely mattered while it ran once per page load,
but a client that polls it would resolve a live prompt as answered in the
terminal, clearing the red alert on every device and the push Approve button
with it. `sweepForList` skips a dialog younger than the delay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:21:37 +01:00
Codeman maintainer be8fc2980a style(local-model): white for the feature, built like the primary launcher
The violet button was the one thing on the start screen outside the app's
blue, and it carried white text and a see-through disc where its neighbour
has dark ink and a solid disc. It is now white with dark ink and a solid
disc, the same on the Install button, and the chooser's selected row, tile
and model bar follow. Light skins turn it over to dark, since a white button
vanishes on a pale page. The context takes the app's blue.

The tokens are named for what they mean now (--lm-brand, --lm-context).

Brings test/local-model-ui.browser.test.ts up to the redesign it had fallen
behind: it is outside the gate, so the button's 46 px, the 12 px bar and the
memory panel's edges had gone unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 04:23:02 +01:00
Codeman maintainer aa76b1b304 style(local-model): rose for the brief peaks in the memory map
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:44:19 +01:00
Codeman maintainer 6b1664abae docs(local-model): the memory map in the frontend rules
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:22:25 +01:00
Codeman maintainer eb91279a3b feat(local-model): a memory map and real colour in the chooser, the button first
The owner's verdict on the chooser: nice, but not nice enough, too much just
blue. He also wants "Run a local model" on top of the start screen, above
Run Claude Code.

The memory bar is now a map: the model, its context and the brief peaks side
by side on the track of what this Mac can give a model, each with its size in
a legend, and the verdict on the fit beside the figure. recommend() sends what
the need is made of as `Candidate.memory`.

Colour now carries meaning: green for good and tested, amber for tight, orange
for slow, teal for the context. Violet is the feature's own colour, on its
start screen button and its main action. Every row has a size tile and a speed
gauge scaled to the fastest row in the list. The light skins get deeper values
of the same colours, and the window is 40 px taller to hold it without
scrolling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 03:21:35 +01:00
Codeman maintainer 04f232f239 feat(local-model): the start screen's local model button in the size of the main one
The owner asked for it much bigger and in the middle. The button keeps the
chip's markup and takes the primary button's height, radius and weight, with
an accent outline and its glyph in a disc, so a model on this Mac is offered
as a main way in rather than as a footnote under the launchers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-07 00:04:57 +01:00
Codeman maintainer 0a8ab84ea1 docs(local-model): name every covered launch path, both install executors, and the port without a login
The runner header says what each caller passes (the job: the compiled stock
command; the route: the merged entry, behind cliManagementEnabled).
docs/cli-registry.md names both executors in its trust boundary. The
security architecture gains a section on the pinned runtime and its hash,
the loopback port without a login and the digest check, and the install
path without cliManagementEnabled; SECURITY.md lists the port as a known
limitation. The guides and the rules name exactly the paths the launch
guard covers, the session that keeps calling a stopped runtime's port, the
claim before the connect writes, and the outcome kept in memory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 23:55:02 +01:00
Codeman maintainer 77d29d4621 feat(local-model): one frame for the window, honest figures, a table for the consent
The window is one frame for the whole flow: a fixed height on desktop, App
Settings' boxed close button, the buttons in a foot of their own outside
the part that scrolls, so a click never moves the next button away and
nothing is cut off; the short screens are centred in it on desktop and sit
above the buttons on a phone.

The chooser lists the pick first and then the other models by download
size, largest first, a column the reader can see. A pick that only just
fits says "Best fit, tight". The memory figure is an estimate and says so
once, in whole GB; its legend names the peak, and the installed line uses
the same words beside what the model takes now, so the two figures are not
read as a contradiction. The legend, the column headers and the foot's
promise are set in --text-dim, the bar's track in --text-muted so its free
part can be seen, and only the download figure keeps tabular figures.

The consent screen is the download as a table: the chosen row, the runtime,
the coding agent with its installer's command as the row's second line, the
total in the strong weight, then where it goes in one line. The download bar
means the model download alone and is gone once the model is in; the screen
that ends an install shows "Start a session" alone; an error on an installed
model is the headline with a grey dot; an error's first sentence is the
headline; Remove is quiet everywhere but red in its own question. The
no-recommendation reasons are one sentence each, and the untested note is
in Codeman's voice. A refusal the server names is printed from its sentence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 23:55:02 +01:00
Codeman maintainer 3c4bb91d6f fix(local-model): ask the launch guard before every start of an agent on the managed endpoint
Only the two session routes asked launchRefusalMessage(). The attach that
finds no pane, the respawn of a dead pane (a click on the tab, a respawn
start, the boot recovery), restartCli() and the reboot-restore rebuild
started an agent on the endpoint unasked. Session gains setLaunchGuard(),
asked right before a NEW agent process is started and never for an attach to
an agent that runs; WebServer.setupSessionListeners() sets it for every
session; a refusal is a LaunchRefusedError that never falls back to a direct
PTY. The reboot-restore route asks before it builds and skips the entry with
its own reason, left on offer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 23:54:46 +01:00
Codeman maintainer fa978008d2 fix(local-model): claim the connection first, keep an outcome the disk refused, say why a start failed
The connect step records the connection in the state file BEFORE it writes
the endpoint record and the settings flag (the flag's earlier value read by
inspectLocalEndpoint()), so a state write that fails or a server killed
between the two never leaves a record of ours that the state file does not
know; a record under the reserved id that says exactly what ours would say
is ours. changePort() writes the state first and puts the old port back when
the endpoint move is refused.

A job whose end could not be written (a full disk) is kept in memory: the
view shows it, Resume and a new install start from it, the next verb writes
it, and Cancel never answers a 500. A first install on a full disk is a 507
with the sentence. "installing" in the file while this process runs no job
always reads as paused or that failure.

An installed model whose runtime does not start says so (runtime-start-failed
with Start as the next step, and the launch check says it too); the stored
runtime version is started while its folder is unpacked. Stop, Remove and
the boot of anything that is not a running installation end a runtime an
earlier server left behind before any file is deleted. Stop and "Use another
port" answer 409 sessions-open while a live session uses the model. A pull
that failed because the runtime is gone is runtime-stopped, not a network
problem. After "does not fit" only a model that needs less memory is offered.
A replaced job's partial blobs are deleted. Every 400 carries its sentence as
data.message, and the six inline sentences are labels with zh-CN entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 23:54:45 +01:00
Codeman maintainer 6319fffce1 fix(local-model): end a runtime an earlier server left behind, and keep its version
The supervisor reports the child's pid the moment it is spawned, before it
answers, so a server killed during a start still leaves a pid that names the
runtime as ours. stop() takes that stored pid (previousPid) and ends a
runtime an earlier server left behind when its command line is a binary
under our runtime folder, so no caller deletes the folder under a live
process. start() takes a version: an installed model keeps the runtime
version it was installed with when a later build pins another one.

removeLocalModelFiles() refuses a local-model folder that is itself a
symbolic link (every check compared against the real path of the root, so
a link in its place passed whatever it pointed at). removePartialModelBlobs()
deletes what an interrupted download left in the model store, which the
runtime answers "not found" for and never deletes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 23:54:45 +01:00
Codeman maintainer 84d5c691ff feat(local-model): one button and a chooser window replace the welcome card
The read-only card is gone. The welcome screen has one button, "Run a local
model" ("Local model" once one is installed), and App Settings > Models has
one row; both open the same window.

The window follows `install.state`: choose a model (a radio group, with a
memory bar filled to what the selected model needs), confirm the sizes and
the folder, watch the download and the steps, and end on "Start a session".
Paused, failed, installed (running or stopped), Remove and "no model fits"
are screens of the same window. It polls the GET once a second while open.

Every dynamic string goes in through textContent, labels and sentences come
from the payload, no CLI id is compared, and every new string has a zh-CN
entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 22:16:38 +01:00
Codeman maintainer 3af94df4f6 feat(local-model): install a model from the server, with a job that can pause
POST /api/local-model/install, cancel, resume and runtime, and DELETE
/api/local-model. The GET gains an `install` object the window polls. All of
it is experimental and admin only in multi-user mode.

One job of five steps (runtime, model, verify, agent, connect) driven by a
pure reducer over explicit state, step and transition tables. One job at a
time (409). The consent is bound to the size shown: `acknowledgedBytes` must
be the server's own total (409 size-changed). A job a server restart
interrupted comes back paused and never continues without a click. Every
failure is a code with one next step.

The model is judged against the budget the runtime measured before it is
downloaded. A missing coding agent is installed with its stock command
through the shared runner, audited. Connect writes one endpoint record under
the reserved id through mutateCustomModelHosts() and switches
customModelEndpointsEnabled on through mutateSettingsFile(), remembering the
flag's value. The model's digest is stored and read again at boot and before
every session start on that endpoint; a changed model refuses the launch.

Remove answers 409 with the session names while a live session uses the
endpoint, undoes the two writes first, restores the flag only when this
feature switched it on and no other endpoint is saved, and deletes only
inside local-model/. The coding agent stays.

The server starts the runtime at boot when a model is installed and stops
it at shutdown beside stopDeepSeekWeb().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 22:16:38 +01:00