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 #674: the first travel past the tap slop decided the
axis for the whole contact, so a scroll that opened on a thumb arc
(+9 px across, +5 px down, then straight down) locked as 'swipe'. Every
later touchmove returned before the scroll code and SwipeHandler gave
the swipe up at its 50 px drift limit, so the gesture did nothing. Past
SwipeHandler.maxVerticalDrift the drag is now handed to 'y' (and the
published axis with it), so it scrolls; a clean sideways swipe, a
vertical scroll and a long-press drag are unchanged.
The source pin in test/session-swipe-gesture.test.ts moves with the
code and also pins the drift value, and a new browser test drives the
real touch listeners: the thumb-arc drag ends as a scroll (it fails
without the fallback, axis stays 'swipe') and a clean sideways swipe
stays a swipe with nothing scrolled. The architecture note and
src/web/CLAUDE.md now say a swipe drag is consumed only up to that
limit.
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 #659.
The Expand button swapped its title and aria-label to "Restore size"
while on, so a screen reader announced "Restore size, pressed". The
label now stays "Expand" and aria-pressed alone carries the state, the
way the MD, Lines and Wrap toggles work.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #664 (optional item): the drag-and-momentum test
for xterm's own SGR mouse tracking sits in its own describe, nested so
it keeps the file's page and touch dispatcher, and the file overview
mentions it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #664 (optional item): _logScrollRouting now prints
mouseEncoding (xterm's coreMouseService.activeEncoding, 'unknown' when
the private cannot be read) and counts it in the per-session signature,
since an app switching encodings changes where a touch drag goes. Adds
a unit test for that line and one asserting SGR_PIXELS fails closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #664: _shouldForwardTouchToApp reads
terminal._core.coreMouseService.activeEncoding, an xterm private. The
bundle-string loop in test/xterm-private-api.test.ts now checks
coreMouseService and activeEncoding too, and both failure messages name
_shouldForwardTouchToApp, so an xterm upgrade that renames either field
cannot silently stop touch forwarding to native SGR tracking apps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #667.
tmux reports the directory of a pane whose directory was deleted while it
ran as `/path (deleted)` (tmux 3.4 on Linux). Today the rediscovered
session falls back to the server cwd only because isValidPath() rejects
the parentheses; this row in the wiring test keeps a later relaxation of
the path pattern from quietly adopting a dead directory.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #645: the describe title mentioned "remote" and the
fork's #13, but the test drives the local primary pane.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merge-time fix for #645: the PATH bootstrap for remote history reads
(src/remote-pane-history.ts, src/remote-hosts.ts and its test) is a
separate fix from the scroll work. On its own it does not reach a host
whose tmux lives only under /opt/homebrew/bin, because the link
precheck, the launch, the alive probe and discovery still run a bare
tmux through the same non-interactive ssh shell. It belongs in a
follow-up PR that routes every remote tmux call through one wrapper.
These files are restored to the PR's merge base; every scroll change
stays.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The terminal's touch handler and the session swipe judged every gesture separately, so a sideways flick also ended as a tap on the session being left (a link, a CLI click, a keyboard toggle) or as a vertical scroll. The terminal now locks the dominant axis past the tap slop, hands a sideways drag it cannot pan to the swipe and publishes the axis, and the swipe stands aside for scrolls, pans, a lift the terminal consumed (its selection gesture), touch selections, a second finger and surfaces with controls of their own. A touch that starts outside the terminal resets the published axis, so swiping from the phone home screen into a session keeps working after a terminal scroll. A deliberate swipe gets 500ms instead of 300ms, and vertical drift cancels it mid-gesture instead of only at the lift.
An accessory key that acts on the terminal (arrows, Tab, Esc, Ctrl, scroll, clear input) took focus on press, so every tap blurred the terminal and the click handler had to focus it again: a blur/focus pair per tap that started a keyboard dismissal and shifted the toolbar each time. Such a key now cancels the mousedown default instead, the same pattern as the Attach input, so the terminal never loses focus on the press; the click-time refocus stays as the fallback. A first tap on clear/compact only arms the confirm and is treated the same way.
A prompt made only of blank lines joined to nothing, and sendwait then sent
a bare Enter onto whatever sat on the worker's composer. It now refuses
with "sendwait: empty prompt" (the test fails without the check).
reference/verbs.md still said sendwait takes one line and a multi-line
prompt can sit unsent; it now says sendwait joins the lines with a space,
which loses their layout, so long multi-line content belongs in a file.
Plugin mirror regenerated.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
`" \nfix it\r"` still pasted a blank first line, because the leading strip
only matched `\n`. /^\s*\n/ drops everything up to the last leading line
break, mirroring the trimEnd() at the other end, and the first real line
keeps its indentation. The test row fails with the old pattern.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
sendwait posted its prompt as is. With the mux now keeping line breaks, a
multi-line prompt goes in as a paste, and right after spawn_worker returns it
can land inside Claude Code's Enter-ignore window. Neither the Enter loop
here nor the server's submit check recognises a pasted block that is still on
the composer, so `Review this:\n- item 1` sat unsent while the re-wait ran
out. sendwait now trims each line, drops blank ones and joins the rest with a
space before building the body, the same result as toSingleLine, like every
other waiting sender.
The preamble stamp moves to 1.43.1 so cached preambles pick the change up;
the plugin mirror is regenerated with scripts/sync-plugin.mjs. The new test
runs the real preamble.sh with CURL stubbed and fails without the join.
docs/api-reference.md gets the submit-check sentence the README and wiki
already carry.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
An owner's tab layout holds at most MAX_TAB_REFS (512) refs, counting saved web
tabs (up to MAX_WEBVIEWS, 50 by default) as well as sessions. With a ceiling of
500, session creation could still fail with HTTP 500 on the layout limit before
the capacity message. 512 - 50 = 462; a test pins the ceiling plus MAX_WEBVIEWS
inside MAX_TAB_REFS so the two limits cannot drift apart again.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The proxy hands undici the request body as a stream and dropped the browser's
content-length, assuming undici recomputes it. For a stream it cannot: undici
frames it as Transfer-Encoding: chunked. A dashboard that reads only
Content-Length (Python's http.server, many small Go/C servers) then saw an
empty body on every POST through a web tab. Found on a WoL dashboard whose
Shut down posted {"confirm":true} and was refused for a missing flag on every
click through Codeman, while curl straight at it worked.
buildUpstreamRequestHeaders now puts the browser's content-length back when a
body goes upstream (relayBodyLength), it is a plain number, and no
transfer-encoding contradicts it. GET/HEAD never carry it. undici 6.28 frames
the stream with that length and fails cleanly on a mismatch. Side effect:
a DELETE/OPTIONS with content-length 0 now goes upstream with neither header
nor body instead of an empty chunked body.
test/webview-proxy-body-framing.test.ts pins it end to end against a
Content-Length-only upstream; it fails on the old code with
"expected 'chunked' to be null".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The input ended on a whitespace run, so the old regex returned a different
string and the test failed on the result before it reached the time bound.
Keep the run inside a line ('a<run>b\nc'): the old regex and the split now
return the same 'a<run>b c', and only the elapsed time tells them apart
(about 1.8 s against well under a millisecond).
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
"\nfix it\r" pasted an empty first line. Leading line breaks are now
dropped like trailing ones. Document what useMux does with a line feed,
and that a pane without bracketed paste receives each pasted line break
as CR, so a multi-line write without \r runs every line but the last
there, where the old line-stripping typed the joined text and ran nothing.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
/\s*[\r\n]+\s*/g backtracks over every start position of a long
whitespace run, so 50,000 spaces took over a second. Split on line breaks,
trim each line, drop blank ones and join with a space: linear, and a
leading or trailing blank line no longer leaves a space behind. A prompt
without a line break is still returned unchanged. session-auto-ops now
imports toSingleLine from the utils barrel like respawn-controller.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Every pane starts in TMUX_LAUNCH_CWD (/tmp) and reaches its workspace with a
cd inside the pane command, but a remote or docker pane never does that cd,
so tmux reports /tmp for it. A rediscovered record carries no remote/docker
metadata and looks local, so adopting that directory would root the session
at /tmp: hooks written into /tmp/.claude, the Files panel at /tmp, and a
respawn running claude in a directory any local account can write to.
reconcileSessions() now skips a pane whose directory is the launch directory,
in both spellings a pane can report it: the literal one and its realpath
(/private/tmp on macOS, where a process cwd comes back resolved). The set is
computed once at module load. Such a session falls back to process.cwd(),
as before this series.
Also stop trimming the list-panes output: the directory is the last field,
so a trim cut a trailing space off the last row's path; parsePaneRows()
already skips empty lines. And the comments that list what a discovered
record knows for certain now include the directory, when tmux reported a
usable one.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Review follow-up. Other bounds assume a small session count: an owner's
tab layout holds at most MAX_TAB_REFS (512) refs and PUT
/api/session-order accepts at most 500 ids, so with
CODEMAN_MAX_SESSIONS=600 the 513th session creation answered HTTP 500
instead of the capacity message. The value is now clamped to a named
MAX_CONCURRENT_SESSIONS_CEILING of 500; any digit string above it
(including one past the safe-integer range) resolves to 500 instead of
falling back to 50. The resolver reads process.env directly, the unused
env parameter is gone.
Docs: a CODEMAN_MAX_SESSIONS row in the Settings Reference environment
table, the env name next to the cap in the cron guide and the
architecture invariants, and both copies of the skill's endpoints
reference now give the per-user cap as half the global cap (25 by
default). The changeset says "1 to 500". The test drops the
`as unknown as string` casts and adds '1000', '501' and an
out-of-range value, each refusing at 500.
Co-Authored-By: Christian Haberl <116439789+christianhaberl@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
- cron resolvePrompt(): give the reason the rest of the cron docs give (the
submit check cannot recognise a multi-line paste; in paste mode the newlines
reach the CLI as keys) instead of the old one about writeViaMux fusing lines.
- sendInputToPane(): it types with send-keys -l and strips line breaks, so it
no longer uses "the same approach as sendInput()".
- TUI client: newlines become spaces (the docstring said stripped), and the
test name no longer cites "multi-line breaks Ink".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Auto-compact writes `/compact <prompt>\r` through writeViaMux when the token
count crosses the threshold, unattended. AutoCompactSchema.prompt accepts line
breaks, so a prompt set through the API can hold several lines. The mux used to
strip them and run the lines together; it now pastes multi-line text as one
block, and the check that re-presses a stranded Enter cannot recognise that
paste in the composer. That is the case respawn's update and kickstart prompts
were already joined for.
The join moves out of respawn-controller into a shared toSingleLine()
(src/utils/single-line.ts), used by both respawn steps and by auto-compact.
Behaviour for respawn is unchanged. The respawn test file becomes
test/unattended-single-line-prompt.test.ts and gains an auto-compact block that
feeds a real Session a token count over its threshold and reads what reaches
writeViaMux.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The update and kickstart prompts are textareas in the respawn settings and
the schema accepts line breaks, so a saved config can hold several lines.
Both steps went out through writeViaMux, which used to strip every `\n` and
run the lines together. Since the mux now pastes multi-line text as one
block, a multi-line respawn prompt would have gone out as a multi-line paste,
and the check that re-presses a stranded Enter cannot recognise that paste in
the composer. Respawn runs unattended, the same situation for which cron
keeps rejecting multi-line prompts.
singleLineRespawnPrompt() joins the lines with a space before the write; a
single-line prompt is passed through unchanged. Respawn joins rather than
rejects, so a multi-line config that was accepted before is still sent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TmuxManager.sendInput() removed every `\n` before the text reached tmux, so a
multi-line prompt sent with `useMux` (or a multi-line respawn prompt) arrived as
one line with its lines run together: `"echo A\necho B\r"` ran `echo Aecho B`.
The text half of a submit has gone in as one bracketed paste
(buildPasteTextCommands) since the Codex paste-burst fix, and inside the paste
markers Claude Code and Codex insert a pasted line break into the composer
instead of submitting. So the `\n` is now kept (`\r\n` loses only its `\r`) and
the existing paste carries the lines; the Enter stays a separate tmux call.
Multi-line text without an Enter is pasted too rather than typed with
`send-keys -l`, where every line break would reach the pane as a key.
Single-line input is unchanged.
The test-mode gate moves from the top of sendInput() into inputRunner(), as
readPaneRows() does, so a test subclass can record the commands sendInput()
would run instead of pinning the branch shapes on the source.
SubmitVerifier still compares the composer's last glyph line with the prompt
minus its newlines, so it does not reliably recognise a multi-line paste and
may not re-press an Enter that Claude Code ignored. `agent send` and the TUI
therefore keep refusing or joining multi-line input; the docs that described
the stripping now describe the paste and that limit.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
reconcileSessions() adopts codeman-* sessions it finds on the socket but has
no record of (a lost or stale mux-sessions.json) as "Restored: ..." entries,
and set their workingDir to process.cwd(). Every such session then looked like
it lived wherever the server was started, and a later respawn of it cd'd
there instead of into its project.
tmux already knows the answer as #{pane_current_path}. It is appended as the
last field of PANE_LIST_FORMAT, so a directory containing the separator stays
whole, and parsePaneRows() reads it as PaneRow.currentPath. No extra tmux call:
the same list-panes read that yields the pid now yields the directory.
The path is set by whoever owns the pane, so it is admitted only if it passes
isValidPath(), the check createSession() and respawnPane() already apply to a
workingDir; otherwise, and when tmux reported none, process.cwd() stays the
fallback. A hand-split session takes the first pane tmux lists.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cap of 50 concurrent sessions was a hard-coded constant, so a machine
with the memory for more (or a small box that wants fewer) had to patch the
source. CODEMAN_MAX_SESSIONS now sets it at startup. The default stays 50,
and anything that is not a positive integer (empty, 0, negative, fractional,
1e3, out of safe-integer range) falls back to it, so a typo never locks
session creation.
Every site keeps reading MAX_CONCURRENT_SESSIONS, so the session routes,
cron launches, the scheduled-run loop and the multi-user per-user cap (half
the global cap unless CODEMAN_MAX_SESSIONS_PER_USER is set) all follow the
variable. The test goes through sessionCapacityState and
sessionCapacityMessage, the check every session-creating route uses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The nightly browser suite failed in run-mode-menu-scroll: the short-menu case
opened the stock menu and asserted it needs no scrollbar on a 390x640 phone.
The GitHub Copilot entry made the stock CLI list eleven rows, 562px of content
against a 538px cap, so the menu now scrolls there, which is the cap-and-scroll
behaviour the CSS is written for (and what the other two cases verify). Every
further CLI would have broken the same assumption again.
The case now hides the whole CLI block (the container, since its buttons are
rebuilt from the registry) and also checks the "not stretched" half of its name:
the border-box height must stay under max-height. clientHeight would not do,
since it leaves out the border and a menu stretched to its cap reads just under it.
Visible side effect worth knowing: on a phone about 640px tall the stock Run
dropdown now scrolls by roughly one row. At 900px it fits (564px) unscrolled.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex cites files as [label](/abs/path). The chat view gave those links target=_blank, so a click opened <origin>/abs/path and landed on a Fastify 404. Path-shaped hrefs now take the linkified-path shape the file preview already opens; scheme, protocol-relative and fragment links are unchanged.
Resolve Copilot forwarding and registry/documentation overlaps against upstream e63bb65b. Reuse upstream always policy for measured version-independent forwarders and test slow clicks for Claude, Codex and Copilot.
Under 600px the window stays full screen whatever Expand or a drag says, and both
controls are hidden. Stepping onto a folder-grid entry goes through the grid so its
index follows. Alt+left/right is swallowed at the ends of the list too. Adds tests
for the key handler, the grip clamp and the history/grid case, zh-CN titles and wiki lines.
Address the remaining PR #645 review: preserve shell Ctrl/sideways local scrolling, restore the paging threshold export, document tile leading-report lifetime, and update internal and user scrolling documentation.
Refs: https://github.com/johoja12/Codeman/issues/7
- A closing notice card stays in the DOM for its 200 ms fade, and its
link and "See what's new" buttons still worked there: a double click
opened the notice twice. close() now makes the card inert and every
button handler does nothing once it is closing.
- Notice links (and the combined card's "See what's new") open with
noopener,noreferrer. noopener alone still sent this instance's origin,
which can be a tailnet name or a tunnel URL, as the Referer. The plain
update toast is unchanged.
- The https rule is checked again at the button
(CodemanSecurityNotices.urlOk): a caller handing showSecurityNotice or
showUpdateNotice an unchecked URL gets no link button.
- A detached session window (/session/:id) is rendered without
__codemanCliAvailable, so it could never select the OpenCode notice,
yet it took the update toast and recorded the version, and the
dashboard then showed the notice as a separate card. While any notice
is pending, a solo window now shows and records nothing and leaves
both to the dashboard; with none pending it shows the plain toast as
before.
- The wiring tests pin the page's own clock (JSDOM gives the window its
own Date). On the real clock 18 of them would have failed on every push
from 2027-01-01, when the fixture notice expires. New tests pin the
once-only close directly (the old double-click comment claimed more
than it checked), the single open, the sink check, the solo windows,
the scroll cap and the light-skin ink.
- CLAUDE.md now says the public .json files are served no-cache, the
exception to the year-long immutable cache.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>