Commit Graph
325 Commits
Author SHA1 Message Date
Codeman maintainer 5e83cd3885 fix(html-view): a ~/ HTML link renders as a page too (#547/#604 integration)
#604 makes `~/` links clickable and passes them through unchanged, since only
the server knows the session host's home. The html-view mint read `~/x.html`
as workspace-relative and answered 404, so the preview fell back to the
source view. It now expands `~/` against this host's home, as the attachment
route does, and refuses it in a Docker case (that `~` is the container's)
with the same message as the attachment and folder routes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:56:58 +02:00
Codeman maintainer 0996f99fbb Merge branch 'rel1411/html-md': render HTML files as pages (#547) and open .md from the terminal in the rendered viewer
Conflicts resolved against #604, #606 and #601:
- constants.js: the rooted link pattern takes html-md's `markdown` and
  `html?` with #606's m4v/ogv; #604's relative pattern gets the same list
  (link-provider-regex pins the two equal); #604's findFilePathLinks /
  folder helpers and html-md's rendersAsPage() are both kept, and
  window.CodemanFilePaths exports all of them.
- terminal-ui.js: #604's resolved `target` (relative, ~/ and folder links)
  now also goes through rendersAsPage(), so every link kind routes .html to
  the page preview; .md routes there through FILE_PREVIEW_EXTENSIONS.
- CLAUDE.md, api-reference, architecture-invariants: both sides' text kept.
- test/folder-grid-ui.test.ts (#604) stubs rendersAsPage() like the other
  panels-ui harnesses.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:55:02 +02:00
Codeman maintainer 0feb5658ca Merge pull request #610: fix(approvals): treat DeepSeek permission prompts as terminal-only alerts 2026-10-13 18:29:01 +02:00
Codeman maintainer 6c7d63d45e Merge pull request #606: feat(mobile): phone Attach button and camera-roll video uploads
Conflicts resolved against #605 (and #602's test mock):
- CLAUDE.md Prompt uploads: #606's rewrite is kept and #605's bounded
  workspace probe sentence follows the remote (SSH) refusal.
- test/routes/session-routes.test.ts: the remote-pane-history and
  bounded-path-probe mocks are kept beside #606's _resetPasteRateBuckets
  import, and both PRs' paste-image tests are kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:21:52 +02:00
Codeman maintainer 6bbfc44054 Merge pull request #605: fix(uploads): probe the workspace bounded before a prompt upload
Conflict resolved in test/routes/session-routes.test.ts: #602's
remote-pane-history mock and #605's bounded-path-probe mock are both kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:20:21 +02:00
Codeman maintainer 60fd71470c Merge pull request #590: fix(cases): show the real reason when Link Existing rejects a case 2026-10-13 18:17:49 +02:00
Codeman maintainer a38eab49cd Merge pull request #528: feat(tabs): place new, child and web tabs into groups
Conflict resolved in docs/api-reference.md: #607's prompt cache section and
#528's tabGroupId section are both kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:10:37 +02:00
Codeman maintainer 076f62fa1b fix(preview): refuse a bare ~ folder in a Docker case too (#604 merge-time fix)
The Docker refusal on POST /api/sessions/:id/folders tested only `~/`, but
listFolder also expands a bare `~` to this host's home, so a Docker case
could list and serve the host home through it (the preview's "Show folder"
for a `~/x.png` link reached it with no typing). A bare `~` now gets the same
400.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:08:38 +02:00
Codeman maintainer 90034d07d7 Merge pull request #604: feat(preview): open ~/, relative and hard-wrapped paths; folder grid 2026-10-13 18:08:12 +02:00
Codeman maintainer 62227145dc Merge pull request #607: feat(telemetry): read out Claude Code's prompt cache state per session 2026-10-13 18:07:15 +02:00
Codeman maintainer 5b8313988f fix(terminal): no paneHistoryLines for a remote session (#583/#602 integration)
A remote session's local pane hosts the remote tmux client in the alternate
screen, so its history_size is always 0. #583 read that as "nothing to load"
and hid the partial-history notice, but with #602 a full=1 load reads the real
scrollback from the remote tmux. The route now leaves the field out for remote
sessions, so their notice keeps the byte-based wording and its Load full
history button.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:05:30 +02:00
Codeman maintainer 239f15437b Merge pull request #609: fix(remote): close the row gaps at the join in a remote history load
Conflict resolved in tmux-manager.ts: #609's screen-aware
joinScrollbackAndFrame keeps #602's merge-time SGR reset before the frame,
and #609's join tests expect it. The #601/#602 route test now mocks the
remote read in #609's { scrollback, screen } shape.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:04:05 +02:00
Codeman maintainer f78b820302 fix(terminal): a remote shell load keeps the remote scrollback (#601/#602 integration)
A remote session's local pane hosts the remote tmux client, so it is always
in the alternate screen. #601's alt=recording fallback therefore threw away
the scrollback #602 had just spliced in from the remote tmux on every remote
shell load. The fallback now applies only when no remote scrollback was
read (the read failed, or the session is not remote).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:02:36 +02:00
Codeman maintainer f9f00bdb5e Merge pull request #602: fix(remote): load a remote session's scrollback from the remote tmux
A full-history load of a remote SSH session reads the scrollback from the
remote tmux in one bounded ssh round trip and splices it above the local
frame. A transport failure (ssh 255, timeout, unparseable output) starts a
30 s per-host back-off; a missing remote session is per-session only.

Conflict resolved in session-routes.ts: #602's remote scrollback read runs
before #601's capture helper and its alternate-screen fallback.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 18:01:15 +02:00
Codeman maintainer 36f8afdaba Merge pull request #601: fix(terminal): load a shell from tmux's rendered history, not the byte recording
Conflicts resolved against #583 and #588:
- One history-depth field: #583's `historyLines` / `capturedHistoryLines`
  (reported as `paneHistoryLines`) is kept, and #601's lines cut now reads it
  instead of a second `capturedHistorySize`. #601's `alternateOn` /
  `capturedAlternateOn` joins it, and the one display-message format string
  ends `#{history_size} #{alternate_on}`.
- selectSession keeps #588's full=1 switch for panes with no scrollback, gated
  off remote and Docker sessions, and #601's SHELL_LOAD_QUERY for shells.
- computeHistoryTruncationNotice names a line count when paneHistoryLines is
  known (#583) and otherwise uses #601's remainder wording.
- docs: api-reference lists both historyLinesCut and paneHistoryLines; the
  notice invariant keys on history_size while the shell's automatic load is
  the one place alternate_on decides.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:59:07 +02:00
Codeman maintainer f2d5af2b4c Merge pull request #583: fix(terminal): partial-history notice only when there is history to load 2026-10-13 17:47:37 +02:00
Codeman maintainer ba5649bcd4 feat(html-view): open network access for rendered pages (owner decision)
Owner decision 2026-10-13 (#547): a rendered .html page behaves like a
file opened from disk in a browser. It may load scripts, styles, fonts,
images and media from any host and make fetch, XHR, WebSocket, beacon
and form requests to any host.

The /html-view Content-Security-Policy is therefore the `sandbox`
directive alone (HTML_VIEW_CSP, now exported and documented): the same
sandbox flags as before, never `allow-same-origin`, and no fetch
directive. It replaces Codeman's own `default-src 'self'` policy on these
responses. Everything that confines a page stays: the opaque origin (no
Codeman cookies or document, API writes with `Origin: null` refused, API
reads not readable cross-origin), and the capability serving only the
page's own folder under every path, symlink, dot, extension and
confinement rule.

A new test builds the route behind registerSecurityHeaders, as in
production, and pins the exact header: one `sandbox` directive with the
current flags, no `allow-same-origin`, no `-src` directive. The security
architecture, architecture invariants, API reference, CLAUDE.md and the
Working With Files wiki page now say what a page can do (reach the
network, read its own folder) and cannot do (Codeman's origin, cookies,
API, other folders), and that this is the owner's decision.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:39:36 +02:00
Codeman maintainer fa23f8be2a feat(html-view): answer Range requests through the shared sendFileBody()
A rendered page's own <video> or <audio> could not seek, because the
route streamed a plain 200 without Accept-Ranges. sendFileBody() and
localFileSource() are now exported from file-routes.ts and the serving
route uses them, so it advertises Accept-Ranges and answers a Range with
206 + Content-Range (or 416), exactly like file-raw, the attachment raw
route and /api/download. The body sender hijacks the reply and copies
every header set before it (the sandbox CSP, CORS, no-store), which the
new test pins on a 206.

Media types stay in ASSET_TYPES: with Range support they work, and they
are served under the same sandbox CSP as everything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:11:50 +02:00
Codeman maintainer bc940a3953 fix(html-view): require the resolved file of a mint to be HTML
The mint checked the extension of the requested path, then built the page
URL from the symlink-resolved file name. A `report.html -> notes.txt` link
minted a page URL for the text file. The resolved name now has to be
.html or .htm as well, the mint-side counterpart of the serving route's
resolved-path extension check.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:10:39 +02:00
Codeman maintainer 432faf7254 refactor(html-view): validate the mint body with a Zod schema
POST /api/sessions/:id/html-view read `path` off an unchecked cast. It now
goes through HtmlViewOpenSchema in schemas.ts (a non-empty string of at
most 4096 characters without NUL or line breaks, no other keys) via
parseBody, like the other file routes, and keeps its "Missing path
parameter" message for an absent path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:10:24 +02:00
Codeman maintainer 69aa5724e7 fix(html-view): scope the hidden-directory rule below the workspace root
The mint refused an HTML file whose absolute path had any dot segment, so
a workspace that itself lives under a hidden folder (~/.local/src/app)
could never render one of its own pages. For a file inside the workspace
the rule now covers the path below the workspace root, which is all the
page's tree can reach; outside the workspace it still covers the whole
path. A hidden directory below the workspace root is still refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:09:28 +02:00
Codeman maintainer 876dbca074 fix(html-view): re-check workspace confinement on every served request
The serving route checked the blocklist per request but not
attachmentConfineToWorkspace, while the attachment serve path
(file-routes.ts) re-checks it every time. A capability minted for a
folder outside the workspace before an admin turned confinement on kept
serving that folder for the rest of its TTL.

The capability record now says whether its folder was admitted from
outside the workspace, and the serving route refuses such a record while
confinement is on. The flag is part of the reuse key, so an
out-of-workspace mint never picks up a capability recorded as
in-workspace for the same folder.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:09:00 +02:00
Codeman maintainer fe50ad0be3 fix(html-view): apply the dot and extension rules to the resolved path
The serving route checked hidden segments and the asset allowlist on the
requested name only; after realpath it checked containment and the
blocklist. A symlink `cfg.json -> .git/config` inside the page's folder
was therefore served as 200 application/json. The dot rule now also runs
on the segments of relative(rootDir, resolvedPath), and the extension
allowlist on the resolved file name, which also types the response.

Both realpath calls on the request path (the mint of an out-of-workspace
file and the serving route) are now `await fs.realpath` instead of
realpathSync, so a slow mount stalls one request, not the event loop.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:08:13 +02:00
Codeman maintainer ded7911d1e fix(html-view): refuse /tmp, /var/tmp and directories above a broad root
os.tmpdir() returns $TMPDIR when it is set, and macOS sets it in every
terminal session, so with TMPDIR set /tmp, /private/tmp and /var/tmp were
never treated as broad and /tmp/report.html got a capability over all of
/tmp. The broad-root list now names /tmp and /var/tmp literally (through
canonical(), so macOS gets /private/tmp), refuses any directory that is an
ancestor of a broad root (/home, /Users, /private), and treats a drive's
mount point (/mnt/d on WSL, /media/x, /Volumes/x) as broad as well.

The new test sets TMPDIR elsewhere and still gets /tmp refused.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:07:36 +02:00
Codeman maintainer 2797098aa5 Merge pull request #547: feat(file-preview): render local HTML files as sandboxed pages
Conflicts resolved:
- CLAUDE.md: kept master's Prompt uploads paragraph and its longer
  File-path links paragraph (XLSX admission limits), and inserted the
  PR's html-view sentence after the download-only rule.
- src/web/public/terminal-ui.js: kept master's tile-aware
  getSessionId() in the shared file-path link provider and added the
  PR's .html routing to the file preview on top of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 17:03:00 +02:00
Codeman maintainer ef071ed485 feat(agent-skill): give the codeman skill to every agent CLI, not just Claude
The agent skill only reached Claude Code: injection wrote
.claude/skills/codeman, which no other CLI reads, and the per-session
preamble was seeded for claude alone. A Codex, Gemini, OpenCode,
Antigravity, Grok, DeepSeek or OMP session inside Codeman never learned it
could start and drive workers.

- Registry field capabilities.agentSkillDir (a closed enum, since
  clis.json can set it and it decides where the server writes) names the
  skills directory each CLI scans. Claude keeps .claude/skills; those seven
  CLIs get the same skill in <case>/.agents/skills/codeman (each measured to
  scan it; Claude Code does not, so it never sees two copies). pi gets none:
  a project skill makes pi ask "Trust project folder?" at startup.
- The preamble is seeded for every local session whose CLI takes the
  skill; nothing in it is claude-specific.
- The skill text is CLI-neutral and names the two CLI-side failures to
  report instead of retrying: a sandbox that blocks loopback (Codex's
  default), and commands that do not carry this session's environment
  (Codex 0.162 runs them in a shared app-server daemon).
- POST /api/sessions injects after the launch pre-flight, so a CLI that
  cannot start leaves nothing in the workspace.
- codeman skill install gains --mode; uninstall --case removes the copy
  from every directory the registry can write.
- The settings toggle moves to Agents & CLIs > All agent CLIs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:33:47 +02:00
RandalixandClaude Sonnet 5.5 5b8c73827e fix(preview): list a remote folder under an explicit sh
sshd runs the command in the remote user's login shell. On Hufflepuff cd is
an alias that echoes the directory, so the listing script captured the folder
path twice and zsh aborted on the glob: every folder grid over ssh failed
with "remote host unreachable: no matches found". Wrap the script in sh -c,
as the transcript read already does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 14:06:19 +02:00
RandalixandClaude Sonnet 5.5 3c08971ff8 fix(preview): refuse ~/ in a Docker case; document the folder-route gate
A Docker case is a local session for the file routes, but `~` inside the
container is the container's own seeded home. Expanding `~/x.png` on the host
opened a different file than the one the agent printed. The attachment POST
and POST …/folders now answer 400 for `~/` when the session has `docker` set
(absolute paths are unaffected: the workspace is a bind mount).

Docs: the resolved-target gate, the route-parameter limit, the listing join
rule and the Docker `~/` refusal in architecture-invariants.md,
security-architecture.md and CLAUDE.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:07:39 +02:00
RandalixandClaude Sonnet 5.5 6bffc0481c fix(preview): hold a folder file's resolved target to the attachment gate; accept long names
Folder routes only looked at the requested name, so `shot.png -> ../secret.bin`
and `notes.txt -> ../history` were served (registration answers 400 for both),
and a FIFO or device behind a supported name parked a worker on an open that
never returned. After resolving, the target's extension must be supported and
it must be a regular file (locally fs.stat, remotely the probe kind).

A 101..255-character file name answered 404 "Route not found": Fastify caps a
path parameter at 100 characters. The server now sets routerOptions.maxParamLength
(MAX_ROUTE_PARAM_LENGTH, shared with the route test harness) and a wiring guard
pins it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-12 12:04:34 +02:00
DDT f5e59283f9 fix(approvals): treat DeepSeek permission prompts as terminal-only alerts
Supervised-hook CLIs (DeepSeek Harness) raise Approvals Inbox items whose
answer route refuses keystrokes, but push notifications and the mobile/desktop
UI still offered Approve/Deny. Mark those items answerInTerminal, omit
approvalId (and therefore push actions) for supervised hooks, and show
"Answer in the terminal" / Open session instead of broken buttons.

Fixes #598 (functional part; docs gaps left for a follow-up).
2026-10-12 16:16:16 +08:00
JD ecb2e3d8f3 fix(telemetry): stamp a compaction on entering the null window
Claude Code's tracker nulls recache_tokens_if_cold at a compaction or a tool-result clearing and only counts the expected rebuild at the next request, which also closes the window, so no report carried both and the old rule never fired. Stamp on the first warm null report after one that carried a value, carry it through idle re-reports and drop it at the next request; the readout now says "Context compacted at HH:MM" since a clearing opens the same window.

Also persist a changed report so a crash restores the latest one, rename the session field to statusLineExporter (statusLineTelemetry is the registry capability) and document its values, word the stake as re-cached, move the readout block above the SSE staleness heading, and drop keep-warm mentions until it lands.
2026-10-11 12:36:59 -04:00
RandalixandClaude Opus 5.5 3857fba065 fix(preview): folder grid review — multi-user scope, bounded probing, smaller items
- Multi-user: a non-admin's folder must resolve inside their own userSpacePath
  or the session's workingDir (folderAllowedForUser, the path picker's rule),
  checked on the resolved folder before it is read and again on each served
  file after symlink resolution; 403 otherwise.
- listFolder probes the local folder with probePathKind({ pastCap: true })
  first (unknown -> 503 via describeUnknownPath, never 404), then async
  fs.realpath; resolveFolderFile uses probePathKind instead of fs.stat.
- The guard runs on the realpath before readdir.
- FOLDER_PATH_LINK_PATTERN ends a folder at ':' (quadratic `://` rescan on
  colon runs; PATH=/a:/b linked as one folder).
- Load counters drop stale preview / folder-grid responses; closing the grid
  invalidates a listing still in flight.
- validFolderFileName refuses a leading dot; the remote listing matches
  extensions in any case.
- Docs: folder grid in the z-index lists; multi-user rule and bounded probe in
  the invariants and security docs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 14:03:20 +02:00
timkjrandClaude Opus 5.5 28b41efa58 fix(remote): keep the remote screen rows above the local frame in a remote history load
The remote history and the local frame are read a round trip apart, so
two seams dropped rows at the join: rows that scrolled off the screen
during the round trip, and the top of a remote screen taller than this
pane (a shared session sized by a taller client under window-size
latest). Both are rows of the remote screen above where the local frame
begins.

The remote script now also captures the screen, right after the
history, in the same round trip. joinScrollbackAndFrame() places the
local frame among the screen rows (findFrameInScreen(): text compare
with escape sequences and trailing blanks ignored, the screen's trailing
blank rows and last content row left out, at least three rows of
overlap with two of them non-blank, highest match wins) and keeps the
screen rows above it between the history and the frame. When the frame
cannot be placed, the join is history over frame as before. The read's
buffer slack grows to 1 MiB, since the screen is outside the tail -c
bound.

Left: the history and screen are two tmux commands, so a row that
scrolls between them (milliseconds) is lost, and a screen that repeats
a whole block can place the frame at the higher copy, losing the rows
between the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:32:04 -05:00
timkjrandClaude Opus 5.5 063e6fc076 fix(terminal): keep the byte-recording load for a shell in the alternate screen, report a lines cut only below the ceiling
Review follow-ups for loading a shell from tmux's rendered history.

tmux adds no history while a pane is in the alternate screen, and a
nested tmux client (remote SSH and Docker cases, or tmux run by hand)
holds its pane there, so a full capture of it is one screen. The
display-message query that reads #{history_size} now also reads
#{alternate_on} (PaneCaptureOptions.capturedAlternateOn), and a request
carrying `alt=recording` (only SHELL_LOAD_QUERY sends it) is answered
the old way on such a pane: the visible frame over the tailed byte
recording. Agents, scroll pulls and Load full history are unchanged.
alternate_on cannot tell a nested tmux from vim or less, so a local
shell with one of those open takes the same fallback while it is open.

A full capture without `lines=` was reported as a 'tail' cut whenever
the pane held more than the configured ceiling (possible on tmux before
3.7), offering a Load full history that could never return more. The
cut is now reported only for a client bound below the ceiling.

The response carries `historyLinesCut` when that bound left lines in
tmux, and the banner then says earlier output may be retained instead
of naming the bytes a later tail cut, which undercount what tmux holds.
A bounded grid tile's shell load sends its own `lines=` (via
SHELL_LOAD_BASE_QUERY). CLAUDE.md, docs/architecture-invariants.md and
docs/api-reference.md describe the shell load, the cut report and the
fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 23:29:19 -05:00
Aamer AkhterandClaude Opus 5.5 afab34608a Merge origin/master (1.41.0) into pr/grouped-rail-placement
One conflict, in i18n.js SKIP_SELECTOR: master added '.tab-name' itself
(the same fix this branch made for zh-CN tab names), so master's line is
kept and this branch's duplicate comment dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:19:01 -04:00
RandalixandClaude Opus 5.5 1010d80b4f feat(preview): folder grid — browse every previewable file of a folder
Folder paths in the terminal and the response viewer are now links
(rooted or ~/ without an extension, relative with a trailing slash); a
click opens a grid of the folder's supported files, newest first, with
subfolder chips and a parent button. A tile opens the file preview,
which pages through the folder with ‹ › / ←/→; the preview of any file
gets a "Show folder" button.

Server: POST /api/sessions/:id/folders grants + lists a folder (local
fs or one ssh round trip for a remote case) under the attachment guard;
GET …/folders/:folderId/files/:name/(raw|preview|thumbnail) serves one
file by bare name through resolveServableAttachmentPath, so browser
requests still never carry a path. Grants live in their own small
registry so a big folder cannot evict the session's attachments. The
attachment raw route's body is now the shared serveServableRaw helper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 21:54:37 +02:00
JD 3e75e7509f fix(uploads): bound the paste-image workspace probe (#516)
The paste-image route wrote under session.workingDir via ensureUploadDir
with no bounded probe, so an upload onto a stalled network mount held one
of libuv's few threadpool workers until the mount answered. Probe the
workspace with probePathKind past the stall cap before the first disk
touch and refuse an unknown mount through describeUnknownPath, the way
POST /api/sessions already does.
2026-10-10 15:03:32 -04:00
timkjrandClaude Opus 5.5 b620e42ffa fix(terminal): load a shell from tmux's rendered history, not the byte recording
A shell's automatic loads (tab select, dropped-output recovery, clear
terminal, a split/grid tile's load) replayed `session.terminalBuffer`,
the raw PTY recording, ahead of the visible frame. That recording holds
full-screen repaints (an ssh'd tmux, less, any TUI: cursor home, then the
rows joined by newlines, prompt on the last row) drawn for the pane
height of the moment. Replayed into a terminal with fewer rows, each
repaint's last newline scrolls and pushes its top row into scrollback,
so one line tmux holds once shows up dozens of times (26 copies in the
case that found this, after the pane went from 24 to 23 rows). Remote
SSH shell sessions hit it constantly: their inner tmux repaints often.

Shells now load a bounded window of tmux's rendered history instead
(`SHELL_LOAD_QUERY`: full=1&tail=1MiB&lines=10000), the same source the
scroll-to-top pull already used. `lines=10000` bounds the synchronous
capture-pane, measured at ~8 ms per 1000 lines (~0.1 s here vs ~0.8 s
for a 100k-line history). Because that bound is shallower than xterm's
scrollback, the server now reports a cut: the capture's existing
display-message query also reads `#{history_size}`
(`PaneCaptureOptions.capturedHistorySize`), and the route marks a full
capture `truncated` / 'tail' when tmux holds more than it read, so the
banner keeps offering Load full history and scroll-to-top keeps pulling
older lines. The byte cap still reports 'capped'. The banner no longer
names an amount it cannot know ("less than 1 KB more") for such a cut.

Agent sessions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:49:29 -05:00
timkjrandClaude Opus 5.5 b33d6da58a fix(remote): load a remote session's scrollback from the remote tmux
A remote SSH session's local pane runs `ssh -t … tmux new-session -A` (or
`attach`), so the local tmux only sees the remote tmux client's full-screen
repaints and keeps a fraction of the history: measured 48 of 279 lines while
output streamed, none after a reattach. The remote pane, where the shell
runs, keeps all of it.

A full-history load (`GET /api/sessions/:id/terminal?full=1`) of a remote
session now reads the scrollback rows from the remote pane in one ssh round
trip (`#{history_size}` + `capture-pane -S -<N> -E -1`, exact `=name:`
target, newest bytes kept by a remote `tail -c`), and the local capture
reads only its visible frame (`-S 0`), spliced below them
(`PaneCaptureOptions.scrollbackOverride`). The local frame and caret are
kept because the live stream keeps drawing into the local pane.

Fail-soft and bounded: connection options from buildSshConnectionArgs(),
the shared remote-ssh limiter (a slot waited for longer than 1.5 s is given
up), a 5 s timeout, and a 30 s per-host back-off after any failure, which
keeps the local capture as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 12:49:09 -05:00
JD 34c903da41 feat(mobile): phone Attach button and camera-roll video uploads
A phone-only toolbar Attach button hands the agent a photo or a video with the
keyboard down. It owns its own file input, positioned over the button so iOS
anchors the picker popover to it; the picked paths land on the prompt as unsent
text, so Compose adopts them together with anything already typed and Use
terminal keyboard hands a single-line draft back the other way.

Videos take a second path on the same route: ?kind=video streams the file to
disk under MAX_PASTE_VIDEO_BYTES (1GB, env CODEMAN_MAX_PASTE_VIDEO_BYTES)
instead of buffering it, sniffs the container from the first bytes as they
arrive, drains a mislabeled body unwritten and unlinks on any failure. It draws
on its own 3/min per-(IP, session) rate bucket beside the image path's 30/min:
at 1GB each, 30/min would be 30GB of disk a minute. Without the flag the route
is byte-identical to the image-only version, same 50MB cap and same bucket.

One client classifier, _promptAttachKind (MIME first, then the name's
extension), drives every pick and paste surface: the Attach button, terminal
drop, the tile grid's drop, the Compose and paste-dialog pickers and the
clipboard handlers. A pick that lands while the tile grid owns the terminal
goes to the PTY rather than the parked main-terminal overlay.
2026-10-10 13:40:04 -04:00
JD 551a7f2792 feat(telemetry): read out Claude Code's prompt cache state per session
Claude Code's statusline has carried a prompt_cache object since v2.1.251, and Codeman's statusline exporter already received it, but StatusTelemetrySchema stripped the whole object, so Codeman never knew whether a session's conversation was still cached. Keep it and show it.

StatusTelemetrySchema now lists every documented scalar field null-tolerant (several are documented as possibly null, and the object gains fields across CLI versions, so a strict shape would 400 the post and blank the footer), while parsePromptCache normalizes only the subset Codeman stores, so a hostile localhost payload cannot bloat state. Session.promptCache rides toState with a change-only session:promptCache event, and the route broadcasts the stored object.

The Session Options (Respawn tab) readout shows warm-until or cold with the re-read stake, the cache hit rate, the last miss's causes, and, for Claude Code's own idle compaction (recache_tokens_if_cold null while expected_rebuilds rose), a "compacted at HH:MM" note that is never raised as an alert. setPromptCache stamps that on a new object rather than mutating the parsed report, so the stamp reaches the broadcast. Sessions without a statusline of their own also get a cache:until HH:MM / cache:cold group in the terminal footer. The last report is restored on a Codeman restart (like displayModel) so the readout is not blank until the pane's next render; the readout is time-aware, so a value whose expiry has passed reads as cold, not stale-warm. A launch without the exporter records why (collection off, workspace statusline, remote) so the row says the cause instead of "not reported yet" forever.

This is the sensor a later opt-in keep-warm would read, and the TTL it reports is the source of truth rather than the plan tier.
2026-10-10 13:09:32 -04:00
Devvyn 0ae679553b fix(cases): show the real reason when Link Existing rejects a case
POST /api/cases/link passed a fixed 'Invalid request body' to parseBody, which replaced the
schema's own message, so a bad name and a bad folder path (a leading ~, a character a session
working directory cannot have) looked the same. Drop the override so the first validation
issue is returned.
2026-10-10 13:50:04 +08:00
Codeman maintainer e2453fe97b fix(terminal): partial-history notice only when there is history to load
The "Showing the most recent 1.0 MB of this session. 4.8 MB more may still
be retained." bar covered the top of the terminal on nearly every tab switch,
and for fullscreen claude panes it could never be acted on: `truncated`
measures the server's byte stream, while Load full history asks tmux, which
holds no scrollback for a pane that lives in the alternate screen.

- The capture reads `#{history_size}` in the cursor query it already makes
  and the terminal route returns it as `paneHistoryLines`.
- A reported 0 hides the notice; a positive count replaces the byte gap in
  the message.
- The notice is lazy: it appears once a wheel/touch gesture reaches the top
  of the browser buffer (after the pull that gesture starts), leaves when
  the user scrolls back to live output or switches tabs, and its dismissal
  sticks per session until reload.
- formatHistoryBytes no longer prints "1024 KB" just under 1 MiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 05:56:37 +02:00
Codeman maintainer 29d8ca16b4 fix(mcp-sync): route tests never follow COPILOT_HOME, and docs name Copilot (#581)
The route test cleared only the registry CLIs' relocation vars, so with
COPILOT_HOME exported it wrote its fixture into that real Copilot config.
It now clears the sync-only tools' vars too, and Copilot's install probe goes
through the test's own installed set instead of the machine's PATH.

Docs: CLAUDE.md, the API reference and the Settings reference name COPILOT_HOME
and the sync-only table; a missing comma in docs/cli-registry.md; the
mcp-sync.ts overview and the Sync confirm mention Copilot.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 03:41:38 +02:00
Codeman maintainer cea199651a Merge pull request #574: feat(uploads): write prompt uploads to a hidden, self-ignoring .codeman-uploads/ folder
Conflicts resolved against the release branch: docs/api-reference.md keeps
both new sections (codeman agent CLI, then Prompt uploads);
test/test-ports-guard.test.ts takes the release side (#570 already removed
the legacy list); test/paste-image-dir-shared.test.ts keeps #570's ephemeral
port and this PR's bounded-path-probe mock.

Also at merge: the Docker sentence in the route comment and the API reference
is narrowed to owned cases, since an adopted container mounts nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-10 03:39:33 +02:00
Codeman maintainer 6db526dbad Merge pull request #562: fix(cli-install): install npm CLIs to ~/.local when the npm global prefix is not writable 2026-10-10 02:53:29 +02:00
Codeman maintainer 2cc6ea1cfd Merge pull request #557: feat(cli): codeman agent — session-to-session verbs for every CLI mode (#445, phase 1) 2026-10-10 02:53:28 +02:00
Codeman maintainer c5ddb77adc Merge pull request #570: test: bind every test server to an ephemeral port (#440, 2/2) 2026-10-10 02:53:27 +02:00
Aamer Akhter 5fb56b0889 fix(tabs): put child follow behind a setting, stop unpinning children on order PUTs
Review fixes for "place new, child and web tabs into groups".

- Spawned tabs follow their parent only with the new synced setting
  spawnedTabsFollowParent (App Settings > Appearance > Tabs, off by default).
  TabLayoutService.sessionCreated() reads it fresh through a
  childrenFollowParent dep: off, a spawned session lands after its parent and
  is marked manual in the same write; on, it follows until moved by hand. The
  row menu shows the placement note and "Follow parent again" only when on.
  "New session" in a group menu is unaffected. zh-CN label and a
  Settings-Reference row.
- applyLegacySessionRank marks a child manual only when the requested order
  splits it from its parent's block. A PUT of the current order, or one that
  moves a family together, leaves every stored placement alone.
- The browser never echoes a create or delete through PUT /api/session-order,
  with or without groups: the layout service already re-projects the order and
  broadcasts it, and syncSessionOrder adopts it.
- The placement note in the row menu is plain text described by the menu, not
  a focusable menu item.
- A tab that first appears inside a collapsed group briefly marks that group's
  header.
- i18n skips .tab-name, so tab names are never translated.
- Tests: legacy-order cases for an unchanged order, a family reorder and a
  split; service cases for the setting and for order PUTs; the scheduled
  registration test now exercises register/undo; the grouped-create test binds
  port 0; one route case runs through the real layout service.
2026-10-09 20:34:37 -04:00
Aamer Akhter b8ccfc33d1 feat(tabs): place new, child and web tabs into groups
New sessions, spawned child sessions and saved web tabs now land in tab
groups instead of always starting in Ungrouped.

New session in a group:
- The group menu gains "New session" (first item). It expands a collapsed
  group and runs ONE launch with the current case and run mode; a launch
  already in flight is reported, not queued.
- run({ count, tabGroupId }) threads the group through every launch path
  (local POST /api/sessions, remote/docker and external-CLI
  /api/quick-start). The count overrides the steppers for that launch only.
- Server: POST /api/sessions and /api/quick-start accept an optional
  tabGroupId (Zod, 1-100 chars; null/empty is a 400). The routes pass it to
  addSession(), and TabLayoutService.sessionCreated() places the session at
  the end of that group in the SAME versioned write that first records it.
  The session is added to the live map inside the owner lock (with a
  rollback when the write fails), so no queued layout edit can commit it
  first. The group is looked up in the creating owner's own layout, so an
  unknown, deleted or foreign id is ignored instead of failing the spawn. An
  explicit group is a hand placement: a child placed this way is manual.
- The create response carries the committed tabLayout (only when tabGroupId
  was sent), adopted before the new tab is drawn. Responses without the
  field are unchanged.

Child sessions (parentSessionId / X-Codeman-Parent-Session):
- A child nobody placed follows its parent: the server's existing
  normalization already puts it after the parent's rows; the browser now
  draws a not-yet-stored child there too (project({ parents })) and, while
  groups exist, adopts the server's order instead of echoing its own append
  back through PUT /api/session-order (which pinned every child manual).
- The row menu says where a child sits (follows, placed by hand, parent
  closed, parent loop) and offers "Follow parent again" only to a hand-placed
  child. A child whose parent is gone stands on its own and is never
  re-adopted.

Saved web tabs:
- Web tabs are layout refs like sessions; openness stays per device. A
  grouped web tab shows (and takes an Alt+N slot) only while open here and
  reopens where the layout keeps it. Pinned by tests; no behaviour change.

Unchanged: the flat rail and header strip (markup, HTML5 drag, append and
save on create), the tree semantics and single tab stop, sessionOrder and
Alt+N, and every request that does not send tabGroupId.
2026-10-09 20:16:45 -04:00