The README hero (both languages) is the same six-tile CRT loop, now with
the header's live stats strip: WS, CPU, memory and the Claude 5H/7D plan
usage chip, captured from a live Codeman with real numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The overlay no longer hides for any scrolled-up viewport, only when the
cursor row leaves the screen. The changeset gains the echo package's patch
bump and the #576 entry.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The README and wiki tour image still showed the 1.7.0 UI. The new one is
a live capture of 1.40.0 (compact header pills with the plan-usage chip
beside them, File Viewer and Tiles buttons, tab logos, Run CC) with the
same three callouts: session tabs, live plan usage, one-click Run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- zh-CN entries for the two new rows (label and description).
- The toast test header still described the dropped drawer logging, and the
noise test typed three members nothing uses any more.
- clickNotification's closing brace had been re-indented by accident.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Custom model endpoints were never gated on the saved flag; the PR's changeset
and JSDoc were narrowed in review, the test header was not.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- docs/browser-testing-guide.md still described the shrink-only legacy list
and the mobile suite's fixed ports; both are gone.
- CLAUDE.md: name the one fixed port left (codex-predictive-echo's separate
lab server on 3222), keep "Never 3000", and say the guard refuses a fixed
port rather than that it checks boundPort is read.
- The tui-client comment described the old "port + 1" dead port.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The port guard refuses a WebServer built on a fixed port. It now uses port 0
and reads server.boundPort, like every other test server.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The hero GIF is now six live agents (DeepSeek Harness, Claude Code, Pi,
Codex, OpenCode, a shell) powering on and off in the tile grid, captured
frame-stepped at 60fps from a real instance. Replaces the July subagent
demo in both READMEs; the old GIF file stays in docs/images.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A grid tile or the split's Pane B (TerminalTile) left the mouse wheel to
xterm for a session running Claude's fullscreen renderer (claude 2.1.187+
with mouse tracking on, cliMouseTracking). That renderer scrolls its own
transcript on SGR wheel reports, which the primary pane sends it, while the
tile's xterm holds only Codeman's replayed repaint frames (tmux keeps no
history for such a pane). So in a grid of fullscreen Claude sessions the
wheel either scrolled nothing or dragged stale frames, Claude's pinned input
box with them, up the tile, and Claude's transcript never moved. This was
tile-grid-plan follow-up 4.
The tile now forwards the wheel the way the primary pane does
(TerminalTile._maybeForwardWheelToCli): the primary pane's own gate,
_shouldForwardWheelToApp(ev, target), asked for the tile's terminal and
session; the cell from _clientPointToCell(x, y, tile.terminal); a scrolled-up
viewport snapped to the live screen first; and the reports flushed through
the tile's own 40 ms coalescer to the tile's session (the primary queue
flushes to the active session). The encoding moved into pure helpers in
terminal-ui.js, CodemanTerminalInput.wheelDeltaWholeLines and
sgrWheelReports, which the primary pane's _wheelScrollLines and
_sendSyntheticSgrWheel now call too, so the two panes send identical bytes.
Shift+wheel, the explicit local-scrollback gesture, was dead in every tile
off macOS: Chrome on Windows delivers it as a horizontal wheel (deltaX), and
xterm's own scroller turns a Shift+vertical wheel into a horizontal one. The
tile now scrolls it itself (_maybeScrollLocalOnShift: scrollLines() on the
dominant axis, sub-line travel carried over, a shell tile's history pull
still asked on the way up), as the primary pane's capture-phase handler does.
Unchanged: inline Claude, opencode and older Claude still take the
PageUp/PageDown route (#555), shells and other modes keep xterm's own plain
wheel, and a tracking xterm or the alternate buffer stays xterm's.
Tests: test/terminal-tile-scroll.test.ts covers forwarding (geometry, tick
cap, coalescing, viewport snap, the tile's session rather than the active
one, Shift/tracking/alternate exclusions, byte equality with the primary
pane) and Shift+wheel (Windows deltaX shape, sub-line carry, shell history
pull). test/terminal-tile-scroll.browser.test.ts adds a real-Chromium case
with trusted page.mouse.wheel() events.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A failed upload only showed a count, so a remote session's refused upload read as a mystery instead of a rule the user could act on. The server already returns the reason in its error envelope and the per-file error carries it, so I surface the first one in the toast. One reason is enough when a whole batch fails for the same cause.
Anything typed on a phone vanished whenever the terminal viewport was not scrolled fully to the bottom. Codeman parks the viewport a few rows above the bottom on purpose (scrollToLastNonEmptyLine after a tab switch, or after the keyboard drops and the terminal regrows, so trailing blank rows stay off screen), and the overlay hid itself on a bare viewportY !== baseY test even though the prompt and the cursor were on screen. The text was buffered the whole time and Enter still sent it, which is what made the keyboard look dead.
The overlay now gates on whether the cursor row is inside the viewport (promptRowInViewport), in both the render path and the scroll handler, so a deep scroll into history still hides it. A buffer without cursorY keeps the old bottom-only rule.
Two of Codeman's custom prompt finders made the matching mistake of treating cursorY, which xterm reports relative to baseY, as a screen row; with the gate relaxed that would have painted the text on the wrong line. Both go through cursorViewportRow now, and the Claude finder looks for its glyph from the cursor's screen row up to the top of the live screen only, since a parked viewport shows scrolled-off history whose old composer rows used to be unreachable and would anchor the overlay on the wrong line.
Codeman turns absolute paths in the terminal into links that open the file
viewer, but agents usually report created files as relative paths, which
cannot be clicked. The generated CLAUDE.md now asks for the full absolute
path of every created file in the final reply, and says why relative, ~/
and markdown-link forms do not work.
It also tells the agent about the codeman skill (start, prompt, wait on and
clean up worker sessions) when the skill is available, and how the user can
install it when it is not.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every animation setting now has its own App Settings section, right after
Appearance (owner: easier to find). It holds the Entrance Theme (the former
Entrance Animations row), Tile Animations, and an Open lab button that closes
settings and opens the per-surface lab (?animlab=1). Appearance keeps the skin,
identity and tab settings. New animation settings go in this section;
test/app-settings-structure.test.ts pins it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CI runs in an English locale, where git already answers in English, so the
real-git clone tests stay green even if the LC_ALL/LANG pin from the previous
commit is dropped. A pure assertion on the env is the only check that fails
then. It also covers LANGUAGE, which gettext ignores once LC_ALL is C.
Refs #568
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
classifyGitFailure() matches git's English stderr, but cloneRepository()
spawned git with gitNonInteractiveEnv(), which left the host's locale in
place. Under de_DE.UTF-8 a missing ref came back as FAILED / 422 instead of
REF_NOT_FOUND / 400, and a missing repository as 422 instead of 404; the
clone dialog showed "git failed: Schwerwiegend: …". gitNonInteractiveEnv()
now sets LC_ALL=C and LANG=C, as git-workspace-status.ts already does.
Two tests depended on the locale on their own:
- git-status-routes: the spy runner called git without the production
runner's LC_ALL=C, so "Kein Git-Repository" was not recognised.
- custom-model-run-menu-ui: the modal formats with toLocaleString(), the
test hard-coded 16,384 and 40,000.
Full suite under de_DE.UTF-8: 5 failed before, 0 after.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tiles become the fifth entrance surface (data-tile-anim), switched on in App
Settings > Appearance > Tile Animations and OFF by default: the default is the
grid's own quick fade (`settle`), exactly as before.
A styled tile plays in two beats: its frame enters as it mounts (fly out of
its session tab, dealt from the Tiles button, CRT power-on, beam down from its
tab, cascade, pop, soft), and its screen then plays the terminal pane style of
the entrance theme when its first capture lands, through the same
html[data-term-anim] rules on .tile-body. Each style has its own exit when the
Tiles button closes the grid (back into the tabs, a CRT switch-off, ...).
Picking an Entrance Animations theme presets the tile style (new Launch theme:
tiles fly from the tabs); the theme readout ignores the tile row, so changing
it never shows "Custom". Frames move transform and opacity only (one fit and
one PTY resize per tile), a reload's restore always settles, and nothing moves
under reduced motion. The lab (?animlab=1) gets a Tile grid group, a cascade
order picker and in-place replay / close + reopen.
Also removes the terminal pane's `boot` entrance style (owner decision); a
saved `boot` falls back to off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Settings Reference gets the new row in the Appearance tab table, the
Dashboard's Session tabs section says what the logo is and where to turn
it off, and the CliEntry.shortBadge comment in docs/cli-registry.md no
longer implies every tab always shows a logo.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since 1.40.0 every agent tab draws its CLI logo through the run-mode-dot
slot, and there was no way to turn that off. App Settings → Appearance →
Tabs now has "CLI Logos on Tabs" (showTabCliLogos), right after Tall Tabs.
- Modelled on tabTwoRows: a per-device display key in the server-settings
merge and an optional boolean in the .strict() SettingsUpdateSchema,
loaded and saved by the id appSettingsShowTabCliLogos. Default ON on
every device; only an explicit false turns it off (tabCliLogosEnabled).
- CSS only, no tab re-render: applyTabOrientation() and the pre-paint
script in index.html stamp html[data-tab-logos="on"|"off"] from the
same stored blob (so a reload never flashes the logos), and one rule in
styles.css hides .session-tab .tab-harness and .home-sessions-harness.
The rows space their children with flex gap, so nothing is left behind.
- A live flip resizes every agent tab with no render behind it, so
applyTabOrientation() then re-takes the strip's one-row wrap decision
(updateTabOverflowMode) and re-anchors the lines drawn from tab rects;
a header that changes height reaches the PTY through the terminal
container's ResizeObserver, as any header change does.
- Covered: the header strip, vertical rail, sidebar, grouped rail, ledger
and case clusters and phone chips (all render the same tab markup) and
the desktop home rail. Untouched: tile and split headers, the Run menus,
the welcome launchers. The phone overview rows, the command palette and
the tab action menu draw no logo. The shell's SH pill and the status dot
stay.
- zh-CN for the label and the description.
test/tab-cli-logos-setting.test.ts drives the real openAppSettings() and
saveAppSettings() in JSDOM (the saved PUT body must pass the schema), the
server-settings merge, the defaults on desktop and phone, the live stamp
and its re-measure without a re-render, the real pre-paint script (on,
off, the phone key, the catch fallback), the CSS rule's exact selectors,
and the translations.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the sweep done, LEGACY_FIXED_PORT_FILES and its staleness test go. A second rule flags a raw listen(<number or …PORT>) and a port: with a number or …PORT constant inside listen({ … }) or new WebSocketServer({ … }); a socket path and a lower-case variable pass. CLAUDE.md, AGENTS.md, CONTRIBUTING.md and the wiki's Contributing page lose the mobile exception, and CLAUDE.md names closedPort() instead of port + 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
http/Fastify servers in daemon-control, deepseek-status-shim, session-input-wait and the three tui tests listen on 0 and read address().port. The two "nothing listens here" ports (a fixed 3243, and the server's port + 1) come from closedPort(): bind 0, read the port, close.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The helper cached servers by port, but every mobile file starts exactly one, so the cache was never shared: it is now a Set that stopAllTestServers() walks. PORTS leaves helpers/constants.ts, the nine tests set baseUrl after start(), and the README drops the port column and the pick-a-port step.
Committed without the pre-commit hook: accessibility, subagent-windows and visual-regression were not prettier-clean on master already; formatting them would bury the port lines.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The files on the guard's legacy list build new WebServer(0, …) and read server.boundPort after start(), as sse-tile-grid-filter does. Files with several servers read each server's own port; quick-start's local helper lost its port parameter so the literal 0 sits where the server is built. Header comments that named a port say "ephemeral". No assertion changed except where it embedded the port.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Split the one-shot keep-open intent from the in-flight guard and consume it before the
first await, so a Save clicked while an Apply is in flight closes the modal.
- Refresh the dependent groups only when the settings PUT returned ok (_apiPut answers null
or a non-ok response instead of throwing), and also when only the webhook save failed.
- Find the 'Apply or Save' hint by a data marker, not its English text; add zh-CN strings
for the new toast and title.
- Tests run the real saveAppSettings() (Apply then Save mid-flight, a failed PUT, a webhook-only
failure, a plain Save).
- Narrow the changeset and JSDoc to MCP sync and CLI management; touch the tray comment, the
architecture note and the wiki.
- Delete every spelling of npm_config_prefix before setting NPM_CONFIG_PREFIX, in the Compose
branch too: npm run exports the lowercase key and a sorting /bin/sh let it win. The
operator guard stays on the uppercase key only.
- A prefix that does not exist yet is judged by its nearest existing ancestor.
- The probe is async (promisified execFile, fs.promises.access, SIGKILL on timeout), awaited
before the spawn and only for commands that run npm.
- Tests: lowercase/mixed-case keys, and the decision logic against a fake npm on PATH
(writable, read-only, not yet created, npm missing). Docs: one clause in cli-registry.md.
From the review of #557:
- An id shorter than 8 characters refuses with exit 4 before any request,
on every verb. `rm 9` resolved to whichever session was alone with that
first character (the user's own tab included) and deleted it. Same floor
as the server's PARENT_SESSION_ID_MIN_PREFIX. `rm` no longer claims a
lineage check: "Delete any session except this one".
- `wait --match` help and the README example say the marker must not appear
verbatim in the prompt (its echo matches at once) and show the split form.
- `send` reads the route's wake-on-LAN answers: `buffered` gets its own
line (exit 0), `dropped` exits 1 instead of printing "accepted".
- `--` for a prompt that starts with "-", in the `send` description and in
the one-argument refusal.
- `stripAnsi` builds on the shared one (OSC sequences go too); the
inputRefusal JSDoc sits above inputRefusal again.
- docs/wiki/Driving-Codeman-From-An-Agent.md gets a `codeman agent` section.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent skill teaches the session verbs to claude only (Codeman seeds its
preamble for local claude sessions). An opencode, codex, pi or gemini agent has
the same environment — CODEMAN_MUX, CODEMAN_SESSION_ID and CODEMAN_API_URL are
exported into every pane — and nothing that teaches it the verbs, so
`codeman agent ls|spawn|send|wait|read|interrupt|rm` packages them as commands.
It is a client of the server, like `codeman tui`, and stays out of
`codeman session` (which drives the in-process SessionManager). No new route and
no second transport: everything goes through CODEMAN_API_URL, so auth,
ownership and the per-session waiter cap apply unchanged. Credentials and the
Basic header come from src/codeman-credentials.ts, in the same order attach and
the TUI use.
Invariants, each in test/cli-agent.test.ts:
- Refuses outside a Codeman session (CODEMAN_MUX=1 + CODEMAN_API_URL); never
guesses a URL.
- `send` takes the prompt as ONE argument (an unquoted multi-line `$(…)` would
otherwise be split by the shell and re-joined into one line), transmits
printable text plus Enter only, and refuses multi-line input loudly instead of
letting sendInput weld the lines. No resend loop of its own: the server's
SubmitVerifier owns the swallowed-Enter case. ESC exists only as `interrupt`,
which never appends Enter.
- `rm` fails closed: empty id, an unprovable self id, or a prefix match in
either direction refuses.
- Nothing mode-shaped in the CLI: the readiness mark `spawn` waits for comes
from the registry (new `capabilities.composerReadyMark`: claude's composer
hint `shift+tab`, deepseek's `❯`), `read` relies on the route's own answer
dispatch, and a `stop`/`blocked` the session cannot fire is the server's 400,
passed through. A `/wait` timeout is a 200 with `timedOut`: exit 2 with a
neutral line, not an error. A worker that dies during spawn's readiness wait
is exit 3, like every other wait.
- `X-Codeman-Agent-Origin` rides only spawn's quick-start, the one request that
may create a case directory; every other verb leaves it off. Server-side,
test/routes/agent-case-marker-routes.test.ts pins that a POST /api/sessions on
an existing workingDir is never labelled, header or not, and that quick-start
labels only a directory it creates.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`readCodemanEnv()` in cli.ts and `parseEnvFile()`/`readCodemanCredentials()` in
tui-client.ts were two copies of the same `.env` reader (the TUI's said so in a
comment), and `codeman agent` was about to need a third. They move into
`src/codeman-credentials.ts`; `codeman attach` and the TUI read from it, and the
TUI keeps re-exporting its names so nothing that imports them changes.
The lookup order is one pure function, `credentialsFrom(env, fileEnv)`: each
field from the environment, then the file, username defaulting to `admin` — the
order attach and the TUI already used. `readCodemanCredentials` takes the
environment as a parameter so a caller with its own (the agent CLI's guard) gets
the same answer. The parser now also tolerates an `export ` prefix, which the
agent skill's preamble already accepted in the same file.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Switches such as MCP server sync only unlock their controls once saved, and
Save closed the modal, so the user had to reopen Settings to continue. Apply
runs the same save, keeps the modal open and refreshes the dependent groups
(MCP sync, custom model endpoints, CLI management) in place.
installEnv() only redirected NPM_CONFIG_PREFIX inside the Docker container
(CODEMAN_IN_CONTAINER=1). On a native install with a root-owned system node
(prefix /usr) `npm install -g @deepseek-ai/dsh` run as the server user died
with EACCES (exit 243), so DeepSeek, pi and other npm-based CLIs could not be
installed from Settings. Ask npm for its global prefix and, when the server
user cannot write it, redirect to $HOME/.local like the container path does.
An operator-set NPM_CONFIG_PREFIX and a writable prefix are left alone.
marked emits no heading ids and, with <base href="/">, a bare #section href points at the dashboard root, so [Install](#installation) in the File Viewer did nothing. The shared click delegate now resolves fragment links against the rendered document: GitHub-style slugs in data-md-anchor (never ids, so a heading cannot capture an app element), case-insensitive and percent-decoded, repeats numbered -1/-2, # = top, explicit ids supported, an unmatched fragment ignored instead of navigating.
Tests: slug/assign/find unit tests (CI gate) and a real-browser test clicking links in a File Viewer document, which fails without the change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JrzFKEdBLwVfu6ev2ZscJS
The phone keyboard's Path key (insertTerminalText) and its clear-prompt key
(clearTerminalInput) always wrote to the main pane: into its local-echo
overlay, or to the active session. With the tile grid open that overlay is
parked behind the grid, so a picked path landed there unseen and only reached
the session later, and with the split open a path meant for Pane B went to
Pane A.
Both now ask _focusedPane() first. When a tile or Pane B holds the keyboard,
the path is sent to that pane's session through the exactly-once queue, and
clearing sends Ctrl+U there (those panes have no overlay, so the TUI owns the
line), then the pane's own terminal takes focus. The main pane's behaviour is
unchanged. Left over from the final checkup's dictation fix (c10), which
covered voice input only.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>