Commit Graph
2214 Commits
Author SHA1 Message Date
Codeman maintainer c4b74415ee chore: sync CLAUDE.md version to 1.28.1
COM step 4. Staged as a single hunk: the shared checkout also holds another
session's in-progress pr-bot discussions work in this file, which is left
untouched and uncommitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codeman@1.28.1
2026-09-14 13:17:53 +02:00
github-actions[bot]andgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> d8a9e2f2bb chore: version packages (#414)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-09-14 13:17:30 +02:00
Codeman maintainer 708cb2cbf0 fix(tabs): let a wrapped desktop tab strip grow the header instead of clipping itself
The fixed 120px/96px caps on the two wrapped layouts were row counts in disguise: a
third row was clipped into a ~4px scroller, hiding tabs inside a container nothing
invites you to scroll, while the header had the page below it to grow into. Both
layouts now share one rule capped at var(--tab-strip-max-height, 40vh), a safety net
for an absurd session count rather than a row limit.

Verified before shipping: .header is min-height + flex-shrink: 0 so it can grow, and
terminal-ui's ResizeObserver refits the terminal when it does; updateTabOverflowMode()
returns early for any non-desktop viewport, and below 1024px mobile.css pins the header
to max-height: 48px, so this is desktop-only in effect; the selector is comma-grouped
rather than :is(), so each arm keeps (0,2,0) and mobile.css's overrides still win on
source order. PostCSS parses the file cleanly (prettier ignores styles.css).

Authored in a parallel session against this shared checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 13:08:57 +02:00
github-actions[bot]andgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 90ac13da1a chore: version packages (#412)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
codeman@1.28.0
2026-09-14 12:59:48 +02:00
Codeman maintainer 48f30f3055 style: drop em-dashes from the text added in c2114615
House style, and these land in the changelog. Only the sentences added in the
previous commit are touched; the em-dashes in contributor text and in the
pre-existing COD-54/COD-115 comments are left alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 12:46:24 +02:00
Codeman maintainer c211461500 fix: merge-time follow-ups for #409, #404 and #399
#409 (Claude truecolor). The changeset becomes the changelog, and its premise
does not hold on tmux 3.2 or newer. Measured here on tmux 3.4: `default-terminal`
sits at its compiled default of `tmux-256color`, a live claude pane reports
`TERM=tmux-256color`, and supports-color reads that as 256 colors, where
rgb(55,55,55) lands on ESC[48;5;237m — visible, just not the color the theme
named. The invisible block the PR describes needs TERM to resolve to a 16-color
entry: tmux older than 3.2, or a ~/.tmux.conf setting `default-terminal screen`,
which Codeman's own tmux server does read (it passes no -f). Both the changeset
and the invariants paragraph now say that, so the next report here gets paired
with the reporter's tmux -V instead of being read as universal. The change itself
stands on the simpler argument: claude was one of two entries not asking for
truecolor while twelve do.

Also reorders buildClaudeEnv(). It applied the registry's unset/exports AFTER the
whole env was built, so a clis.json entry naming CODEMAN_HOOK_SECRET_FILE or PATH
would strip it on the direct-PTY path while the tmux pane kept it — buildEnvExports()
emits `...cliEnv` ahead of `export CODEMAN_MUX=1` and cannot. The block now runs
first and Codeman's own keys are assigned on top, matching the pane.

#404 (Ctrl+Z trap). Adds the missing changeset, and records what the trap does
not cover: an agent CLI already holds its tty with ISIG off (verified on three
live panes: `susp = ^Z -isig -icanon`), so this is defence for the startup window
rather than a fix for the steady state, and two input paths still reach the PTY
unfiltered — the mobile accessory bar's one-shot Ctrl and the CJK textarea.

#399 (path picker sort). The server sorts by name and cuts at 500, so the client
sorting those 500 by date gives "the newest of the first 500 by name", which is
wrong in exactly the >500-entry folder the date sort exists for. The status line
now says "(first 500 by name)" so the cut is legible, with the reasoning parked
on _sortEntries.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 12:45:41 +02:00
Ark0N 37929cb671 Merge pull request #404 from timkjr/pr-ctrlz-suspend-trap
fix(terminal): trap Ctrl+Z in non-shell sessions to prevent accidental suspend
2026-09-14 12:35:33 +02:00
Ark0N e0ebbbdc91 Merge pull request #409 from irisitymichaelgrundberg/fix/claude-truecolor-in-panes
fix(terminal): let Claude use truecolor so its themed backgrounds render
2026-09-14 12:35:28 +02:00
Ark0N 8c237223b0 Merge pull request #399 from shenlvkang-collab/pr/path-picker-sort-jump
feat(files): let the path picker jump to a typed path and sort by name or date
2026-09-14 12:35:23 +02:00
Michael GrundbergandClaude Opus 5 dae2ac580f fix(terminal): read the colour env from the registry on every local spawn path
buildClaudeEnv(), the direct-PTY fallback taken when mux creation fails, now
reads getCli('claude').env and applies its unset and exports lists. It used to
delete COLORTERM and CLAUDECODE from a hand-maintained list of its own, which
left it contradicting the registry entry that the tmux pane and the attach
client both read. An engine value needing a mux name has nothing to resolve
against on this path, so it is skipped rather than guessed.

Claude no longer unsets NO_COLOR. The invisible-background bug does not need
it, and unsetting it overrides a preference the user set deliberately, so a
user who exports NO_COLOR globally keeps monochrome panes. The other seven
truecolor CLIs still unset it; that inconsistency is intentional and the
comment on the entry says so.

The invariants doc gains a Terminal colour env paragraph under Session launch
modes, where a reader looking up Claude will find it — the previous sentence
sat under a heading that lists only the non-Claude CLIs. It now says the lists
are the stock catalog and a clis.json override replaces them wholesale, and
that the declarations reach the tmux pane, its attach client and the direct
PTY but not a remote pane, whose command carries no env exports at all. Docker
hands COLORTERM=truecolor to every mode, including the two the registry says
must unset it.

The changeset named six peer CLIs and there are seven: deepseek also exports
truecolor. A test beside the existing OpenCode assertion pins the new
behaviour, so a future registry edit cannot make the backgrounds vanish again
in silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 14:42:30 +02:00
Michael GrundbergandClaude Opus 5 7767b16d4f fix(terminal): let Claude use truecolor so its themed backgrounds render
Claude draws the user's own messages as a block of background color, and
inside a Codeman pane that block was invisible. tmux hands each pane
TERM=screen, which supports-color reads as 16 colors, and Claude's registry
entry deleted COLORTERM on top of that. Claude therefore quantized every RGB
color its theme asked for down to the basic palette, where rgb(55, 55, 55)
and every other dark background becomes ESC[40m, the terminal's own black.
Changing the color in a custom Claude theme moved nothing on screen.

Claude now exports COLORTERM=truecolor and unsets NO_COLOR, matching codex,
gemini, antigravity, pi, grok and omp. CLAUDECODE stays unset, because Claude
reads it as a signal that it is running nested inside itself. Both the tmux
session and the attach client read this one registry entry, so they cannot
disagree.

PR #3 introduced the unset in February, citing xterm.js#484 for the claim
that xterm.js mishandles truecolor. xterm.js closed that issue in April 2019,
Codeman now depends on @xterm/xterm 6, and TmuxManager already sets
terminal-overrides ",*:Tc" on its own tmux server, so 24-bit color reaches
the browser today for every CLI that asks for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-13 12:15:28 +02:00
DevvynandClaude Sonnet 5 a0628a40e8 fix(cli-registry): address maintainer review on #380
Rebased onto current master (the one real conflict was the import line
in docker-hosts.ts Ark0N flagged; kept both), then addressed every
point from the review:

**1. Rebase.** Done — this branch now sits on current upstream/master.

**2. Agent-image special cases are data now, not an id-keyed table
outside stock.ts.** `AGENT_IMAGE_SPECIAL_CASE_IDS`/`AGENT_IMAGE_SPECIAL_CASES`
are gone. `CliDiscovery.install.agentImageLayer?: { kind: 'dedicated';
reason: string }` is a field on the registry entry itself (pi,
deepseek), `reason` is required by schema.ts, both producers
(docker-hosts.ts and cli-catalog.mjs) filter on its presence instead
of an id, and the coverage test reads it from the generated catalogue.
Also added the npm-package-name validation to the TS producer, which
only the .mjs one had — same SAFE_PACKAGE regex, duplicated
(necessarily, one side can't import the other) and now pinned
byte-identical by a new parity test.

**3. Changeset said five, it's eight.** (Not nine — see the DeepSeek
point below, which changes the true count.) Reworded to state it
structurally rather than pin a number that will go stale again.

Then the four behavior-changing findings:

- **DeepSeek was offered as a normal install option but can't actually
  drive a pane.** `npm install -g @deepseek-ai/dsh` installs the
  launcher only; DeepSeek ships no profile that can run standalone.
  The generator now emits an empty install command for any
  `launcherProfile` entry, so install.sh's menu (which requires a
  non-empty command) skips it and falls through to its docs URL hint
  instead — matching what the old hand-written code did before this
  PR replaced it.
- **wget-only hosts lost every automatic install, including the npm
  ones that never needed curl.** The menu-building loop now filters
  PER ENTRY (only a command starting with `curl ` is held back) rather
  than wiping the whole menu when DOWNLOADER != curl.
- **The DISPLAY/TRUSTED split and the catalogue refresh didn't hold up
  under review** (refresh's only real write was the label; it ran
  before the Node existence check; its own eval-detection test was
  tripped by the word "eval'd" in a comment). Dropped entirely per
  your own recommendation — embedded catalogue only, no network
  fetch, no second array. install-sh-invariants.test.ts now asserts
  the refresh/DISPLAY machinery does not exist rather than testing its
  internals.

The three take-or-leave items, applied:

- `dsh_banner_probe`'s bash 3.2 empty-array bug: `${runner[@]}` →
  `${runner[@]+"${runner[@]}"}`. Verified live in a real `bash:3.2.57`
  container with `timeout` removed from PATH — crashed before, clean
  now, full `detect_all_clis` path exercised end to end.
- `docker-agent-image-coverage.test.ts` now anchors on each layer's
  `<binary> --version` proof line instead of `Dockerfile.includes(binary)`,
  which stayed true if a layer were deleted but its comment survived.
- Doc drift: docs/docker-cases.md (four → five, and now describes the
  data field), docker/agent.Dockerfile's "other four CLIs" comment (no
  longer a magic number — CLI_NPM_PACKAGES is generated and can grow),
  CLAUDE.md's install.sh size (104KB → ~112KB) and its stale mention of
  the now-dropped refresh.

Verified: tsc clean, prettier clean, the full targeted suite (142
tests across the 8 affected files) green, and the full `npm test` gate
diffed BY TEST NAME against a clean upstream/master baseline run on
this same machine — identical 201-name failure set both sides (168
tests / 67 files, all pre-existing Windows-environment noise: symlinks,
PTY spawning, POSIX permission bits — none of it touching anything
this PR changes), zero new failures either side of the diff.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
2026-09-13 17:43:14 +08:00
DevvynandClaude Opus 5 c5c015d648 docs(cli-registry): document the catalogue's consumers and the trust boundary
Adds a "Consumers outside the server" section covering the two generated
artifacts, why each exists (neither install.sh nor a .mjs can import
TypeScript), what is deliberately NOT exported and why, the three-rule install
command trust boundary, and the bash 3.2 constraint with the offset/length
window shape it forces.

The adding-a-CLI checklist gains the regenerate step, since forgetting it is how
the installer would keep detecting the old set while the server offers the new
one — the drift this change removes, one level out.

docs/docker-cases.md gains how CLI_NPM_PACKAGES is derived, why it reads the
stock catalogue and not the merged registry, and a table of the four documented
Dockerfile special cases with their reasons. CLAUDE.md gains a command row and
names the generated block, the bash 3.2 rule and the trust boundary in its
install.sh paragraph.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:14 +08:00
DevvynandClaude Opus 5 7af4dbc0f8 feat(docker): derive the agent image's npm CLI list from the catalogue
docker/agent.Dockerfile hardcoded the four npm-published CLIs it installs, one
of the several lists that had to be kept in step with the registry by hand.

It now takes them as `ARG CLI_NPM_PACKAGES`, supplied by
scripts/build-agent-image.mjs from config/clis.stock.json, with the default set
to today's list so a bare `docker build` still produces the same image. The arg
is expanded unquoted because word splitting is what turns the list into several
arguments, which is exactly why every token is validated against
^[@A-Za-z0-9][@A-Za-z0-9/._-]*$ on the producing side; a package name carrying a
space or a metacharacter is refused rather than reaching the RUN line. Verified
by building the layer: four packages in, four arguments out, and the default
still applies with no arg.

The list is filtered on each entry's `enabled` flag — the field whose absence
was the maintainer's §3 finding, where a CLI shipping disabled still got baked
into every image. No stock entry is disabled today, so that assertion would pass
vacuously; a unit test feeds the pure helper a fabricated disabled entry so the
fix is covered now rather than the first time someone ships one.

⚠️ It reads the STOCK catalogue, never the merged registry. A user's
~/.codeman/clis.json must not change what is inside an image tagged
codeman/agent:base, or two machines holding that tag hold different images.

Four CLIs keep hand-written layers because the registry cannot describe what
makes them special: pi's --ignore-scripts, deepseek's pnpm companion and dsh-tui
profile, and the three standalone installers. Rather than extend the schema for
a Docker-only benefit, the coverage test requires each to carry a written reason
AND still be present, so an exclusion cannot quietly become an omission.

There are two producers of this command line and there have to be — the .mjs
cannot import TypeScript, and src/docker-hosts.ts builds the same argv for the
in-app auto-build — so a parity test pins them together, package list, arg pairs
and rendered argv. Their order is pinned too: a different order is a different
RUN string and so a needless cache miss between the two build paths.

docker/server.Dockerfile is deliberately NOT edited (PRs #373 and #377 both
modify it); its narrower list is asserted as a declared omission list instead, so
the divergence is reviewable without touching the file.

Also fixes the in-app hint at index.html, which the new coverage test caught
still omitting omp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00
DevvynandClaude Opus 5 1ca35095e7 refactor(install): drive CLI detection, the install menu and hints from the catalogue
install.sh carried nine search-path arrays, eighteen near-identical
check_<cli>/get_<cli>_path functions, and three separately hand-maintained
enumerations of all nine CLIs. They had to agree and did not: upstream b6d0f1fa
is "wire OMP into install.sh's CLI detection (it had none)", and the section
comment above the roll-call named six of the nine.

All of it now reads the generated catalogue. `detect_all_clis` resolves every
CLI in one memoized pass into CLI_FOUND_PATH/CLI_FOUND_COUNT; `check_cli` and
`get_cli_path` replace the eighteen pairs; the roll-call, the "no AI CLI found"
gate and the closing reminder become loops. Probe order per CLI is unchanged and
`test/install-sh-detection-parity.test.ts` proves it against the literals
transcribed from the arrays this deletes.

Behaviour changes worth naming:

- The install menu is built from the catalogue, so it offers every enabled CLI
  that is not installed and ships a command — five instead of two. Gemini had a
  command in the registry and appeared in NO list in this script.
- Its labels are now the registry's ("Claude" rather than "Claude Code"), the
  same trade PR A made for `codeman doctor` rows. A suffix map would just be the
  hand-maintained list again.
- On a wget-only host the menu prints commands instead of running them. The
  registry's commands call curl, whereas the two literals this replaces went
  through download_to_stdout; rewriting curl to wget inside a string we are
  about to execute is the wrong instinct.

The trust boundary is mechanical, not a promise: CLI_INSTALL_CMD_TRUSTED is
written only from the generated per-platform arrays and is the only thing ever
executed; CLI_INSTALL_CMD_DISPLAY is what the optional, opt-in refresh may
rewrite. The refresh warns on all three failure shapes — empty body, unparseable
content, failed fetch — which is the silent-degradation bug from the review, and
it parses with node into tab-separated records read by `read`, never eval.

Bash 3.2 throughout (macOS ships it): parallel indexed arrays, offset/length
windows instead of delimiters, no associative arrays, namerefs, mapfile or
here-strings. Verified by executing the script under a real bash 3.2 container,
which is also now a CI step alongside `bash -n` and a catalogue `--check` — the
empty-window case (`shell` has no binaries) is a runtime `set -u` abort that
`bash -n` cannot see. Running it that way caught `detect_os` being called inside
the platform loop: ten forks, and ten copies of one error, since a `die` inside
`$( )` can only exit the subshell.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00
DevvynandClaude Opus 5 7d6f612ef5 feat(cli-registry): generate a CLI catalogue for install.sh and the Docker build
Two consumers of the registry cannot import TypeScript: `install.sh`, which runs
via `curl | bash` before any checkout exists, and `scripts/build-agent-image.mjs`.
Both currently hand-maintain their own CLI lists, and both have already drifted.

`scripts/generate-cli-catalog.mts` (`npm run generate:cli-catalog`, plus a
`--check` mode) emits from `STOCK_CLIS`:

- `config/clis.stock.json` for the `.mjs` and the tests. It carries `enabled` —
  the field the earlier attempt omitted, which is how a disabled CLI's npm
  package still got baked into every agent image.
- a marker-delimited block inside `install.sh`, embedded rather than fetched.
  The embedded copy is the FULL catalogue on purpose: the earlier design fetched
  it and fell back to a hardcoded two-CLI list, degrading silently on an empty
  response. There is no degraded mode to fall into now.

The block is bash 3.2 safe: parallel indexed arrays, no associative arrays, no
namerefs, no mapfile. Variable-length lists use OFFSET/LENGTH windows into one
flat array rather than a delimiter, so a $HOME containing a space needs no IFS
handling and `shell` (no binaries) gets length 0 and is never iterated. Search
paths are emitted dir-major, matching the probe order the hand-written arrays
use and `test/install-sh-detection-parity.test.ts` pins.

Only fields the two consumers need are exported. `launch`/`env`/`capabilities`/
`overlays` are spawn-time concerns the server alone interprets, and a test
asserts they never leak into the artifact.

`main()` sits behind an `isMainModule()` guard so the sync test can import the
renderers. Without it, importing the module would rewrite the artifacts as a
side effect of checking them — passing always, guarding never.

This commit adds the block; it does not yet delete the hand-written arrays, so
the detection pin keeps measuring both against each other.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00
DevvynandClaude Opus 5 84f71e5704 test(install): pin install.sh's CLI detection paths before generating them
PR B replaces nine hand-written `*_SEARCH_PATHS` arrays in install.sh with one
block generated from `STOCK_CLIS`. This lands FIRST, against the hand-written
arrays, so the replacement has something to be measured against.

The arrays are not uniform, which is why "generate them from the registry" is a
claim rather than an obvious truth: claude alone has `~/.claude/local`, opencode
alone has `~/go/bin`, opencode/codex/gemini/pi/omp carry `~/.bun/bin` while
dsh/grok/agy do not, and omp's `~/.omp/bin` sits second rather than first. A
generated list that silently narrows leaves a user with that CLI installed being
told no AI CLI was found — upstream `b6d0f1fa` is that bug, fixed for omp by
hand after it shipped.

The test asserts a three-way identity: the pinned literals equal what install.sh
contains today, AND equal `searchDirs x binaries` from the registry, dir-major so
the probe ORDER is pinned too and not just the set. Both halves were verified to
fail independently — dropping one path from install.sh fails the first, changing
one `searchDirs` entry fails the second — because a pin that cannot fail is
worse than no pin. A fourth case asserts every stock CLI with a binary is
covered, which is the omp bug restated so it cannot recur silently.

No production code changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015EMxQreQUZX5ZyybxAGh12
2026-09-13 17:43:13 +08:00
DevvynandClaude Sonnet 5 b6f75b87f5 fix(custom-model): don't clamp DEEPSEEK_API_KEY as a privileged env key
CI caught a real regression: DEEPSEEK_API_KEY was added to deepseek's
privilegedEnvKeys alongside DEEPSEEK_BASE_URL on the theory that "the pair
travels together," but that contradicts the documented and tested design
(clampEnvOverridesForOwner()'s own docstring in session-routes.ts) — a
non-granted owner supplying their OWN DeepSeek key removes privilege
rather than granting it, since the exfiltration vector is the BASE URL
(which redirects the server's own forwarded key to a foreign host), not
the key itself. Removed it from the list; test/deepseek-mode.test.ts's
existing two clamp tests now pass again.

Also swapped that test's "unrelated override" example off CODEX_HOME,
which the earlier commit in this same PR legitimately made privileged
(closing a real pre-existing gap, documented in PR.md) — so it stopped
being a valid "unrelated" example the moment that fix landed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
2026-09-13 17:42:35 +08:00
DevvynandClaude Sonnet 5 e18499aa67 docs(pr): drop the draft/WIP framing now that the PR is submitted
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
2026-09-13 17:42:35 +08:00
DevvynandClaude Sonnet 5 61779745aa test(custom-model): make the harness smoke test dynamic, verify all 9 CLIs end-to-end
Rewrites scripts/test-local-llm-harnesses.mjs -> .ts to read the live CLI
registry (enabledClis()) and call the real production
buildCustomModelInjection()/applyConfigDirInjection() instead of keeping a
second hand-maintained copy of every CLI's env/config shape. A future
registry change (new CLI, edited env var, fixed config template) is now
picked up automatically with zero edits to this script; only the one-shot
invocation flags (info the registry genuinely doesn't model) stay in a
small hand-maintained ONE_SHOT table, and a registry CLI with no entry
there reports UNKNOWN rather than being silently skipped.

Extracted src/custom-model-injection-apply.ts (applyConfigDirInjection/
removeConfigDir) so the production route and this script share one
implementation instead of two.

Full end-to-end run against a real llama-swap server, inside a
codeman/agent:llm-test Docker image with all 9 CLI binaries:

- claude, opencode, pi, grok, omp: PASS, real "hello world" replies
- codex: confirmed FAIL for a real protocol reason, not a bug — it only
  speaks the Responses API since Feb 2026, which llama.cpp/llama-swap
  don't implement
- gemini: confirmed FAIL, unresolved after real investigation — an
  undocumented GATEWAY AuthType gemini-cli selects once
  GOOGLE_GEMINI_BASE_URL is set rejects every auth-key format/override
  tried
- deepseek: reaches the server (env vars are read) but gets a consistent
  HTTP_404; root cause not identified, documented as best-effort/unknown
- antigravity: SKIP, no known mechanism (unchanged)

Two real bugs found and fixed along the way (grok, pi/omp registry
entries in stock.ts): grok's original recipe (env vars) was flat-out
wrong, not just unverified — the real mechanism is a config.toml
[model.<name>] block redirected via GROK_HOME. pi/omp's PI_CONFIG_DIR
does nothing for either (grepped pi's entire bundled source — the string
appears nowhere); the real redirect is the child process's own HOME, and
both need `models` as an array of {id} objects, not an object keyed by
id (silently loaded zero models otherwise).

deployment_plan.md, PR.md, docs/custom-model-endpoints.md, and CLAUDE.md
updated with the final confidence table reflecting all of the above.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
2026-09-13 17:42:35 +08:00
DevvynandClaude Sonnet 5 41416566aa feat(custom-model): Custom Model Endpoint Profiles (local or cloud, all harnesses)
Point any Codeman-supported harness (Claude, opencode, Codex, Gemini, Pi,
Grok, DeepSeek, OMP) at a custom OpenAI-compatible endpoint instead of its
native cloud backend, for a given session. Covers local hardware (llama.cpp,
Ollama, vLLM, DGX Spark, Strix Halo) and cloud (Azure AI Foundry, OpenRouter).
Off by default (customModelEndpointsEnabled, synced, default OFF).

- Registry: capabilities.customModelInjection per CLI entry (env /
  configContentEnv / configDir / unsupported kinds)
- Pure injection builder (custom-model-injection.ts) turning an endpoint +
  model id into the real env vars / config content per CLI
- Endpoint store + CRUD routes (custom-model-hosts.ts,
  custom-model-routes.ts), discovery via GET /v1/models, SSRF-guarded
- Session integration: Session.setCustomModel()/restartCli()
  (POST /api/sessions/:id/custom-model), reusing the existing
  respawn-pane -k primitive to restart the CLI process with new env
- Multi-user hardening: every new redirect-capable env var added to its
  CLI's privilegedEnvKeys, closing a pre-existing gap where several were
  already reachable via the generic envOverrides field's prefix allowlist
- Standalone scripts/test-local-llm-harnesses.mjs: spawns real CLI binaries
  against a real endpoint outside the web UI, independent of tmux/sessions
- Mock-server contract tests (test/fixtures/mock-openai-server.ts) replaying
  every CLI's injected values through a real HTTP shape

Real end-to-end validation against a live llama-swap server (inside a
codeman/agent:llm-test Docker image with all 9 CLI binaries) found and
fixed three real bugs before they shipped:
- Codex's config.toml schema was wrong ([model].default table instead of
  a top-level model string + [model_providers.custom]); fixing it then
  surfaced a genuine, documented protocol incompatibility (Codex only
  speaks the Responses API since Feb 2026, which llama.cpp/llama-swap
  don't implement)
- Claude Code's async session-title-generation call validates
  ANTHROPIC_DEFAULT_HAIKU_MODEL against its own internal model list and
  hangs the whole -p invocation on an unrecognized name; documented for
  chunk 6, worked around in the standalone script only (--bare is NOT
  safe for a real interactive session, which needs hooks)
- The discovery route's authStyle: 'both' option (send both Authorization
  and api-key headers) reliably hung a real server; removed the option
  entirely rather than just changing the default

Status: draft. Chunk 6 (frontend toolbar/settings UI) not yet built — see
PR.md and deployment_plan.md for the full chunk breakdown and confidence
table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017HqNWfmtBU2KN29SvSVWB3
2026-09-13 17:42:35 +08:00
DevvynandClaude Sonnet 5 c179daf869 fix(docker): re-assert /opt/codeman-cli ownership every start, not just at build
/opt/codeman-cli is chowned to PUID:PGID once, at image build time, from
the PUID/PGID build args. That bake only happens when the image is
actually rebuilt (`docker compose up --build`, which Start-Codeman.sh
always does) — a deployment that runs the compose file directly instead
(Unraid's Compose Manager, a native systemd unit, any plain
`docker compose up`/`restart`) can change PUID/PGID in .env and restart
without ever rebuilding. The container then runs as the NEW uid via
entrypoint's setpriv (Linux needs no /etc/passwd entry to setuid to an
arbitrary number) while the CLI directory is still owned by the OLD one
baked into the image layer — silently breaking the self-update-a-CLI-
in-place fix that directory exists for.

Unlike HOME/CODEMAN_CASES_PATH, this one is pure image content Codeman
itself populated, never host data that might legitimately belong to
someone else, so there is no ownership to be careful about — it is
always correct for it to be owned by whoever the container is about to
run as. Re-assert it unconditionally on every start.

Verified live: built an image with PUID=99/PGID=100, ran it with
PUID=1234/PGID=4321 (no rebuild, simulating a changed .env restarted
directly), confirmed /opt/codeman-cli ends up 1234:4321-owned and is
genuinely writable by the running process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
2026-09-13 17:41:31 +08:00
DevvynandClaude Sonnet 5 ae32daf135 fix(docker): address maintainer review on #377
Two real bugs the review caught, both verified live against a real
build on the Unraid host:

1. entrypoint.sh's chown fired on ANY ownership mismatch, not just a
   directory the daemon itself created root-owned. A host tree
   legitimately owned by some other account - an existing
   CODEMAN_CASES_PATH the README already allows pointing at a normal
   projects directory, or appdata under a different PUID/PGID
   convention than the one in use - got silently recursively re-owned
   with one log line to explain it. Now gated on the target actually
   being root-owned; anything else is a clean refusal naming the
   directory, its owner, and PUID/PGID. Start-Codeman.sh also now
   pre-creates CODEMAN_CASES_PATH the same way it already did
   CODEMAN_APPDATA_PATH, so Compose never has to materialise a missing
   bind source as root in the first place - the in-container chown
   becomes a safety net, not the primary mechanism.

2. The CLI-update chown (chown -R .../node_modules /usr/local/bin)
   handed the runtime account write access to entrypoint.sh itself
   (root-owned, executed as root on every container start with
   CHOWN/DAC_OVERRIDE/SETUID/SETGID) and the node binary - owning the
   DIRECTORY is enough to rename it aside and drop a replacement, which
   would let a compromised session arrange for its own script to run
   as root at the next restart. The four CLIs now install into a
   dedicated /opt/codeman-cli prefix (NPM_CONFIG_PREFIX); only that
   directory is chowned, /usr/local stays root-owned throughout.

Smaller fixes from the same review:

- Start-Codeman.sh's volume-refresh label filter wasn't project-scoped:
  a second Compose stack on the same host sharing the `codeman-dist`
  volume KEY could have had ITS volume deleted. Added a
  com.docker.compose.project filter, resolved from this stack's own
  `compose config --format json`.
- Override-file precedence was backwards (checked .yaml before .yml;
  Compose actually prefers .yml) - swapped, plus a warning when both
  exist.
- entrypoint.sh's setpriv now also passes --bounding-set -all, so
  CapBnd actually clears post-drop rather than just CapPrm/CapEff.
- A comment on git_head_commit() noting it returns nothing for a
  worktree checkout (.git as a file), consistent with the script's
  existing -d .git convention elsewhere.
- Doc drift: CLAUDE.md's Docker Compose section still described the
  old pre-created-and-chowned-by-hand model and didn't mention the
  root-then-drop entrypoint; the state-files list was missing
  docker-build-source.json; docs/docker-compose.md and
  docker/.env.example still had the pre-rename `Coding/codeman` path
  in one place each.

Verified end to end against a real build on the Unraid host: a
root-owned bind source is corrected as before; a directory owned by
neither root nor PUID:PGID is refused rather than silently rewritten;
a correctly-owned directory is left alone entirely; the four CLIs
resolve via PATH from /opt/codeman-cli while /usr/local/bin,
/usr/local/lib/node_modules and entrypoint.sh itself stay root-owned;
CapBnd is fully cleared post-drop.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
2026-09-13 17:41:31 +08:00
DevvynandClaude Sonnet 5 8fe3f34fc5 fix(docker): detect and refresh stale build-artefact volumes
codeman-node-modules and codeman-dist (docker-compose.yaml) are seeded
from the image only while empty, so a rebuilt image's fresh dist/
node_modules sat unused behind old volume content until something
cleared it. The in-app self-updater never hit this (it rebuilds INSIDE
the running container, into the very volume already in use), but a
`docker compose build` triggered from outside it — Start-Codeman.sh,
after a manual `git pull` — did: the container came back up looking
unchanged, serving stale compiled routes against current source.

Start-Codeman.sh now compares the checkout's HEAD commit and
package-lock.json hash against a recorded marker
(docker-build-source.json) and clears just the affected volume(s)
before its own --build when either moved.

The in-place self-update path writes that same marker after a
successful build, so the two mechanisms agree on what the volumes
currently reflect — without it, the next plain Start-Codeman.sh run
would see the HEAD self-update just checked out, not recognise it as
already accounted for, and wipe the volumes self-update just correctly
rebuilt right back to the older baked image.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
2026-09-13 17:41:31 +08:00
DevvynandClaude Sonnet 5 89e2cb5814 fix(docker): let the runtime account update its own global CLIs
The four CLIs (claude, gemini, codex, opencode) are npm-installed
globally as root during the image build, before the unprivileged
runtime account exists. A session running as that account (e.g. a
codex-mode terminal) then hits EACCES the moment it tries to update
one in place, because npm renames the old package directory aside
before installing the new one, which needs write access to the
parent (/usr/local/lib/node_modules), not just the target package.

Chown that tree plus /usr/local/bin's CLI symlinks to PUID:PGID in
the same step that creates/renames the runtime account.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R9ZSTEenc8soSu9bTi8Xru
2026-09-13 17:41:31 +08:00
DevvynandClaude Sonnet 5 d38bf33a69 docs(docker): document the reverse-proxy host allowlist
CODEMAN_ALLOWED_HOSTS is a real, documented application setting (the Host-
header allowlist in network-auth-policy.ts), but docker-compose.yaml does not
forward it from .env into the container - Compose only passes through
variables explicitly listed under environment:, and this is not one of them.
Set without that passthrough, any request through a reverse proxy is rejected
with 403 Forbidden: host not allowed before it reaches any handler, and
nothing in the Docker deployment docs said why.

Document the variable and the override needed to forward it, using the
Local customisation mechanism already described above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 17:41:31 +08:00
DevvynandClaude Opus 5 9702126046 chore(docker): name the default runtime account codeman
CODEMAN_RUNTIME_USER defaulted to `opencode`, which no longer matches the
project and is confusing in a deployment whose every other identifier is
codeman. Rename the default in .env.example and in the Dockerfile ARG that
mirrors it, and correct the example comment that referred to
/home/opencode/codeman-cases.

Also drop the `Coding/` component from the example application-data path.
CODEMAN_APPDATA_PATH and CODEMAN_CASES_PATH now suggest /mnt/user/appdata/codeman
and its codeman-cases child, matching the account name and removing a directory
level that meant nothing outside the original author's host. README.md is
updated to match, including the chown example.

The npm package `opencode-ai` and the references to the OpenCode CLI are
deliberately left alone: those name a different tool, not this account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:41:31 +08:00
DevvynandClaude Opus 5 748bbf5423 fix(docker): honour docker-compose.override.yml in Start-Codeman.sh
Naming a Compose file with -f disables Compose's automatic discovery of the
override file, so Start-Codeman.sh silently ignored docker-compose.override.yml.
Any local customisation placed in the conventional override file was dropped
without warning, and the only way to notice was to inspect the running
container.

Collect the -f arguments into an array, append the override file when one is
present, and reuse that array for the final launch so the two cannot drift
apart again. Both .yml and .yaml are checked, in Compose's own precedence
order, and the chosen file is reported on startup.

Document the override file in docker/README.md, including the two things that
are easy to get wrong: it is ignored when -f is passed without naming it, and
it cannot remove a key such as ports, which Compose concatenates. Add the
override file to .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:41:30 +08:00
DevvynandClaude Opus 5 10876aa440 fix(docker): correct bind-mount ownership before dropping privileges
Compose binds CODEMAN_APPDATA_PATH and CODEMAN_CASES_PATH from the host. When
either path does not exist yet - a first run, a cleared application-data
directory, a restored backup - the Docker daemon creates it owned by root. The
server runs unprivileged as CODEMAN_RUNTIME_USER, so it cannot create its own
state directory, and the container restarts forever on:

  Failed to start web server: EACCES: permission denied, mkdir '/home/<user>/.codeman'

Start-Codeman.sh already worked around this by preparing the directory on the
host, so the failure only appears when Compose is run directly, which the README
documents as a supported path.

Add docker/entrypoint.sh, which starts as root, corrects the ownership of both
bind mounts, then drops to PUID:PGID with setpriv. The Dockerfile's USER
instruction is replaced by that entrypoint and CMD is unchanged.
docker-compose.yaml adds back only the four capabilities the chown and the
privilege drop require, so cap_drop: ALL continues to remove everything else.

Two guards keep existing deployments working:

- A container started with an explicit `user:` is left alone. The entrypoint
  execs straight through, with no elevation and no chown.
- A chown that fails is a warning, not an error. Bind mounts backed by NFS,
  CIFS or a rootless daemon can refuse chown while remaining perfectly
  writable, and those deployments must keep starting.

PUID and PGID are also exported as runtime environment defaults so the image
behaves correctly when run without Compose, rather than depending on build args
alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 17:41:30 +08:00
codeman-local b357fe832e feat(mobile): add Shift arrow keys for Codex prompt navigation 2026-09-12 20:45:05 +08:00
Codeman maintainer a017e9a8e0 chore: version packages codeman@1.27.0 2026-09-12 06:10:14 +02:00
Codeman maintainer 65ddedd1d4 fix: act on the 1.27.0 pre-release review
A Fable 5.1 reviewer read the whole release diff against 1.26.2 and returned
SHIP WITH FIXES. These are its findings, verified before acting on each.

**The changelog advertised a feature the code refuses (major).** The #401
changeset and docs/web-tabs.md both listed `*.localhost` in the loopback set.
The follow-up in 02b0e278 moved it out of the auto-route set on security
grounds and updated CLAUDE.md but neither of those, and that changeset becomes
the 1.27.0 CHANGELOG entry: a user would have read the release notes, tapped
`http://app.localhost:3000/` on a phone and got a connection error from a
documented feature. Both corrected, and the user guide now says why it is
excluded and that adding such a dashboard by hand still works.

**Dictation delivered its text twice (minor, #388).** `keydownSnapshot` started
`null`, so `keydownSnapshot ?? canonicalCount` at the input event read a counter
xterm had ALREADY bumped: on a fresh page load with no keydown yet, xterm's own
capture listener forwards the `insertText` itself (it is not gated behind a
keydown), then the snapshot equals the bumped count, `count > snapshot` is
false, and the controller emits the same text again. Reproduced directly
against the module: it emitted `hello` for input xterm had already delivered.
A `0` baseline restores that file's own invariant, that a missed recovery is
acceptable and a duplicated keystroke is not. Two regression tests, covering
both the xterm-already-delivered and genuinely-dropped halves.

**The sorted rail's arrow-key walk followed the DOM (minor).** `_tabKeydownHandler`
steps `querySelectorAll` order, which is `sessionOrder`, while a sorted rail
paints its rows with the flex `order` property, so ArrowDown from the top card
landed wherever that session happened to sit in the tab order. It now sorts its
node list by the COMPUTED order first: computed rather than inline, because web
tabs take their `order: 9999` from CSS and would otherwise read as 0 and lead
the walk. This is the one place that follows the paint; the Alt+N badge, the
drag model and the filter all still deliberately read the DOM.

**A trusted dashboard was auto-reused by a tapped link (minor, #401).** The
reuse loop skipped `managed` and direct-mode records but not `trusted`. A
trusted frame is mounted with `allow-same-origin`, i.e. on Codeman's origin
with the user's cookie, and these links come from agent output, which is the
threat model the loopback allowlist was just narrowed for. An agent that can
write into the dev server's tree could print a path that one tap opens inside
that privileged frame. Excluded from auto-reuse, with a test; opening it from
the Run dropdown is still an explicit action and unchanged.

**Two documentation claims that were no longer true.** CLAUDE.md said
test/location-overlay-commands.test.ts pins every remote pane command, but
remote claude and remote omp now have their own arm in `buildRemoteLaunchCommand`
and never reach `defaultRemoteCommandForMode`, which is what that test asserts,
so it pins nothing for them and changing either arm will not fail it. Named the
real pins instead. Also documented the arrow-key-walk exception in the rail
paragraph.

Left as follow-ups, deliberately: `POST /api/webviews` does not dedupe by URL
server-side, so two devices tapping one link concurrently can still save two
dashboards for one origin (pre-existing endpoint behaviour that #401 makes
reachable by a tap), and the location-overlay golden should assert the real
remote claude/omp commands rather than a branch neither reaches.

Full gate green: 359 files, 6869 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 06:09:43 +02:00
Codeman maintainer 8b23f3e260 feat(rail): sort the vertical tab rail by activity, and give its rows the home screen's card
The vertical rail lists exactly the sessions both home screens list, so it now
answers their question the same way instead of showing the raw tab order.

Order: new per-device `tabRailSort` (App Settings -> Appearance -> Tabs ->
Vertical Rail Order, default "By activity"). It runs `CodemanSessionOrder` over
rows classified by `_mobileOverviewState`, i.e. literally the home screens'
comparator, `lastSubmitAt`-anchored running group included.

It is applied as the flex `order` property, never by reordering the DOM.
`#sessionTabs` stays in `sessionOrder`, which is what keeps the Alt+N badge
honest (it names a shortcut, not a row position, so it deliberately does NOT
run 1,2,3 down a sorted rail), and keeps drag-and-drop, the arrow-key walk, the
sidebar filter and `_scrollActiveTabIntoView()` all reading the list they
always read. A session changing state then moves one inline style instead of
forcing the full rebuild that would restart every card's animation on every SSE
tick. The incremental render path re-applies it, since a state flip adds no tab
and never reaches the full rebuild, and an empty string is what clears it when
sorting stops. Web tabs are pinned past the cards by a CSS `order: 9999`, since
`renderWebviewTabs()` emits the same markup for every layout and the flex
default of 0 would interleave them. Drag is switched off while sorting (the
drop rewrites `sessionOrder` correctly and the sort puts the card straight
back, so the affordance would be a lie); 'manual' is the way back.

Cards: detailed rail rows become bordered cards on `--bg-card`, with the stamps
line on its own full-width row and the pill at its right end. The state dot
goes 6px to 9px, keeps its orbiting ring while working and gains the green
halo; idle mutes toward `--text-muted` as the home rail does. Needs/error/
waiting reuse `home-sessions-blink-red`/`-yellow` rather than a second copy.

These card rules are RAIL-SCOPED and deliberately absent from the comma-grouped
selectors that carry both vertical surfaces: the rail is an occasional,
resizable list you scan, while the sidebar is a permanently-docked nav column
where 20 stacked cards read as a wall. Every state-dot rule also excludes
`.tab-alert-action`/`.tab-alert-idle` by hand, because those alert rules are
only (0,3,0) and these are (0,5,1)+.

Lines: the lineage bracket already drew in the rail, but its track sat 6px from
the left edge, so half of its 11px outer glow was clipped by the window frame
and it read as a thread pinned to the frame. It now runs at 10px, mid-channel
in the gutter the rail already reserves.

Tests: test/tab-rail-order.test.ts (17) drives the real `isTabRailSorted()` and
`_tabRailSortOrder()` out of app.js, covering the row model (a WORKING row
ranked by `lastSubmitAt`, which would otherwise rank every running turn as
freshly started and fail no rendering test), the Alt+N badge, and the opt-out.

Verified in Chromium across sorted/manual/simple/header-strip/sidebar with the
setting flipped at runtime: no page errors, and the header strip and sidebar
render byte-identically to before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 06:09:28 +02:00
Codeman maintainer 02b0e27898 fix: merge-time follow-ups for #400, #401, #362 and #388
Each item is from the pre-merge review of the PR it names, applied on master
rather than by pushing to a contributor branch.

#400 (response viewer, shenlvkang-collab)
- The brief view opened at `scrollTop = 0`, right when it was a single card
  holding the last row. Now that it renders the whole turn, the top is the
  turn's first narration line and the answer can be screens below it, while
  loadFullContext already scrolls to the bottom of the same turn. A multi-row
  turn now opens at its newest text; a single card still opens at the top.

#401 (loopback links as web tabs, shenlvkang-collab)
- Drop `*.localhost` from the auto-route set. Every other member is an address
  literal that can only mean this box; a `*.localhost` DNS name is not one, and
  a resolver with a search domain retries `evil.localhost` as
  `evil.localhost.<search domain>`. The link source is agent-written terminal
  output, so that set is the whole confinement on a tap that makes Codeman
  fetch a URL server-side and persist it. The page-side test stays broader
  (`isOnBoxHostname`), where a false positive only declines to proxy.
- A link to the origin root navigated nothing: the path was flattened to '',
  which openWebview reads as "no deep link", leaving an open frame where it was.
- `this.webviews` being set does not mean it is loaded. initWebviews() assigns a
  truthy empty map and only then awaits the list, so a tap during page load
  found nothing to reuse and POSTed a duplicate record. Join the in-flight
  refresh instead.
- One dashboard per dev server rather than per host spelling, which is what the
  method's own comment already promised.
- Toast on the auto-create: it writes webviews.json, broadcasts over SSE and
  adds a Run-dropdown row on every signed-in device, with a new tab as its only
  previous signal.

#362 (remote omp continuation, timkjr)
- Accept the allowlisted `mode === 'omp'` arm as-is; a blanket registry render
  would hand deepseek a locally-resolved --profile and bypass claude's own
  overlay. A registry-declared switch is the follow-up if a third mode needs it.
- Revert the whole-file Prettier reformat of docs/remote-sessions.md (docs/ is
  hand-formatted and outside `npm run format`), keeping only the two new
  sections.
- Correct three stale passages: architecture-invariants' `exec claude
  --dangerously-skip-permissions`, the `exec <cli>` paragraph (claude and omp
  now have their own arms, and the claude pane's PID is the login shell), and
  omp-integration's `-c 'omp'`. RemoteCommandMode gains deepseek and omp.
- Add the missing `_maybeCaptureOmpSessionId` remote-guard test; the sibling
  guard in `_pinOmpRespawnId` had one and this path runs earlier, on the first
  idle turn.

#388 (keyCode 229 recovery, aakhter)
- Gate notifyCanonicalData on shouldSuppressTerminalQueryResponse and
  isTerminalFocusOrMouseReport. onData also carries the DA/DSR/CPR/OSC replies
  xterm answers during Ink redraws and its SGR mouse and focus reports; any of
  those landing between the keydown and the candidate's resolution was read as
  "xterm spoke for this keystroke", standing the recovery down and leaving the
  character dropped, worst on a busy agent pane. Reached through
  window.CodemanTerminalInput: the predicates live in a module IIFE that closes
  long before this call site, so bare references would throw into the
  surrounding try/catch and stop the notify from ever running.

Every fix has a test that fails without it (verified by reverting each).
Full gate green on the combined tree: 358 files, 6849 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-12 05:15:41 +02:00
Ark0N 9d664ffe01 Merge pull request #388 from aakhter/pr/keycode-229-input-recovery
fix(terminal): recover dropped keyCode 229 input (Android/IME)
2026-09-12 05:14:39 +02:00
Ark0N a28b04c368 Merge pull request #362 from timkjr/feat/omp-remote-continuation
fix(omp,remote): thread remote-omp resume/continue through respawn and reattach
2026-09-12 05:14:24 +02:00
Ark0N e35b68e253 Merge pull request #401 from shenlvkang-collab/pr/loopback-links-webtab
feat(webview): open localhost links through a proxied web tab from another device
2026-09-12 05:14:09 +02:00
Ark0N 77d9ad59f7 Merge pull request #400 from shenlvkang-collab/pr/claude-viewer-last-turn
fix(web): show the whole last turn in the Claude response viewer's brief view
2026-09-12 05:13:55 +02:00
timkjrandClaude Sonnet 5 aeb55c92b0 fix(settings): reconcile showPlanUsageLimits default on first read
planUsageChipEnabled() (settings-ui.js) shows the header chip and the App
Settings checkbox as already ON whenever showPlanUsageLimits has never been
set — a discoverability default from 1.9.3. readPlanUsageTelemetryEnabled()
(hooks-config.ts) deliberately treats an absent key as "no telemetry" — a
privacy default, pinned by its own unit tests (never POST usage data
without an explicit persisted yes). Nothing reconciled those two
independent guesses, so a fresh install showed a checked box that silently
collected nothing until the user opened Settings and hit Save at least
once.

Verified live: an install that had never touched this setting had no
showPlanUsageLimits key in settings.json at all, and its running Claude
process's argv carried no --settings flag — zero telemetry ever collected
despite the chip rendering as enabled.

GET /api/settings now persists the resolved default (true) the first time
the key is truly absent — not explicit false — so "chip visible" and
"telemetry collected" become the same fact. readPlanUsageTelemetryEnabled's
own absent-means-false contract is untouched; after this runs once the key
is never absent again, so that branch stays correct in isolation while
being unreachable in practice for any install that has ever called this
route. An explicit false set afterward is respected forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 19:10:25 -05:00
timkjrandClaude Sonnet 5 0cedf05d13 fix(terminal): trap Ctrl+Z in non-shell sessions to prevent accidental suspend
Ctrl+Z (SIGTSTP) suspends the foreground job on the pane's tty. In a plain
shell session that's the user's own job-control tool (suspend, fg back),
but in claude/omp/pi/codex/etc. sessions it stops an unattended agent loop
dead with no visible output — the same failure shape as an XOFF freeze,
just via job control instead of tty flow control. Ink-based TUIs usually
run in raw mode (ISIG off) where ^Z is inert, but that only holds once the
CLI is actually running and stays in raw mode; it's live at the shell
prompt before launch and during any raw-mode toggle.

Swallow it client-side in attachCustomKeyEventHandler, mode-gated so shell
sessions keep normal job control, mirroring the existing Ctrl+V/Ctrl+Backspace
interception pattern in the same handler. Case-insensitive key match (Caps
Lock flips ev.key to 'Z' without setting shiftKey, so a plain === 'z' check
let the exact suspend keystroke this exists to catch slip through).

Also cover subagent/teammate terminal windows (panels-ui.js's
initTeammateTerminal), which render a separate xterm instance with no
custom key handler at all and are always running an agent CLI — never a
shell — so the trap there is unconditional.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 18:30:48 -05:00
shenlvkang-collabandClaude Fable 5.1 349a89ec3b fix(webview): let a proxied single-page app route on its own path, and recover a frame that reloads
A dashboard served through a web tab saw `/webview/<cap>/` as its
`location.pathname`, and no app has a route for that: a React Router, Vue
Router or Vite dev-server page painted its HTML and CSS and then replaced
them with its own "page not found" the moment its script ran (reproduced
with a minimal history-routed page).

The proxy's runtime shim now rewrites the history entry to the path the
page would see on its own origin, before any page script runs. The base
element still resolves relative URLs inside the prefix and every root-
absolute sink is rewritten back into it, so only what the page READS
changes. With the document URL masked the Referer-keyed 404 rescue can no
longer help a request the shim misses, so the remaining URL-taking entry
points (`Worker`, `SharedWorker`, `navigator.sendBeacon`, `window.open`)
are covered by the shim as well.

A navigation the page starts itself afterwards — `location.reload()`
(a dev server's full-reload HMR), a root-absolute `location.href` — lands
on Codeman's root with no capability anywhere: no prefix in the path, no
cookie in an opaque-origin frame, a Referer naming the masked page. It is
recognised by shape (a top-level iframe navigation asking for HTML, for a
path Codeman does not serve) and answered with a static page whose only
script posts `{type:'codeman:webview-lost', path}` to the parent; the tab
that owns the frame (matched by `event.source`, never by the payload)
remounts it inside the prefix at that path, bounded per frame. The
unauthenticated form is answered in the auth middleware before the
credential checks, so a dev server that reloads on every save cannot
rate-limit its own user out of Codeman; the authenticated form (Basic
auth, trusted mode) is answered by the 404 handler.

Verified end to end against a history-routed page: boots on `/`, its
API call succeeds, a reload inside the frame comes back routed on the
path it had pushed, `location.href = '/about'` comes back on `/about`,
and a deep link opens on its path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 14:13:15 +08:00
shenlvkang-collabandClaude Fable 5.1 d9eeb039db feat(webview): open localhost links through a proxied web tab from another device
An agent prints `http://localhost:5173/` (a dev server, a preview it just
served) and the user taps it on a phone. That address only exists on the
Codeman box, so the link was a guaranteed connection error from any other
device — while the web-tab proxy fetches from the server, where it works.

A loopback link (`localhost`, `*.localhost`, 127/8, 0.0.0.0, ::1) activated
in the terminal or clicked in the Response Viewer now opens as a proxied
web tab whenever the Codeman page itself is not on that box. A saved
proxied dashboard on the same origin is reused, with the link's own path,
query and fragment opened inside it (a mounted frame is navigated, not torn
down, so its state survives); otherwise one is saved under its host:port,
sandboxed like any other web tab, so it is in the Run dropdown next time.

Only loopback is routed this way. A LAN or tailnet address may well be
reachable from the device (a VPN, the same Wi-Fi) and a direct open is the
cheaper, richer path, so those keep opening in a new browser tab; on the
box itself every link opens directly. The terminal link provider and the
viewer's click handler consult one hook and fall through to their existing
behaviour when it declines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
2026-09-10 12:48:47 +08:00
shenlvkang-collabandClaude Fable 5.1 bd61735393 fix(web): show the whole last turn in the Claude response viewer's brief view
The eye button rendered `data.text`, which is one row: the last assistant
row of the transcript. A Claude answer is a median of 3 model messages
(p90 11) split around tool calls, so the brief view usually showed the tail
of an answer ("Done.", "Let me look.") and the substance appeared only after
More. The full view was fine, which is why the brief one read as broken by
comparison.

The brief view now asks `?context=turn`. The reader answers with the
assistant messages of the last ANSWERED turn (`selectLastAnsweredTurn`: the
highest `turn` that has an assistant row, so a prompt queued after the
answer does not blank the view) and the frontend renders them exactly as
the full view renders that turn: one badge, then continuation segments,
gated on the numeric `turn` as before.

`data.text` is unchanged in every context — still the last assistant row,
never `messages.at(-1)` — because agent pollers hash it. Readers that emit
no turns (Codex, the pane parser, DeepSeek, an older server) return `text`
only for `context=turn`, and the brief view keeps its single card for them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
2026-09-10 12:45:16 +08:00
shenlvkang-collabandClaude Fable 5.1 58b4cb06d8 feat(files): let the path picker jump to a typed path and sort by name or date
The picker's current-folder line was a read-only breadcrumb, so reaching a
deep folder meant tapping through every level, and the listing was fixed to
name order, so the file an agent had just written was somewhere in a
500-entry list.

The current folder is now an editable field: Enter or Go jumps there, a full
file path lands in its folder with that file selected, and a path that does
not resolve keeps the listing you had and says so, instead of the reset to
the root that a stale initialPath gets. A Sort control orders the listing by
name or modified time in either direction, folders always first, and the
choice is remembered per device like the hidden toggle. Each entry shows a
compact modified time (time of day today, month-day this year, else the
date).

GET /api/filesystem/browse stamps every entry with mtimeMs to make that
possible; the stat that already fetched a file's size now serves both, so
it is still one stat per entry. Entries without an mtime (an older server,
the in-container listing) sort after dated ones and then by name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McLWqCWBuQYGuPMScb4Aou
2026-09-10 12:42:33 +08:00
Codeman maintainer 5b667264b4 chore: version packages codeman@1.26.2 2026-09-10 03:20:58 +02:00
Ark0N e3d5fd90cd Merge pull request #392 from JDProfresh/fix/ios-safari-toolbar-gap
fix(mobile): lift the iOS Safari toolbar by the measured chrome overlap
2026-09-10 03:10:12 +02:00
Ark0N 713f632a64 Merge pull request #397 from irisitymichaelgrundberg/fix/detached-session-owns-its-pane-size
fix(terminal): let a detached session's own window own its pane size
2026-09-10 02:58:54 +02:00
Ark0N 92b5dfacb0 Merge pull request #396 from irisitymichaelgrundberg/fix/terminal-font-settle-before-fit
fix(terminal): fit the terminal only once the terminal font is measurable
2026-09-10 02:58:48 +02:00
Ark0N 890a1b0902 Merge pull request #395 from irisitymichaelgrundberg/fix/full-history-replay-row-alignment
fix(terminal): keep row alignment in the full-history pane replay
2026-09-10 02:58:42 +02:00
Ark0N a360763890 Merge pull request #394 from irisitymichaelgrundberg/fix/ctrl-v-pastes-twice
fix(paste): handle only the first paste event the Ctrl+V trap receives
2026-09-10 02:58:36 +02:00