183efacc turned drag off while the rail search narrows the list, as the
owner asked, by refusing the flat manual rail's dragstart. That also
refused a drag that never reorders anything: with the tile grid open, a
found tab dragged onto a tile or an empty cell set no draggedTabId, so
_acceptTabDrops() (tile-grid.js) saw nothing and the drop was lost. On
the PR head that drop replaced or swapped the tile. The owner's reason
covers the reorder only: a reorder drop is saved for every device,
relative to rows the search hides, while the tile grid is per-device and
lands beside nothing hidden.
dragstart is unconditional again. The rows refuse the reorder instead:
their dragover returns before preventDefault while _tabRailSearchActive()
(the browser shows no-drop and fires no drop there), and their drop
returns before touching sessionOrder, for anything above the row that
might let a drop through. `draggable` stays on, so a cleared search
reorders with the same rows. The grouped rail's pointer drag keeps its
refusal in _onTabLayoutPointerDown(): it has no drop target besides the
rail.
Test: during a search that leaves both rows showing, dragstart is not
cancelled and sets draggedTabId, dragover on another row is not
accepted, a drop there leaves sessionOrder alone and saves nothing, the
real _acceptTabDrops() binder accepts the drag on a tile and hands it
the id, and after clearing the same rows reorder. It fails on the
previous commit, without the dragover guard, and without the drop guard.
CLAUDE.md, the invariants, the Dashboard wiki row and the release
changeset now say reordering by drag is off and a tile drop still works.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A floating subagent or ultracode window whose parent session's row the
rail search hid drew its connector from the viewport's top-left corner.
The hidden row is display:none, and getBoundingClientRect() still
answers it with an all-zero DOMRect, which is truthy, so the
`if (!tabRect) continue` paths never skipped it and _tabAnchor() put the
line at (0, 0) ("M 0 0 C 350 0, 350 400, 700 400" in Chromium). The
sidebar filter already had the same defect; #580 brought it to the rail,
and the keystroke redraw from 5f373a93 now ran it on every keystroke that
hides the parent.
A new _paintedSessionTab() in app.js returns the parent's row only while
getClientRects() is non-empty, and every place a floating window
measures its parent tab goes through it:
- the shared `tab:<id>` rect cache, filled by the subagent connectors
and by both ultracode connector layers (all three fill it, so guarding
only the first would let the next one cache the zero rect itself);
- the ultracode window's spawn position, which now cascades;
- the subagent window's fly-from-tab spawn, the same defect, not in the
report, now positioned as a window without a tab;
- the ultracode genie on minimize, which now tears down at once.
Lineage lines need nothing: they measure on a cache miss and computeTree
already drops zero-size rects.
Tests: in the gate, with the page laid out by hand (jsdom has no
layout), a subagent window, an ultracode run window and an ultracode
agent window from a hidden row draw no line and draw it again from the
row once the search is cleared, an ultracode window spawns from a
painted row and cascades from a hidden one, and the genie is skipped for
a hidden row. In the browser suite, real Chromium shows the hidden row's
rect is all zero and the connector is gone, then back after Clear. All
four fail on the previous commit. CLAUDE.md and the invariants'
Connectors bullet record the rule.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the owner's review of #580 rather than the bot's report: "Drag
while searching: I'd turn it off. A drop is saved for every device
(session order or the tab layout), and where it lands relative to the
rows the search is hiding is something you only see after clearing it."
The PR head left drag on.
Both rail drags now refuse while the search narrows the list: the
grouped rail's pointer drag in _onTabLayoutPointerDown(), and the flat
manual rail's HTML5 drag in its dragstart listener. The flat rail is
refused in the listener, not by flipping `draggable`, because a
keystroke in the box does not re-render the rows, so a cleared search
drags again with the same rows. The sidebar filter box, the header strip
and the keyboard moves (Ctrl+Shift+{ }, the row menu) are unchanged.
Tests: a grouped-rail press during a search starts no drag and one after
clearing does; a flat-rail dragstart during a search is refused and one
after clearing goes through. CLAUDE.md and the Dashboard wiki row say
so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
CodemanTabSearch lower-cased the query and each row with
toLocaleLowerCase(), i.e. the browser's locale. Under a Turkish or Azeri
locale "API Review" lowers to "apı review", so a search for "api" missed
it, and the sidebar filter box, which used locale-independent
toLowerCase() before #580, changed matching with it. Both now use
toLowerCase(), like every other frontend search.
Test: a vm context of its own whose toLocaleLowerCase behaves like the
Turkish locale (the prototype is that context's alone) checks that "API"
still normalizes to "api" and that "API Review" matches. The test file's
overview also lists the pins the #580 fixes added.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Since the sidebar filter and the rail search share _applyTabListFilter(),
the sidebar's filter box also marks a .tab-cluster it emptied in the
by-case tab layout and rewrites its count. But the only rule hiding
.tab-cluster.tab-filtered-out was the rail-scoped one, so in the sidebar
the emptied box stayed painted with its label and a count of 0 while its
rows were hidden.
The sidebar rule now hides the marked case box as well. It stays scoped
to html[data-session-list="sidebar"], so a leaked class still cannot hide
anything on the header strip. An alerted row still counts toward its box
(the owner's call on #580), so a box holding a row that needs the user
never hides.
Tests: a unit test renders the sidebar in the by-case layout and checks
the emptied box is marked with a 0 count, that an alerted row keeps its
box on screen and counted, and that clearing restores the totals; a
stylesheet check pins the scoped selector; the browser test measures the
shipped styles.css in Chromium: the marked box is not painted in the
sidebar layout, an unmarked one is, and the header layout hides nothing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A keystroke in the rail search box that does not change the group
structure only toggles classes, so hidden rows collapsed and the rows
below them moved up while the lineage lines and the subagent/ultracode
connectors kept pointing at the old positions until the next render. The
sidebar filter box had the same gap for its connectors.
_applyTabListFilter() now notes whether anything it touched actually
appeared or disappeared (a row, a group or case box, the state headings
via tabs-filtering, the "No sessions match" line) and calls
updateConnectionLines() only then. That covers both boxes and the alert
re-render, costs nothing on the re-apply every render tail runs when
nothing changed (the incremental path's lineage gate keeps its meaning),
and coalesces with a render's own redraw.
Same flag, owner's optional nit from the review: the grouped tree's
posinset/roving-stop fix-up at the filter tail now runs only while a
search hides something or right after one changed what shows, since both
render paths already set them over an unfiltered tree.
Tests: unit tests for the rail keystroke path (flat and grouped rail, no
render taken), the empty note, the sidebar box, and no redraw when
nothing moved; the browser test checks in Chromium that a class-only
keystroke moves a row up and redraws exactly once.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The global key handler in setupEventListeners() sits on document in the
capture phase, so it ran before the box's inline onkeydown: the Escape that
cleared the search also ran closeAllPanels() (collapsing the Monitor and
Subagents panels), closeHelp() and closeSessionManager(). Calling
stopPropagation() from the inline handler came too late.
The global Escape branch now claims an Escape whose target is
#tabRailSearch while the box holds text, the same way it already claims
one for the group menu, the grouped-rail drag and the Tiles count menu,
and routes it to handleTabRailSearchKeydown(). An empty box still leaves
Escape to the global handler, and an Escape that cancels an IME
composition is left to the IME.
Tests: the unit test installs the real global listener and asserts that
Escape with text in the box closes nothing, that an empty box closes as
before, and that an Escape outside the box is not claimed. The browser
test installs setupEventListeners() for real in Chromium, so the
capture-before-inline ordering is the shipped one.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Conflict in docs/wiki/Settings-Reference.md resolved by keeping #565's Apply
wording and adding Copilot (and COPILOT_HOME, which the bot's review found
missing from this list).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
The new Animations section (rail item, heading, Entrance Theme and Tile
Animations rows with their options, the lab row) had no zh-CN entries.
Tab names were not in SKIP_SELECTOR (the list names .session-tab-name, which
the strip does not use), so a session called "Lab", "Soft" or "New session"
rendered translated. .tab-name is skipped now, like every other user-text
surface.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A session with a tab alert (red action or yellow idle, whatever tabAlerts
holds, the same set a collapsed group header surfaces) now stays visible
while the rail search or the sidebar filter is narrowing the list, even
when its name does not match. A prompt waiting on you should never be
hidden by a view filter.
The pure CodemanTabSearch.filter decides it: a row passed with keep: true
is never hidden. It counts toward its group, so the group stays on screen
and the header number is the rows left showing, but not toward
matchCount, so "No sessions match" still shows above a lone alerted row.
_applyTabListFilter() flags session rows from tabAlerts; web tabs carry
no alerts and are never kept.
No new wiring: updateTabAlertFromHooks() and _onSessionWorking() already
call renderSessionTabs(), and both render paths end in the shared filter.
- 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>
A "Search sessions" box at the top of the vertical tab rail narrows the
list to the tabs whose name matches (case-insensitive substring; a web
tab matches by its title). It searches every group, collapsed ones
included: while a search runs the projection draws every group open and
the header will not toggle, and the stored per-device collapse state is
left alone. Groups with no match hide, an empty result says "No sessions
match", and the flat rail (no groups) filters the same way. Escape or
the clear button empties it; leaving the vertical orientation resets it.
It is a view filter only: rows get the same tab-filtered-out class the
sidebar filter box uses, through one shared _applyTabListFilter() over
the pure CodemanTabSearch matcher in constants.js. Grouping, order,
Alt+N badges and drag are untouched, nothing is persisted or sent to
the server. The sidebar keeps matching name plus working directory.
In the grouped tree, hidden rows and the headers of emptied groups leave
the roving walk and posinset/setsize, and the tab stop moves onto a
visible item. zh-CN strings added.
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.
The hourly sweep called lstatSync and realpathSync on every live session's working directory, so a linked case on a mount that stopped answering blocked the event loop 30 s after boot and then every hour; the old sweep was fully async. uploadDirs() now probes the workspace with probePathKind() first and skips an unknown path without touching it, then uses fs.promises for the lstat and realpath. The sweep keeps the probe's stall cap; cleanupSession(), acting on one path at the user's request, passes pastCap and removes the directories with fs.rm.
Refusing an upload dir that sits strictly inside the data dir protected nothing (it only ever holds uploads, and a link is already excluded by lstat) while the route kept writing there, so uploads under a workspace like the ~/.codeman/app that install.sh clones were never swept and never removed. Only the two cases that matter stay refused: the upload dir being the data dir, or containing it.
Also: the ignore file's take-back after a failed write no longer replaces the error that caused it with its own, and the shared-dir test's header no longer names the fixed port it stopped using.
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>
A pasted image landed in <workspace>/.claude-images/, a name that belongs to another tool, in a folder nothing ignored, so it showed up in git status of every case that ever received a paste. Uploads now go to a flat <workspace>/.codeman-uploads/ that carries a .gitignore of `*` (written once, never over a file already there): in the workspace because that is the only path identical for a local agent and a container, flat because a nested .codeman/ is the data dir itself when the workspace is the home directory.
The directory names live in paste-image-gc.ts alone. The old folder receives nothing but stays readable for one release: the hourly sweep and the delete cleanup go through uploadDirs(), the image watcher's ignore filter reads the names. uploadDirs() lists only real directories, none that is, contains or sits inside the data dir, and nothing for a remote session, since both consumers delete. The route refuses a remote (SSH) session before touching disk: the file would land on this host under the remote path, where the agent cannot read it.
test/paste-image-dir-shared.test.ts binds port 0 and leaves the port guard's legacy list.
Decided in #553.
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>
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>
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>
- 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>
Copilot CLI keeps its user MCP list in ~/.copilot/mcp-config.json (COPILOT_HOME moves it) but is
not a Codeman run mode, so it has no registry entry. Add the copilot-json dialect (mcpServers,
tools ["*"], type local/http/sse, checked against `copilot mcp add` 1.0.94) and declare Copilot as
a sync-only target in src/mcp-sync-targets.ts, listed after the registry CLIs. A server switched
off with `copilot mcp disable` is recorded in settings.json (disabledMcpServers), not on the
entry: sync reads that list so it is not copied, and reports the target unreadable if the file
is not valid JSON instead of guessing.
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>
Redraw (Ctrl+Shift+R and the header button) shows five literal toasts and a
size report on the main pane, a tile or the split's Pane B. None had a zh-CN
entry, including the two the final checkup's tile Redraw fix added, and
"Failed to restore terminal size" fell to the generic "Failed to" pattern,
which left English behind. They now translate, and the size report keeps its
numbers through a pattern rule. test/redraw-toast-i18n.test.ts reads the
toasts from restoreTerminalSize() itself, so a reworded one without an entry
fails.
Comments and docs that still described an older default:
- styles.css: the Tiles header button is no longer opt-in; it is on by default
on desktop and off on phones and coarse-pointer tablets.
- terminal-ui.js: the desktop branch of getDefaultSettings is no longer always
{}; what the comment needs is that it sets no copyStripMargin.
- docs/tile-grid-plan.md: the Tiles default bullet names the tablet default.
- docs/cli-registry.md: codex's footer is read in a two-row window since
codex 0.162's hint row, not from its last row.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the read-only final review of the release, adversarially verified, then reviewed again:
- tiles: a file dropped on the grid uploads to that tile's session instead of navigating away; app-driven tile changes no longer move the keyboard into another session; popping out the last tile no longer leaves a frozen view; "Open group as tiles" no longer merges an open split; the Tiles button defaults off on touch tablets (opt-in)
- voice: dictation with the grid open reaches the focused tile
- css: By case stays one scrolling strip on 600-767px tablets, the needs-you pulse animates opacity only, phone welcome chips are 40px
- i18n: zh-CN for the case picker rows, the git status settings, new toasts, tile and spreadsheet texts
- cli registry: codex launch defaults are registry data, not an id branch; the codex footer reads an ultra effort
- build and docs: a dependency preflight runs before the build deletes dist; docs no longer name 1.36.0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- i18n: the spreadsheet notice's feature words (charts, drawings, macros,
pivot tables, external links) were bare, case-insensitive zh-CN keys, so
the page translator also renamed a charts/ or macros/ folder in the Files
panel and a case of that name in the case picker. They are now scoped
'Spreadsheet feature: <word>' keys; warningText() falls back to the plain
word when the scoped key has no translation (English, no i18n). Removing
the bare keys closes this branch's regression. The older 'models' key has
the same class of problem; it is left alone here, since adding
.case-combobox-option-label to USER_TEXT_SELECTOR would also untranslate
the picker's two action rows and still miss the title attribute.
- Spreadsheet notice bar: marked data-i18n-skip. It is written already
translated, item by item, and ends with a number format's code, which the
observer's t() over the whole line rewrote ({name}, "Codeman").
- Connection tile (Header Stats Style Tiles): applyLocalization() now repaints
the indicator, so a switch back to English no longer leaves the Chinese
value word in its data-i18n-skip span until the next keystroke or ACK.
- Tiles default: loadAppSettingsFromStorage() no longer caches the
posture-dependent showTileGridButton default, so the init merge never
persists it; a 2-in-1 first opened as a tablet gets the button once docked.
Every reader still resolves the absent key through a fresh
getDefaultSettings(), so phones stay OFF and desktops ON.
- Tile grid over a split: closeSplitPane() skips Pane A's closing resize only
when Pane A becomes a tile. With mergeSplit false (Open group as tiles, a
stored grid) a Pane A left out of the set gets its full width back before
the main terminal parks, instead of keeping the split's half width.
- By-case tab strip on tablets and phones: with the boxes dissolved, the
-<case> part of a generated name shows again, so w1-alpha and w1-beta no
longer both read "w1".
Each new assertion fails against the previous source.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Codex footer model detection (c28): the modelDetect.screenLine effort
alternation is now built from CODEX_REASONING_EFFORTS plus 'default', so
'ultra' (offered by the codexReasoningEffort App Setting and codex's own
/model picker) is read and the launch enum and the footer reader cannot
drift again. Still one capture group, 125 characters, no new quantifier.
New session-display-model case loops every effort level, ultra included.
- No CLI-id branching for launch defaults (c27): the two mode === 'codex'
branches the synced codex model/effort defaults added to the create and
quick-start routes are replaced by a registry capability,
capabilities.launchDefaults (launch param -> settings key, values from a
closed enum), declared on the codex entry only. The resolver moved from
web/codex-launch-defaults.ts to web/launch-defaults.ts as
applyLaunchDefaults(mode, configs, customEndpoint), filling the entry's
legacyConfigField object through legacyConfigAliases, still re-validating
with SettingsUpdateSchema and never overwriting a caller's value. The
route exclusions are unchanged (create: not remote; quick-start: not
remote, not Docker, not a custom model endpoint), and quick-start still
derives the session model from a bag without ompConfig, as before.
schema.ts refuses an undeclared param, an unknown settings key, an empty
map, and launchDefaults on an entry with no legacyConfigField.
- The no-id-branching guard now carries an exact occurrence count per
allowlisted key, so a new copy of an already approved expression fails
instead of riding the old approval, with a synthetic anti-vacuity case.
- SettingsUpdateSchema JSDoc (c21/c29): 'classic' is the tabArrangement
default and 'compact' the headerStatsStyle default, matching the
resolvers and the pre-paint script; state/case/ledger are marked opt-in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>