mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
jcode (1jehuang/jcode) joins the CLI registry as the `jcode` run mode:
Run menu, phone overview, TUI, quick-start and POST /api/sessions
(`jcodeConfig: { model?, resumeSessionId? }`), local, SSH and Docker
(dedicated agent-image layer), plus codeman doctor and install.sh
through the generated catalogue. Measured on a live jcode 0.98.1 pane.
- Work detection reads jcode's braille status row and `<n>>` composer;
alt screen with mouse strip and PageUp/PageDown paging like opencode.
- Every jcode TUI is a client of one shared per-user daemon whose tools
run with the daemon's env, so no agent skill injection and no JCODE_*
envOverrides prefix.
- New `capabilities.sharedDaemon`: a session kill spares descendants
outside the pane's process group, so closing the tab that started the
daemon no longer cuts every other jcode session off (measured).
- last-response answers an honest empty for an external CLI with no
transcript reader that the pane segmenter cannot read (jcode alone).
- GET /api/jcode/status via the generic registry resolver.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
278 lines
16 KiB
Markdown
278 lines
16 KiB
Markdown
# jcode sessions
|
||
|
||
Codeman can drive [jcode](https://github.com/1jehuang/jcode) (1jehuang/jcode, MIT, a
|
||
coding-agent TUI written in Rust) as a session backend, alongside Claude Code, OpenCode,
|
||
Codex, Gemini, Antigravity, Pi, Grok, DeepSeek, OMP and GitHub Copilot. `jcode` is its own **run mode**:
|
||
its own PTY, its own tmux session, its own tab identity (amber, `JC` badge, a plain ring
|
||
standing in for jcode's halftone donut). It is not a location overlay like Docker or
|
||
remote-SSH cases, and it is not a web tab.
|
||
|
||
Everything here was measured against jcode v0.98.1 on 2026-10-16 (see
|
||
[How it was measured](#how-it-was-measured)). The registry entry in
|
||
`src/config/cli-registry/stock.ts` carries the same facts in its comments and is the
|
||
source of truth when the two disagree.
|
||
|
||
## Install
|
||
|
||
```bash
|
||
curl -fsSL https://jcode.sh/install | bash
|
||
```
|
||
|
||
The installer downloads a checksummed release binary into
|
||
`~/.jcode/builds/versions/<version>/` and links `~/.local/bin/jcode` (its
|
||
`JCODE_INSTALL_DIR` default) to it. The linked `jcode` is a small `sh` **launcher** that
|
||
resolves its own path and execs the `jcode-<os>-<arch>.bin` beside it, so the version
|
||
directory is what matters, not the single file the link points at.
|
||
|
||
Codeman resolves the binary through the generic registry resolver (there is no
|
||
jcode-specific resolver module): the server PATH, then `~/.local/bin`, `/usr/local/bin`
|
||
and `~/bin`, then an interactive login shell. npm carries an unrelated package also called
|
||
`jcode`, so a PATH hit is not trusted on its own: `jcode --version` must print
|
||
`jcode v<semver>` (the real one prints `jcode v0.98.1 (1c7426eb1)`), and anything else
|
||
counts as absent. Check what it resolved:
|
||
|
||
```bash
|
||
curl -s localhost:3000/api/jcode/status | jq
|
||
# { "available": true, "path": "/home/you/.local/bin", "version": "0.98.1" }
|
||
```
|
||
|
||
## Authenticate
|
||
|
||
jcode keeps its own logins: OAuth accounts in `~/.jcode/auth.json`, API keys in
|
||
`~/.config/jcode/<provider>.env`, the default provider and model in `~/.jcode/config.toml`.
|
||
Set them up with jcode itself (`jcode login`, `/login` inside the TUI, or
|
||
`jcode provider add <name> --base-url ... --model ...` for an OpenAI-compatible server).
|
||
jcode can also offer to reuse logins other CLIs left behind; it asks before reading any of
|
||
them. Codeman forwards no credentials into a jcode pane.
|
||
|
||
## What Codeman wires up
|
||
|
||
`JcodeConfig` (per session, persisted in `state.json`, round-trips through respawn and
|
||
server restart):
|
||
|
||
| Field | Flag | Notes |
|
||
| ----------------- | --------------- | ---------------------------------------------------------------------------- |
|
||
| `model` | `--model <v>` | e.g. `claude-opus-4-8`, `gpt-5.5` |
|
||
| `resumeSessionId` | `--resume <v>` | a session id (`session_fox_1792160401272_f5b47ce47d631006`) or its short name (`fox`) |
|
||
|
||
Both values are regex-validated at the route and checked again by the argv engine, and a
|
||
value that fails is **dropped**, never escaped into the command. `resumeSessionId` uses the
|
||
`path-segment` pattern (a leading letter or digit) on purpose: jcode's `--resume` takes an
|
||
OPTIONAL value (a bare `--resume` opens its session picker), so a value starting with `-`
|
||
would be parsed as a flag of its own (`--resume --no-update` is two flags).
|
||
|
||
The Run button sends **no** `jcodeConfig`: jcode has no approval prompts and no bypass
|
||
flag, so there is nothing to opt into (the same shape as pi). A bare spawn is just `jcode`.
|
||
|
||
There is **no** `envOverrides` prefix for jcode: `JCODE_*` is not allowlisted. See
|
||
[The shared daemon](#the-shared-daemon) for why.
|
||
|
||
## The shared daemon
|
||
|
||
Every jcode TUI is a CLIENT of one shared per-user daemon (`jcode serve`). The first client
|
||
starts it, in its own session and process group (`setsid`), and every later client attaches
|
||
to it. It stays the first client's CHILD by parent pid, though, which is what the
|
||
[session kill](#closing-a-tab-never-takes-the-daemon-down) has to account for. The agent and its tools run in the DAEMON, with the daemon's
|
||
environment. Measured: a second client started with `CODEMAN_PROBE=second-client` asked its
|
||
bash tool to `echo "probe=[$CODEMAN_PROBE]"` and got `probe=[]`.
|
||
|
||
Three things follow, and each one is a deliberate decision:
|
||
|
||
- **The agent skill is not injected.** jcode does read `./.agents/skills` (and
|
||
`./.jcode/skills`, `./.claude/skills`), but a jcode agent's tools see the
|
||
`CODEMAN_SESSION_ID`, `CODEMAN_API_URL` and `CODEMAN_MUX` of whichever session started
|
||
the daemon, or none at all if it was started outside Codeman. An injected skill would act
|
||
as THAT session: spawn children under the wrong parent, prompt another session's
|
||
workers. A user-level copy of the skill (`~/.agents/skills`, which jcode also reads) has
|
||
the same problem; this is the same trap as codex's shared `app-server`, only it is jcode's
|
||
normal case rather than an edge.
|
||
- **No `JCODE_*` envOverrides prefix.** Most of that namespace is read by the daemon, so a
|
||
per-session value would do nothing, and where it did act (in the session that happens to
|
||
start the daemon) it would reach every jcode session on the machine. The namespace
|
||
includes variables that run code or redirect traffic: `JCODE_HOOK_*`, `JCODE_SPAWN_HOOK`,
|
||
`JCODE_*_COMMAND`, `JCODE_*_API_BASE`. Configure jcode in `~/.jcode/config.toml`, or in
|
||
the Codeman server's own environment.
|
||
- **Deleting a Codeman jcode session leaves the daemon running**, by design: the other
|
||
sessions live there (next section). `jcode server stop --force` stops it by hand.
|
||
|
||
### Closing a tab never takes the daemon down
|
||
|
||
Codeman's session kill signals every descendant of the pane (by parent pid), so it used to
|
||
kill the shared daemon whenever the closed tab happened to be the one that started it.
|
||
Measured on a beta before the fix: two jcode sessions, the first one deleted, and the second
|
||
one's client looped on `Connection lost - retrying (attempt 9) - server closed the
|
||
connection` with its prompt queued (`⏳`), while a send-and-wait on it timed out.
|
||
|
||
The jcode entry declares `capabilities.sharedDaemon: true`, and for such a CLI the kill
|
||
signals only the descendants still in the pane's own process group
|
||
(`descendantsInPaneGroup()`, `src/proc-tree.ts`, one bounded `ps -o pid=,pgid=`). The daemon
|
||
left that group when it called `setsid`, and everything it runs inherits its group, so the
|
||
daemon and its tools are spared while the client and anything else in the pane's group are
|
||
killed as before. A lookup that fails keeps the old behaviour (kill every descendant). Every
|
||
other CLI is unaffected. Measured after the fix: the kill logs `Sparing 1 process(es)
|
||
outside pane <pid>'s group (the CLI's shared daemon)`, the daemon keeps its pid, and the
|
||
remaining session answered a send-and-wait in 5 s.
|
||
|
||
The flip side: a process a jcode agent started (a dev server, a watcher) is a child of the
|
||
daemon, not of the pane, so closing the tab does not stop it. That is how jcode behaves in a
|
||
plain terminal too.
|
||
|
||
## Approvals and multi-user mode
|
||
|
||
jcode has no approval dialog. Its bash tool runs ungated apart from jcode's own model-side
|
||
reflection gate on risky commands and a hard deny on catastrophic paths. So there is no
|
||
bypass flag to clamp, nothing for the multi-user clamp to force off, and no dialog for
|
||
Codeman to read as `blocked`. In multi-user mode a jcode session is like a pi session: an
|
||
agent that runs tools without asking, under the one OS account every session shares.
|
||
|
||
## Terminal behavior
|
||
|
||
jcode is a **full-screen alternate-screen TUI with mouse tracking on** (tmux
|
||
`alternate_on=1`, `mouse_any_flag=1`, `mouse_sgr_flag=1`). It takes the opencode strip,
|
||
`altScreen: 'strip-mux-and-mouse'`: the mouse DECSETs are stripped, so a drag selects text
|
||
locally (Auto Copy and copy-on-select work) instead of being reported to jcode.
|
||
|
||
Scrolling: jcode pages its own transcript on PageUp/PageDown and also scrolls on SGR wheel
|
||
reports (both measured). It declares `transcriptPageKeys: true`, so over the hollow
|
||
alt-screen buffer a wheel or a swipe becomes PageUp/PageDown instead of doing nothing.
|
||
|
||
Input: a long prompt sent the way Codeman sends one (a bracketed paste, then a separate
|
||
Enter) submits cleanly (measured with a 170-character prompt).
|
||
|
||
### How Codeman reads a working pane
|
||
|
||
At rest the composer row is `<n>> ` (the prompt number, then `>`); a submitted prompt is
|
||
echoed into the transcript as `<n>› <text>`. While a turn runs the composer row reads
|
||
`<n>…` and a status row appears directly above it: a braille spinner (the gemini/omp frame
|
||
set, about 12 fps), then the phase and the elapsed time:
|
||
|
||
```
|
||
⠋ sending… 0s
|
||
⠦ thinking… 2s · https
|
||
⠼ waiting for response… 3s · https
|
||
⠸ running bash · <detail> · 12s · Alt+B bg
|
||
⠼ 12s · 45.1 tps · ↑16k ↓20 (streaming: the time comes first)
|
||
↻ network disconnected, waiting to retry · … · 5s
|
||
```
|
||
|
||
Past a minute the time reads `1m 3s`, past an hour `1h 2m`. At rest that row is empty or a
|
||
dim tip, never a spinner, and the turn leaves a summary row (` 21s · 199.6 tps · ↑17k ↓2k`)
|
||
that has a time but no spinner. The `workingLine` pattern therefore needs a spinner (or the
|
||
`↻ network disconnected` row) at the start of a row AND an elapsed time on it.
|
||
|
||
The composer glyph is the bare `>`. tmux redraws only the cells that changed, and on the
|
||
composer row at the end of a turn that is the `…` turning into `>` (the recorded client
|
||
stream carries `\x1b[43;2H...> `), never the prompt number, so `>` is what reliably arrives.
|
||
Mid-turn stream chunks never contain it. The glyph only ARMS the idle check; the screen and
|
||
the silence decide.
|
||
|
||
Silence is a sound signal for jcode: the spinner repaints the pane at least every 100 ms for
|
||
the whole turn, and at rest jcode sends nothing at all (12 seconds recorded: zero bytes).
|
||
|
||
The submit verifier stays inert for jcode, which is what it wants (it has no early-Enter
|
||
defect): every composer row starts with its prompt number, so no screen row starts with
|
||
`>` the way the verifier requires.
|
||
|
||
### Reading a jcode worker's answer
|
||
|
||
`GET /api/sessions/:id/last-response` returns an **honest empty** (`text: ""`, and
|
||
`messages: []` with `?context=full`) for jcode. The pane segmenter the other TUIs use cannot
|
||
read a repainting alternate-screen stream (measured: it served jcode's welcome banner as
|
||
"the answer"), and without this the route would fall through to the Claude transcript scan,
|
||
which could adopt a Claude conversation from the same folder. Read a jcode worker's screen
|
||
instead:
|
||
|
||
- `codeman agent read <id> --tail 3000`
|
||
- MCP `read_output` with `source: "terminal"`
|
||
- `GET /api/sessions/:id/terminal?tail=3000`
|
||
|
||
There are no `stop`/`blocked` hook signals for jcode: wait for `idle`, or use `wait-output`
|
||
markers, as with every hook-less CLI.
|
||
|
||
## First run: the onboarding wizard
|
||
|
||
A fresh jcode install opens an onboarding wizard ("Welcome to jcode onboarding / Log in to
|
||
OpenAI?") instead of the composer, on every launch, until its launch counter passes 5 or
|
||
10 sessions exist. A prompt typed into the wizard is lost (measured: it wedged that client).
|
||
Before driving jcode from Codeman, run `jcode` once in a terminal and finish or skip the
|
||
wizard, or press Esc in the Codeman pane.
|
||
|
||
## jcode's own defaults Codeman does not change
|
||
|
||
- **Telemetry** is on by default in jcode (install count, version, OS, session activity,
|
||
tool counts; jcode's `TELEMETRY.md`). Opt out with `jcode telemetry disable`, or set
|
||
`JCODE_NO_TELEMETRY=1` in the Codeman SERVER's environment (it is not a per-session env
|
||
override, see above).
|
||
- **Auto-update** is on by default for release builds; `--no-update` is jcode's own flag and
|
||
Codeman does not pass it.
|
||
- **Self-dev mode**: jcode switches into it when started inside a checkout of jcode itself.
|
||
|
||
## Docker cases
|
||
|
||
jcode is not on npm, so the agent image installs it in a dedicated Dockerfile step through
|
||
jcode.sh's installer, then copies the WHOLE version directory to `/opt/jcode` (the launcher
|
||
execs the binary beside it, so copying only the linked file, as the grok and omp steps do,
|
||
would leave a launcher with nothing to run) and links `/usr/local/bin/jcode` to the launcher.
|
||
Root's `~/.jcode` is dropped in the same layer so the image does not carry the ~160 MB
|
||
binary twice. Verified in `node:22-bookworm-slim`, including `jcode --version` as the
|
||
unprivileged `agent` user. Rebuild with the mandatory `--no-cache`:
|
||
|
||
```bash
|
||
node scripts/build-agent-image.mjs --no-cache
|
||
```
|
||
|
||
No credentials are seeded into the container: `~/.jcode` holds the OAuth tokens beside the
|
||
builds, sessions and SQLite caches, and API keys live in `~/.config/jcode`. Sign in inside
|
||
the container instead.
|
||
|
||
## Remote SSH cases
|
||
|
||
`jcode` mode is routed through an interactive login shell
|
||
(`exec "$SHELL" -i -l -c 'jcode'`), because sshd's remote-command PATH does not include
|
||
`~/.local/bin`. Per-session config does not cross ssh and is rejected rather than silently
|
||
ignored; a host whose `jcode` lives elsewhere takes the per-host `commands.jcode` override.
|
||
|
||
## Not supported yet, and why
|
||
|
||
- **A transcript reader** (a real `last-response`, Response Viewer content). jcode writes
|
||
sessions as `~/.jcode/sessions/session_<name>_<ms>_<hex>.json` plus a `.journal.jsonl`,
|
||
which is easy to parse, but pairing a Codeman pane with ITS session file is ambiguous: the
|
||
session JSON's `last_pid` is the shared daemon's, not the pane's, so two jcode panes in
|
||
one folder cannot be told apart from the files alone.
|
||
- **Custom Model Endpoint Profiles** (`customModelInjection: unsupported`). jcode's provider
|
||
is resolved in the daemon, not the pane, so a per-session redirect has nothing reliable
|
||
to land on.
|
||
- **The model in tile headers** (`modelDetect`). The status bar names provider and model
|
||
(`qwen5090 qwen3.8 27b`), but its layout varies by provider and width and was not
|
||
measured enough to anchor a pattern on. A session shows its launch model, if any.
|
||
- **Hook-based `stop`/`blocked` signals.** jcode has lifecycle hooks (`session_start`,
|
||
`turn_start`, `turn_end`, `session_end`), but they run in the daemon too, with the
|
||
daemon's environment, so they cannot tell Codeman which session they belong to.
|
||
- **Ralph, respawn heuristics, token/CLI-info parsing and the `❯` readiness probe are off**
|
||
for jcode, as for every external CLI.
|
||
|
||
A per-session daemon (`jcode --socket <path>`) would give each pane its own environment,
|
||
but it gives up the shared daemon that is jcode's main design point (cross-session swarm
|
||
awareness, a few MB per extra session), so it was not done here.
|
||
|
||
## How it was measured
|
||
|
||
- jcode installed with its own installer into a scratch `HOME` with telemetry off
|
||
(`DO_NOT_TRACK=1`, `JCODE_NO_TELEMETRY=1`) and a short `XDG_RUNTIME_DIR` (the daemon's
|
||
Unix socket path must stay under the `SUN_LEN` limit), so nothing touched the real home.
|
||
- A provider set up with `jcode provider add` pointing at a local OpenAI-compatible server
|
||
(an RTX 5090 serving Qwen 3.8 27B), so no subscription login was involved.
|
||
- jcode run in a scratch tmux socket at 160x45; `tmux capture-pane -p` every 200 ms through
|
||
real turns (a plain reply, a bash tool call, a streamed answer); `pipe-pane` at rest; and a
|
||
tmux CLIENT attached through a PTY to record exactly what Codeman's PTY reads.
|
||
- PageUp/PageDown sent with `send-keys`; SGR wheel reports (`\x1b[<64;40;10M`) pasted raw.
|
||
- A second client with its own environment to prove where tools run.
|
||
- The Docker step run in a throwaway `node:22-bookworm-slim` container.
|
||
- End to end through an isolated beta Codeman built from this branch (its own
|
||
`CODEMAN_INSTANCE`, port and tmux socket): `POST /api/quick-start` with `mode: "jcode"`,
|
||
`/api/jcode/status`, send-and-wait (`wait: "idle"` returned after the turn, 12.7 s for a
|
||
turn with a 5 s tool call, not during it), two sessions with the first deleted, and a
|
||
Playwright pass over the Run menu entry, the `Run JC` button and the rendered transcript.
|
||
|
||
The fixtures in `test/jcode-mode.test.ts` are verbatim from these captures.
|