Commit Graph
20 Commits
Author SHA1 Message Date
d fei 631386d3f7 fix(cjk): forward Ctrl/Alt-modified navigation keys to the CLI
claude advertises "Jump to bottom (ctrl+End)", so that chord has to actually
reach it. But PASSTHROUGH_KEYS carried only the bare forms (End -> \x1b[F) and
CTRL_KEYS held just six letters (c/d/l/z/a/e), which cannot express End. Ctrl+End
therefore failed in both directions:

- with an empty composer it went out as a bare \x1b[F, the modifier silently
  dropped, so the CLI received a plain End;
- with text in the composer the forwarding branch requires empty, so nothing was
  forwarded and the browser default applied — the caret jumped to the end of the
  draft, which is the "the shortcut now edits my input box" the user saw.

Encode them as CSI 1;<mod><final> instead, and forward Ctrl/Alt-modified
navigation keys whether or not the composer is empty: they are commands for the
CLI, and the composer has no editing semantics for them worth preserving (bare
Home/End still use the old table and edit locally).

⚠️ Bare Shift is deliberately excluded: Shift+arrow selects text in the composer,
a real editing gesture that must stay local. Shift held together with Ctrl/Alt is
still encoded into the modifier mask.

(cherry picked from commit 3fbaadadfb)
2026-09-14 23:56:19 +02:00
d fei b3a6ba2eb6 feat(terminal): make Shift+drag select, and right-click copy the selection
In a native terminal running a TUI with mouse tracking on (claude, codex), Shift
is the "let me select text" modifier: it bypasses the application's mouse
reporting so the emulator selects locally. Users bring that habit here, where it
did nothing — measured, `hasSelection` was already false during a Shift+drag and
no clearSelection call ran at all, because there was never a selection to clear.

The mismatch is that the two Shifts mean different things. xterm reads Shift as
"force selection", but that path is only taken when the application really has
mouse tracking on. The server strips the mouse DECSETs for claude/codex/gemini
(isAltScreenStripMode), so xterm's mouseTrackingMode is permanently `none`, that
branch is unreachable, and Shift instead lands in _onIncrementalClick — which
EXTENDS an existing selection. Extension is a no-op while selectionStart is
empty, so the drag had no anchor.

So plant the anchor xterm is missing. The listener sits on the capture phase of
the `.xterm` root, an ancestor of the `.xterm-screen` that SelectionService binds
to, and therefore runs before xterm's own mousedown; xterm then extends from our
anchor and the drag behaves like any other. Length is 0 so a Shift+click without
a drag does not select a stray character. An existing selection is left alone —
that is a genuine extend gesture, and xterm handles it correctly.

Right-click copies the selection (the mintty/PuTTY convention), completing the
gesture: until now there was nowhere for a finished selection to go. With no
selection the native menu is not hijacked — taking it away while offering
nothing in return is a pure loss.

(cherry picked from commit 7ab5015737)
2026-09-14 23:56:19 +02:00
d fei 47ee49128c style: match the prettier version the lockfile pins
Format check failed twice, on different files each time, because three prettier
versions were in play: package.json says ^3.4.0, package-lock pins 3.8.3 (CI runs
npm ci, so that is the one CI uses), and the local node_modules had 3.9.6. Files
formatted with 3.9.6 were then "fixed" with 3.4.2, pushing session-routes and
system-routes onto a third style — every version change moved the failure to a
different set of files.

Line-break placement in `await import` and a union type only; no logic changes.
2026-08-29 23:49:04 -07:00
d fei 8e5e207386 fix(docker): send the probe body as an object, and explain an unreachable container
The run menu still offered every mode for an attached container. The browser's
actual request showed why:

  POST /api/docker-cases/adopt-preflight -> 400
  {"error":"Invalid input: expected object, received string"}

_api serializes `body` and sets Content-Type itself, and three call sites each
passed an already-stringified body, so it was encoded twice and the server saw a
JSON string where it expects an object. curl was fine throughout, so nothing in
the server logs pointed at it.

Also fixes the design defect underneath: a failed probe fell through to "do not
gate", which silently offered every mode. When the container has been recreated,
is stopped, or the engine is unreachable, the user sees claude, clicks it, and
it can only fail — with the reason visible nowhere. A failed probe now hides
every agent mode (Shell needs no CLI and stays) and shows the server's own
reason at the top of the menu.

Two static guards switched from a character window to brace matching. They
sliced between two call sites, and _loadRunModeHistory's call appears above its
definition, so the slice came out empty and the assertion verified nothing —
the same trap twice in one file.
2026-08-29 23:31:28 -07:00
d fei 5452ad5c5a feat(docker): add a folder picker to both path fields
Both paths in the adoption form had to be typed. Each gets a Browse button
using the same path-input-group markup Link Existing uses, so the two look and
behave alike.

What they can browse differs, and that is the point. The host workspace path
reuses the existing host picker. The container workdir cannot: an adopted
container has nothing mounted at a matching host path, so a host listing would
be a different filesystem — and getting this field wrong is the source of the
opaque OCI chdir error at launch, which makes it the field that most needs to
be clickable.

Adds a read-only POST /api/docker-cases/browse: one `ls` through docker exec, no
writes, no lifecycle, path shell-escaped like every other value. `ls -Ap` marks
directories with a trailing slash and keeps names with spaces intact.

PathPicker takes an optional fetchListing source rather than being forked: the
container variant only swaps where the rows come from, and reuses the rendering,
navigation, Up and Choose/Select unchanged.
2026-08-29 23:31:28 -07:00
d fei 23ab2e77fd fix(files): give the picker a root when the server runs as root
Link Existing's Browse did nothing: GET /api/filesystem/browse answered 403
"No filesystem browse roots are available".

Two rules were fighting. /root is a default blocked tree in the attachment
guard, and Codeman running as root — containers, plenty of servers — makes
homedir() exactly /root, so the picker's own allowlisted Home root was blocked;
the other candidates live under it or do not exist. The root list came out
empty and there was nothing the user could open.

The blocked trees exist to keep ~/.ssh and friends out of reach, not to seal off
the user's own home. Only trees that would swallow a configured root whole are
dropped now: /root goes when Home is it (or sits inside it), /etc holds no
configured root and is untouched. Secrets stay protected — isSensitivePath
independently matches .ssh/, .env and credentials* at any depth, and it is what
the directory probe asks about.

⚠️ Navigation must reuse the same narrowed list the roots were chosen with.
Handing the raw trees downstream admits a root and then refuses every path
inside it, which reads as a picker that opens and does nothing.
2026-08-29 23:31:28 -07:00
d fei 3685ad85bc fix(docker): stop requiring the CLI on the host for a container session
Attaching a container, picking claude and hitting Run gave one line —
`execvp(3) failed.: No such file or directory` — and the run-mode menu offered
every mode. Three separate defects, found on a real deployment.

TmuxManager.createSession resolved the CLI directory without distinguishing a
docker session, so a host with no claude threw, the catch fell back to a direct
PTY, and that PTY exec'd the CLI on the HOST. The failure surfaced as a bare
execvp error naming nothing. A docker session runs its CLI inside the container;
the host does not need it. All eight modes now sit behind a cliRunsInContainer
guard, and whether the container has the CLI is settled by the adoption
preflight or the image gate before launch.

The running check used a bare double quote and command substitution. The whole
chain is embedded in an outer `bash -c "…"`, so the unescaped quote closed that
string early and the remainder was re-tokenized. It is now a `grep -qx` pipeline
using only the single-quote form every other line in the builder already uses.

Claude Code refuses --dangerously-skip-permissions as root. Our base image runs
a non-root user, so an owned container never hit this; an adopted container's
user belongs to its owner and is frequently root, and keeping the flag killed
the pane with a message visible only inside the container. The preflight now
reports runsAsRoot and the launch chain drops the flag for it.

The menu also showed every mode because the container CLI probe only started
when the menu opened. It is warmed when the case is selected instead.
2026-08-29 23:31:28 -07:00
d fei 06e7cbe286 fix(docker): probe the container's CLIs live instead of trusting attach time
Storing the container's CLIs on the case at attach time left two gaps: a case
linked before that field existed has none at all, and a container's CLIs can be
installed or removed long after it was linked. A real deployment hit the first
one — the host had only codex, the container only claude, and with no stored
list the menu still gated on the host and hid the mode that actually worked.

The probe now runs when a container case is selected, reusing the existing
adopt-preflight endpoint, so there is no new backend surface. Results are cached
per case for the page's lifetime, since the menu opens often and the probe is a
`docker exec` round trip; a concurrent probe for the same case is deduplicated
with an in-flight marker.

A failed probe leaves the cache empty, which the caller reads as "unknown" and
therefore does not gate. Hiding every mode because one probe failed is worse
than offering one that turns out to be missing, which the launch path already
refuses with a specific message.

The repaint only happens while the menu is still open, so a late answer cannot
make the list jump under a user who already closed it.
2026-08-29 21:19:17 -07:00
d fei 8b20f5b1f8 fix(docker): probe container CLIs by their real binary name
The adoption preflight used the mode name as the binary name. claude, codex,
opencode, gemini and pi happen to match, so it never showed — but antigravity
ships as `agy` and deepseek as `dsh`, so a container that has either was
reported as not having it, and the mode was silently dropped from the case.

Adds a MODE_BINARIES map, single-sourced with defaultDockerCommandForMode, which
launches those same binaries. Probing and result filtering share one `binaryFor`
so the two cannot drift apart.
2026-08-29 21:02:19 -07:00
d fei 2f83a37c6d feat(docker): take run-mode availability from the container
The run-mode dropdown hides CLIs that are not installed on the HOST (#201). That
is right for local sessions and wrong for a container case, whose agents run
inside the container: a host with no claude installed hides the mode while the
container ships one, which is exactly what happened on a real deployment.

The adoption preflight already probes what the container has, so that result is
persisted on the case and surfaced through CaseInfo. Docker cases gate on it;
every other case keeps the host probe unchanged.

An absent list reads as "do not gate" rather than "nothing available": an owned
container runs our base image, which ships every CLI, and treating unknown as
empty would leave the menu with Shell alone.
2026-08-29 21:02:19 -07:00
d fei 34c12ca18b feat(docker): make the container field a picker you can also type into
Typing a container name from memory is error-prone. The field becomes a native
datalist: pick from the engine's containers, type to filter, or type a name that
is not listed (the engine may be remote, or the container may not exist yet).
A datalist gives all three natively, so no dropdown state machine is introduced.

Adds listDockerContainers and GET /api/docker-hosts/:hostId/containers, following
the listRemoteCodemanSessions discovery precedent: read-only and never throwing,
so an unreachable daemon returns an empty list and the field degrades to plain
text instead of erroring.

Stopped containers stay in the list, sorted after running ones and labelled.
Attaching does require a running container, but hiding stopped ones turns "my
container is not in the list" into a dead end, while showing
`Exited (137) 8 days ago` says exactly what to fix.
2026-08-29 21:02:19 -07:00
d fei e2f750cb30 i18n(docker): translate the attach panel, and unblock translation
The new strings were English only. Adding entries surfaced a deeper problem: the
translator matches whole text nodes and skips `code`/`pre`, so an inline `<code>`
mid-sentence splits a hint into fragments that can never match an entry — which is
why the panel's existing "Build it once with <code>...</code>" hint was never
translated either.

Drops the inline markup from the new hints so each is a single text node, then
adds the zh-CN entries. The brand name goes through the existing {name}
placeholder.

Server-side error bodies are deliberately not added: the client receives them
already interpolated with a concrete container name, so a template key could
never match.
2026-08-29 21:02:19 -07:00
d fei bb45909169 feat(docker): link to container attach from the Create New tab
Attaching lived only on the Docker tab, but the place users look for anything
container-shaped is the "Run in an isolated Docker container" checkbox on Create
New. A feature nobody can find is a feature nobody has.

Adds a one-click link there that switches to the Docker tab, turns the toggle on
and focuses the container field. Reuses switchCaseModalTab and the existing sync
helper; no new CSS.
2026-08-29 21:02:19 -07:00
d fei c98a59d709 fix(docker): verify the container workdir and end the probe with exit 0
Two defects that only a real container exposes.

The probe chained `command -v X && echo X` with semicolons, and a script's exit
status is its last command's. A container without the last probed CLI made the
whole `sh -lc` exit 1, so a perfectly healthy container with tmux and claude was
reported as "could not exec into the container". A missing CLI is data here, not
failure, so the script now ends with `exit 0`.

containerWorkdir defaulted to hostWorkspacePath. That default holds for an owned
container only because the create-time bind mount puts the host directory at that
exact path; attaching mounts nothing, so the two are independent facts. A host
path absent inside the container makes `docker exec --workdir` fail with an OCI
chdir error that surfaces in the pane as a bare "execvp failed". The preflight now
proves the directory exists inside the container and refuses at link time.
2026-08-29 21:02:19 -07:00
d fei bc55b6b0da feat(docker): add the attach-an-existing-container panel
The Docker tab gains an "Attach to an existing container" toggle. Ticking it
swaps the create-time fields (image, network, advanced) — which describe a
`docker create` attaching never runs — for the container name, and routes the
submit to the adopt endpoint.

Reuses the existing linkDockerCase flow end to end: only the final call differs.
The docker-host upsert still applies, since it is what resolves the
engine/context/daemon for `docker exec`; its create-time fields are simply never
read for an attached case.
2026-08-29 21:02:19 -07:00
d fei 15eebde832 feat(docker): attach a case to an already-running container
Docker cases could only run in a container Codeman created itself. Attaching to
one the user already built and runs means Codeman must leave that container's
lifecycle completely alone, which the launch chain could not do: it was
`image inspect` -> `inspect || create` -> `start` -> `exec`.

Adds `DockerCase.owned`, mirroring the `owned:false` contract remote-SSH already
uses for attached sessions. Absent (every existing case) means owned, so current
behaviour is byte-identical. `false` means the container belongs to the user and
Codeman may only exec into it.

The launch chain for an attached container only looks, then execs: no image gate
(the image is theirs), no create, and no `start` — starting a container we do not
own is the very mutation attaching promises not to perform. A missing or stopped
container fails closed with an actionable message instead. Credential seeding is
skipped too: those copies read from create-time read-only mounts that do not
exist here, and writing host credentials into someone's container is not ours to
do, so its CLIs must already be authenticated inside it.

Four fail-closed guards. buildDockerStopCommand and buildDockerRemoveCommand
throw during pure string construction, so no caller bug can turn into a
`docker stop`/`rm` on a container we do not own; removeDockerContainer refuses
again at the lowest layer; drift reports "none" for an attached container, which
carries no `codeman.confighash` label and would otherwise always look drifted and
409 the launch gate forever; and the orphan reaper skips attached containers
through a check deliberately independent of the two conditions already covering
them.

`owned` is applied AFTER the config hash is computed. dockerConfigHash takes an
explicit field list, so ownership can never shift an existing case's hash — if it
did, every pre-existing case would trip the drift gate at once, and the remedy
the UI offers is "recreate the container".

Adds POST /api/cases/docker-adopt and a read-only
POST /api/docker-cases/adopt-preflight. The preflight refuses at LINK time rather
than at session launch, where the only ways out would be a dead pane or starting
a container we do not own.

Tests assert the negative guarantee directly — that create, start, stop, rm,
restart and kill are absent from the generated commands while `docker exec -it`
and `new-session -A` remain — since it cannot be observed by using the feature.
2026-08-29 21:02:19 -07:00
d fei 7c62b16e5f fix(terminal): bound live xterm backpressure 2026-08-24 19:06:50 +08:00
d fei 458e751a33 fix(terminal): keep shell history loading explicit 2026-08-22 01:55:50 +08:00
d fei dab432b3fd fix(terminal): bound shell history replay 2026-08-21 08:23:31 -04:00
d fei f744719650 fix(terminal): preserve IME punctuation input 2026-08-20 10:35:23 -04:00