Opening another session in the case you are already working in meant re-picking the case and the mode in the toolbar. An opt-in + now sits with each tab's gear and close icons (App Settings, Tabs, New Session Button on Tabs: per device, off by default everywhere). It opens a menu of the CLIs that tab's case can start, each shown with its logo and name as in the Run menus, and a pick starts one session in that tab's case, or the managed worktree it runs in, and in its named group, without changing the Run button's mode or case.
The CLIs offered follow the Run menu's own rule for the case, so a Docker case lists what its container has rather than what the host has. A tab outside any named group sends no group, so the case's default session group places the new session the way it places a toolbar Run. Wherever a tab has a row menu (the vertical rail, the sidebar) that menu gains "New session here…", the keyboard route to the same picker, since Enter on the + itself does nothing. On a phone the + joins gear and close on the active tab only, and the label reserve grows to 68px while the setting is on, so the tab's centre stays off the icons.
The shared tab layout menu no longer closes when focus leaves an item for nowhere: Safari never focuses a clicked button, so pressing an item blurred the focused one with no related target and the click was lost. This is the tile count menu's rule, and it fixes the named-group menu as well.
Every keyboard open and close resized the PTY, and Claude Code's inline renderer answers each height change by clearing the screen and reprinting only the transcript tail that fits, so rows above that tail were lost from tmux for good. Measured on Claude Code 2.1.306 with tmux 3.2a: five keyboard cycles (62x38 to 62x15 and back) left 75 of 100 numbered lines in the pane's history; with the rows held the PTY stays 62x38, no SIGWINCH reaches the pane and all 100 remain (#672).
While the keyboard is up the rows captured at the show transition are held through getTerminalDimensions, columns still follow the box, and the screen, now taller than its box, is translated up by --kb-clip so the last content row sits at the bottom of the visible area. This is the one documented exception to the columns-only geometry rule: there the extra rows came from another device and nothing kept the composer in view, here they are this device's own rows held on purpose and the clip keeps the composer visible. The local-echo overlay's setRowLimit narrows the renderer's existing totalRows bound, so one place decides where a long draft stops growing downward.
Only inline renderers hold, through a new registry capability keyboardHold: 'inline' (Claude alone) published to the page as window.__codemanKeyboardHoldModes. Full-height layouts such as codex, Claude's fullscreen mode (mouse tracking on, unmeasured), and remote or docker panes resize as before. The hold is released on keyboard close, a shape change, a tab switch, a session update, a reconnect, or the setting turned off. Per-device setting Keep Size Under Keyboard, default on.
The rename lock is checked once, in the Session constructor, but a session
reaches the server's map (where the rename route builds its list of open
sessions) only later, after the create awaits the owner's layout lock and a
fresh settings read (and, for POST /api/sessions, inferLocalCaseName). A
rename in that gap neither listed nor stopped the session, moved the folder,
and the create then registered a session whose pane starts with
`cd "<old path>"`, skipping the 409 requiresStop / stopSessions guarantee.
- case-rename.ts keeps a record of sessions constructed but not yet
registered (`noteCaseSessionStarting` / `caseSessionSettled` /
`isCaseSessionStarting`), with a 30 s TTL for one that never registers
(a failed create, a one-shot prompt), swept on every note and check.
- The Session constructor records itself (local sessions only, as the
route ignores remote ones); registerSessionWithLayout()'s register
callback, the boot-restore registration and stop() settle it.
- The rename route answers 409 CONFLICT with renameCode BUSY ("A session is
starting in this case") for a folder move while one is recorded at or
under the folder. The check runs in the same tick that takes the lock, so
every interleaving is either refused here or by the constructor. An
alias-only rename is not refused.
A create whose path was validated before the move but whose Session is
constructed after it is still the ordinary check-then-use race of any
create path; CLAUDE.md now says so instead of "nothing starts mid-move".
Tests: unit (record, settle, stop, TTL), route (BUSY, then requiresStop once
registered; alias-only unaffected) and an end-to-end WebServer test that
holds the create between constructor and registration (it fails without
the route check: the folder moves under the session).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#689's density steps hide every `.header-system-stats .stat-label` once the
toolbar is narrowed, and #692's readings (GPU, VRAM, GPU MHz, CPU MHz) have
nothing else naming them: at tight, dense and tiny the header showed "40%"
beside CPU's "12%", "3.2G" beside MEM's "7.9G", and two bare "1800 MHz" /
"4200 MHz" with no way to tell the GPU clock from the CPU one. At Compact +
tiny, where values are hidden too, the two clocks (no ring) were left as
0-px items that still took an 8 px flex gap each. Neither PR shows it alone.
- The four host items carry `stat-item--host`, excluded from the density
label hide, so they keep their labels at every step.
- `stat-item--ringless` (static on the two clocks, toggled by
`_updateHostSensors` on VRAM while its total, and so its ring, is unknown)
is hidden at Compact + tiny instead of staying an empty item.
Tests: the browser test drives every density in all three styles against
the real markup and styles.css (it fails on the previous files), and the
unit test pins the VRAM ringless toggle.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in src/web/routes/session-routes.ts (the import block) resolved
by keeping both sides: #680's claudeProjectDirName import stays, and the
fastify import takes #690's form without FastifyReply, since #690 moved
abortOnClientHangUp (its only user here) into route-helpers.ts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in config/test-suites.ts resolved by keeping both sides: #689's
header-divider and #692's header-host-sensors browser tests are both
listed in BROWSER_TEST_GLOBS.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude history merges, never overwriting, into a project directory a deleted case of the new name left behind, instead of staying orphaned under the old key.
The free-target pre-check also refuses a dangling symlink (lstat), so stopSessions no longer ends sessions before a 409 EXISTS.
The rename dialog says stopped sessions are closed (their tabs go away) and, after a failed rename, how many were already closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Boot prune keeps the inboxes of sessions on the reboot-restore offer (their records are already pruned and a rebuild reuses the id), with a case in test/agent-inbox-server-wiring.test.ts.
docs/wiki: post, inbox and ack in the codeman agent command block of Driving-Codeman-From-An-Agent, and "Keep agent inbox across restarts" in Settings-Reference.
codeman agent inbox prints message text through terminalSafeText (ANSI stripped, other C0, DEL and C1 controls shown as \xHH); --json stays as stored.
Shutdown flushes the inbox snapshot again after app.close(), for a post that lands after the first flush.
POST /api/cleanup-state drops the inboxes of the records it removes.
InboxPostSchema derives its fence from MAX_MESSAGE_BYTES instead of the literal.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
host-sensors: never await the PowerShell RAM probe on a read; keep the resolved total and show the memory in use alone until it answers (test with a held PowerShell)
test: rename the Windows sampler test that said the GPU counters are added "for good"
test: make host-sensors.test.ts's @fileoverview describe what that file covers
host-sensors: make the nvidiaSkipUntil JSDoc describe the back-off deadline
docs: Settings Reference says the host readings live inside System Stats and nothing is fetched while it is off
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Multi-user: add a (username, IP) failure bucket checked before verifyPassword, capped at AUTH_FAILURE_MAX, with the same decay as the other buckets; the per-IP pre-gate stays at AUTH_IP_FAILURE_MAX and the per-username bucket stays after the verify
A correct password now also clears that account's (username, IP) bucket, beside the IP and per-username buckets
The new bucket is disposed on server stop like the others; the comments above the gate and the constants' JSDoc describe the three buckets
Tests: 10 wrong then the correct password from one IP is a 429, the same account logs in from another IP, another account behind that IP is unaffected, and a correct password clears the pair bucket
test/multiuser-auth.test.ts: the spray test loops to AUTH_IP_FAILURE_MAX instead of a literal 50
Reworded the rate-limit lines in auth-config.ts, security-architecture.md, README.md, README.zh-CN.md, wiki/Security.md, endpoints.md (plus the plugin mirror), CLAUDE.md and architecture-invariants.md
docs/wiki/Remote-Access.md: add the multi-user note to the tunnel page
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Clamp the stored toolbar width to MIN_WIDTH in apply() and step the arrow keys from the width on screen, so a drag to the window edge or a held ArrowRight no longer stores a negative width or leaves an ArrowLeft dead zone (browser test added)
Drop Escape as a reset key on the handle: the global capture-phase Escape handler already closes every panel on it (static pin added)
Say in the changeset, the wiki and the densityFor comment that the percentages go at the narrowest step in the Compact style only, and drop Escape from the changeset
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
palette agent switch: aria-hidden on .command-palette-agent, arrows lose role="button" and aria-label (no nested buttons in the row)
i18n: zh-CN entry for the "Agent for the new session (Left/Right to change)" tooltip
docs: Keyboard-Shortcuts Ctrl+K row says Left/Right change the agent on the New session row
test: pin the aria-hidden switch and the zh-CN tooltip entry in command-palette-ui.test.ts
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Restore settings.json (caseOrder, lastUsedCase) when linked-cases.json cannot be written, with the rollback test extended
List Claude Code's per-project ~/.claude.json entry as not migrated in Core-Concepts.md and CLAUDE.md
Say in the 500 that the folder is now at the new path when moving it back fails
Describe the rename dialog's folder checkbox in Core-Concepts.md, carry stoppedSessions on errors, count 5 case SSE events
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- remove() drops the session's slot once the file is gone: a session that comes
back with the same id (reboot restore) starts afresh instead of answering
`removed` until the next restart. A run in flight stays on its own slot's store,
and the routes start a sync in the same tick as their session check, so a
deletion right after it still stops that sync. A store that is not loaded is
deleted without being parsed (deleteStore).
- A new conversation (/clear, resume) first reads the previous transcript, next
to the new one, to its end with its subagents, so edits it got after the last
read are not lost with its cursors. "Load older" reads the stored conversation
while the switch is pending.
- At most MAX_LOADED_STORES (3) stores stay in memory; the least recently used one
without a run is written if it changed (settle) and loads again on demand.
- Tmp files are named per process start, so a reused pid (a container's pid 1)
no longer keeps a stale one through the startup sweep.
- load() keeps only a plain transcript id (the previous transcript's path is built
from it) and no cursors without one; a pending Bash call with an id, cwd or
agent id over 4,096 characters is not kept at parse time either.
- nextRange() is gone: production reads forward through the reader. The store
tests mirror the reader's range choice, and a new end-to-end test runs the
real reader, sync and store (/clear cases, 40 seeded random transcripts).
- The wiring guard scans all of src for holders of the sync.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Forward: after a line longer than the budget, the next line is read from
its start instead of being skipped with it when it crosses the chunk end.
- Backward: the line before a skipped long line is read whole; a floor that
turned out to be a line start no longer stalls.
- A chunk the shared forward budget cut short is not applied (it read as one
over-long line) but read in a later pass, so a first open of a long main
transcript reaches the subagents in the same sync.
- Load older reads a full budget block of up to three transcripts per click,
taking turns, instead of splitting one budget (a normal Write longer than
the share was skipped for good).
- A subagent transcript that vanished loses its cursor, so Load older does not
stay on forever.
- POST /api/cleanup-state forgets the history of every record it drops; a
startup sweep deletes edit-history files no session or record owns and
stale tmp files of other processes.
- load() rebuilds entries from known fields (string diff lines, per-edit cap
re-applied, ids counted against the byte cap), bounds pending Bash fields
and agent ids, and does not parse a file over 32 MiB.
- No re-read after Codeman's own keep-alive turn.
- Tests: regression tests for each of the above (they fail on the previous
commit); the remove-during-write test now meets the write under way; the
sync and route tests remove their stores before the temp dir, so no late
save recreates it in /tmp.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The history is deleted when the session's persisted record goes (forgetEditHistory):
kill via cleanupSession (a pinned record is only demoted and keeps it), the
persisted-only DELETE, and stale cleanup at startup. After remove() neither a sync in
flight nor a later request writes the file again; writes are serialized.
- load() re-bounds a damaged file: cursors, pending Bash calls (MAX_PENDING_BASH) and
subagent labels (MAX_AGENTS, oldest unused dropped first).
- Load older reads one block of every transcript, budget split between them, and only
the files it asked for; a block short of budget reads its end. A line longer than the
budget is skipped going back as well (floorMidLine).
- Concurrent first callers share one store; a forward request folded with an older one
still reads forward.
- Images outside the workspace get no thumbnail (one attachment id per render); a click
opens the shared preview, which registers once with notify: false.
- Docs: per-edit diff cap, deletion rule, load-time bounds, thumbnails, follow triggers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Part 1 of #643 (local sessions only; remote follows as its own PR). An
opt-in, per-device header button opens a panel with the active session's
edits, newest first: every Edit/Write/MultiEdit/NotebookEdit of the main
agent and its subagents with its diff, plus images a Bash call rendered or
the agent sent. Remote, Docker and non-claude sessions answer `unsupported`.
- Reads only on request: `?sync=1` when the panel opens; while it is open on
that session it re-reads after an edit-capable `transcript:tool_end` or a
`transcript:complete` from the existing transcript watcher. No hook, timer,
second watcher or new SSE event; a wiring guard pins it.
- Main transcript: the path the transcript watcher follows (hook-reported),
else `<id>.jsonl` under the session's own CLAUDE_CONFIG_DIR override or
the server's config dir. A config dir set outside Codeman is found with
the next hook; until then the panel says so.
- Store: dataPath('edit-history')/<id>.json, 0600 via tmp+rename, capped at
500 entries and 4 MiB of diff text per session, deleted with the session,
blocked in isSensitivePath, never fed to /api/search. Every route goes
through findSessionOrFail with req.
- Images only through existing file surfaces: file-raw inside the
workspace, the attachment route with notify:false outside it.
- textContent rendering, bounded byte-offset reads, marker-class header
button, hidden on phones (locked in the mobile policy test).
Docs: Working-With-Files, Settings-Reference, architecture invariants, API
reference, CLAUDE.md key pattern.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Single-user mode has no per-username bucket, so its per-IP bucket is the
only limit on guesses against the one password, and behind a tunnel every
client shares 127.0.0.1. Put it back on AUTH_FAILURE_MAX (10).
AUTH_IP_FAILURE_MAX (50) now gates only the multi-user per-IP pre-gate,
which runs before the scrypt verify and so refuses even a correct password;
the per-username bucket still caps each account at 10.
Move the headroom test out of the single-user suite and add a multi-user
test that sprays wrong passwords across usernames (9 each) and expects 401
for the first 50, then 429 for the 51st and for a correct password.
Update the docs that still described 10 per IP for every mode, and resync
the plugin mirror of the codeman skill.
Only an admin's rename touches the server-wide settings.json, which is now
written before linked-cases.json with the registry restored on failure. The
Resume list follows a renamed folder, the owner's default session group is
carried over, remote sessions are never listed or stopped, errorCode maps
EXISTS and BUSY, and the unmigrated items are documented.
nextSession/prevSession walk the canonical sessionOrder, but the case and state tab layouts, a sorted rail and named groups reorder the strip, so with any of them a swipe jumped to tabs that were not next to the current one on screen. The swipe now asks for the drawn order, read with the same walk the strip's arrow keys already use (factored out as _visibleTabRows); Alt+[ / Alt+], Ctrl+Tab and Alt+1..9 keep the canonical order. An active session the list does not draw steps in at the first or last row, and an empty drawn list goes nowhere instead of falling back to a hidden session.
Second merge of #674: the author pushed the thumb-arc handback from the review themselves (e8fc08be), byte-identical in code to the merge-time fix already here, so this brings their round in and keeps the browser test and docs added at merge.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
# docs/wiki/Mobile-Guide.md
# src/web/public/terminal-ui.js
# test/session-swipe-gesture.test.ts
Conflict in CLAUDE.md's State Files line resolved by keeping #661's new
config-backup entries and this PR's "case default groups" in state.json.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts in CLAUDE.md, README.md, docs/api-reference.md and
src/web/server.ts were additions beside #662's Copilot quota read and
#666's keep-warm sections; both sides kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A scroll whose first move past the tap slop leaned sideways (a thumb arc) locked as 'swipe' for the whole contact, and SwipeHandler drops a swipe at 50 px of vertical drift, so the gesture did nothing. Past that limit the drag is now a 'y' scroll. Also brings the mobile-handlers fileoverview to the current 500 ms / 50 px limits and notes in the Mobile Guide that a selection pauses the swipe and a wide terminal pans instead.
Squash of PR #658. Discovery records each llama.cpp model's parallel slot
count (from --parallel/-np, or /props total_slots when the launch command
states none), and a Claude launch warns when the context split across
those slots leaves less than 40K tokens per request. An explicit
--parallel beside a /props context records a slot count of 1, so a later
round overwrites an earlier count, and a non-2xx /props is remembered per
launch command while a network error is retried.
Merge-time fix by the maintainer: the architecture-invariants sentence on
the explicit --parallel case now says it records a slot count of 1.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #676 (review finding 3 and nit 9). The State Files entry
for state.json names the new caseDefaultGroups key, and the Owner tab layouts
paragraph says where a case's default session group sits in the placement
order of sessionCreated() and that only TabLayoutService reads or writes it.
The wiki endpoint map counts the two new case routes (37 to 39).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Squash of PR #662. The header plan-usage chip gains a Copilot row: the
calendar-month premium-request quota, with the request counts and the
reset in the tooltip, read every 10 minutes from GitHub's
copilot_internal/user endpoint with the Copilot CLI's own sign-in token.
It rides showPlanUsageLimits and is on by default while the CLI is
installed and signed in; any failure leaves the chip without the row.
Merge-time fixes by the maintainer: the gates (setting, then token, then
the CLI probe) live in readCopilotPlanUsageIfEnabled() so a host with no
Copilot sign-in never runs the login-shell probe each tick, with a test
that Plan Usage Limits off means no request; the X-GitHub-Api-Version
header the Copilot CLI itself does not send is dropped; and CLAUDE.md,
architecture-invariants, README and copilot-integration describe the read.
The hooks-config import in server.ts was merged with #666's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The remote-history PATH bootstrap that rode along in this PR was left out
at merge (it belongs in its own PR that routes every remote tmux call
through one wrapper), so 242bbfcd's title overstates what lands here.
Conflicts in CLAUDE.md and docs/architecture-invariants.md resolved by
keeping this PR's rewrite of the wheel/touch paragraphs and re-adding
#664's touch-forwarding sentence to both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Squash of PR #669. Browser terminals could be moved onto another session
by tmux's prefix keys: Ctrl+B then ), (, s or w switched the tab's tmux
client to a different session while the tab kept the original's name.
Codeman's tmux socket now keeps only Ctrl+B Ctrl+B (send-prefix) in its
prefix table, and codeman tui and scripts/tmux-manager.sh get F1 as their
way out instead of Ctrl+B d.
Merge-time fixes by the maintainer: on an empty prefix table the
unbind-key that fails no longer skips binding send-prefix; a key tmux lists
escaped (prefix C-\ shows as C-\\) compares equal, so the reset does not rerun on every create;
and reconcileSessions() reduces the table at boot when the socket has live
sessions, so sessions that survive an upgrade are covered too.
Co-authored-by: christianhaberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #665.
- A markdown link converted to a preview link became `a.rv-path`, so its
label ("Our incident record") took the path style: monospace, 0.92em
and `word-break: break-all`, which split prose mid-word on a phone. The
anchor now also carries `rv-path-link`, and
`.rv-text a.rv-path.rv-path-link` resets font and word-break, as the
File Viewer does for its labelled document links.
- The first new test adds `/tmp/a.json?raw=true` (pins the `?query`
strip) and `/tmp/` (pins the trailing-slash folder check, since
findFilePathLinks finds no folder in a bare `/tmp/`), and checks the
new class.
- The `_linkifyFilePaths` JSDoc and docs/architecture-invariants.md say
the function also rewrites anchors whose href is a scheme-less,
non-`//` path, on purpose without FILE_PATH_LINK_PATTERN's root and
extension filter.
- Mobile Guide: "a web link" in the response viewer opens a tab.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #661. The pre-restore copy was forced, so a restore made
while the live files equalled the newest backup spent a retention slot on a
duplicate. It now goes through the normal change check: when nothing changed
since the newest backup, no copy is made and safetySnapshot names that
backup. Two tests that relied on the forced copy now change a file before the
restore; a new one pins the reuse.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #674: the mobile-handlers.js @fileoverview still
gave the old 300 ms and 100 px limits (now 500 ms and 50 px), and the
Mobile Guide now says a visible text selection pauses the swipe and a
terminal wider than the screen pans instead of switching.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #659.
- Alt+Left/Right (File Viewer back/forward) is a display-only entry in
DEFAULT_SHORTCUTS, so the Ctrl+? overlay lists it, and a row in the
help modal's Panels group, pinned in help-modal-shortcuts.test.ts.
zh-CN entry for the new label; the "Restore size" entry is dropped,
since nothing shows that string any more.
- The wiki row moves from "Sessions and tabs" to "Everything else".
- The grip comment in styles.css says only phones hide the grip;
tablets show and use it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #661. A restore that could not write one file (a full
disk, a directory where the file should be) used to stop there and answer a
bare 500, naming neither the files it had already put back nor the
pre-restore copy, and the Settings form was not re-read, so the next Save
wrote the old settings back over a restored settings.json.
restoreConfigBackup now writes every verified file it can, then throws
ConfigBackupPartialRestoreError with restored, failed (name and error),
untouched and safetySnapshot. The route runs the same reload hooks and audit
for the files written as a full restore does, and answers 500 with the error
envelope plus those fields (and restartRequired) in data, as the tab-layout
409 carries its layout. Settings refills the form when settings.json is among
the restored files, also on that path. Tests: a unit case where a later file
is still restored, the clis.json-as-a-directory route case, and two vm cases
for the Settings handler.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #666. `recordHookEvent()` ran for every `stop`, the run
summary files a `stop` as a warning, and the Away digest lists warnings under
Needs Attention, so each keep-alive nudge added a row there and to the
session's Summary timeline. The hook route now skips the run summary while
`keepAliveReason` is set (the keep-alive turn's `stop` and `idle_prompt`).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>