mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix: correct the pi wheel docs and comments (release review)
The gate comment in terminal-ui.js said a stale-false flag falls through to the PageUp/PageDown fallback, which is false for pi since the second commit of this PR removed that fallback. The FOOTGUN comment above it made the same claim. Say what happens instead. "Every pi below 1.0.0 emits no mouse enables" was too absolute: regular was the default there, but fullscreen exists from 0.84.0. Measured with --tui-mode fullscreen under tmux on 0.84.1, 0.85.0, 0.86.0 and 0.99.2: alternate_on=1, mouse_any_flag=1, mouse_sgr_flag=1. Default settings on 0.99.2 report all 0. Document that Wheel Scrolls Local History leaves the wheel dead in fullscreen pi (wiki, integration guide, changeset), add a pi block to Troubleshooting, cross-reference the accepted exception from the dead gesture rule in architecture-invariants, and mark the pi-integration-plan passages this PR reversed as superseded. Comments and docs only, no runtime change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b5aa3fbdf7
commit
b6eea702a8
8 files changed
+50
-17
No files matched your search
@@ -61,7 +61,7 @@ Pure helpers unit-tested in `test/base-path.test.ts` + `test/webview-proxy.test.
|
||||
|
||||
Model is NOT a session field: it is a composition entry in the profile's config tree (`agent-default-model`), configured in `~/.dsh/settings.yaml` + `cordis.patch.yml`, so both create paths deliberately resolve no model for this mode. Env allowlist: `DSH_*` + `DEEPSEEK_*`; provider keys named by a settings-file `apiKeyEnv` stay OUT, which is pi's 34-provider-key problem in a new shape and gets the same answer. Docker seeds `~/.dsh` per-file (`.env`, `settings.yaml`, `cordis.patch.yml`) and the image installs its OWN profile, because `profiles/` is a per-profile `node_modules` tree — host-arch-specific and far too large to copy per container start. Stays OUT of `isAltScreenStripMode()` (third-party fullscreen TUI — the opencode case, i.e. `isMuxMouseStripMode` is the shape to measure here too). ⚠️ `classifyProfile()` reads the profile's BUNDLES, and "unknown means launchable" is deliberate (anyone can publish an app bundle), but it has one knowably-wrong case: `readProfile()` returns an empty bundle list for a `package.json` with no `dsh.profile.bundles`, which made the SHIPPED `web`/`headless` profiles look third-party and launchable. The directory name is therefore consulted as a LAST resort (`STOCK_NON_INTERACTIVE_PROFILES`), after the bundle patterns, so real bundle evidence always wins over a name the user chose. The loose `tui` arm carries word boundaries for the same reason: it decides which profile boots by default, and matching the middle of `intuition` is not a rule anyone could predict. ⚠️ The generated shim is written **temp + rename**, not in place: the TUI can be exec'ing that exact path while an upgraded Codeman refreshes it, and a half-written file is a syntax error the caller then retries four times per state change forever. Bump `SHIM_VERSION` whenever `SHIM_SOURCE` changes, or an existing shim keeps matching the embedded marker and is never refreshed. Availability via `GET /api/deepseek/status`, the widest per-CLI status shape (`available`/`runnable`/`path`/`version`/`dshHome`/`defaultProfile`/`profiles`); `POST /api/deepseek/install-profile` bootstraps a profile and is the only endpoint in Codeman that installs third-party code — regex-confined specifier, argv-array spawn, privileged grant required in multi-user mode, and the held-open request is bounded by a HAND-ROLLED timeout over a `detached: true` process group (negative-pid SIGTERM→SIGKILL, as `runGit()` does in git-clone.ts). ⚠️ Node's own `spawn` `timeout` is NOT enough: a plugin install fans out into package-manager children, the built-in timeout signals only the direct child, and the survivors hold the inherited stdio pipes open so `close` never fires and the request leaks forever. User guide: `docs/deepseek-integration.md`. Tests: `test/deepseek-mode.test.ts`, `test/deepseek-cli-resolver.test.ts`.
|
||||
|
||||
**Pi specifics** (#206, `docs/pi-integration.md`): command built by `buildPiCommand()` (`--model` — the only builder whose model regex admits `:` and `/`, for `sonnet:high` and `openai/gpt-4o` — plus `--provider`, `--thinking`, `--session <id>` / `-c`, and the TRI-STATE `--approve`/`--no-approve`). ⚠️ **Pi has no permission prompts and no sandbox**, so there is no `--dangerously-skip-permissions` analog and Codeman must not invent one; the privilege-shaped knob is `approveProjectTrust`, which makes pi LOAD AND EXECUTE repo-local `.pi/extensions` TypeScript and npm-install missing project packages. It therefore joins `clampExternalCliBypassForOwner()`'s **materialize** branch (gemini's, not codex/antigravity's only-if-sent one): an absent config still yields `--no-approve` for a non-granted owner, because pi's own default is an interactive prompt the session user could answer themselves. ⚠️ `--api-key` is NEVER wired — it would put a provider secret on the spawn command line. ⚠️ Pi stays **out** of `isAltScreenStripMode()`: it takes the mouse strip instead, because its TUI is fullscreen by default since 1.0.0 (`tuiMode`) and inline with terminal-owned scrollback under `tuiMode: regular` and below 1.0.0, so the full strip's `3J` removal would have no owner in the second shape. See "Pi is the same shape" in the scrollback section. ⚠️ Only the `PI_*` env prefix was added; pi's ~34 provider keys share no prefix and `ALLOWED_ENV_PREFIXES` is a single GLOBAL list with no mode context, so admitting them would widen the allowlist for every mode at once (a mode-aware allowlist is the tracked follow-up). ⚠️ `pi` is a short, GENERIC binary name, so unlike the sibling resolvers `pi-cli-resolver.ts` sanity-probes `pi --version` (cached, vitest-skipped) and requires semver-shaped output; `GET /api/pi/status` carries `version` on top of the sibling `{available, path}` shape so a misresolution is diagnosable. Local echo: pi lands on the `'buffer'` overlay via the fallthrough in `_updateLocalEchoState` (pinned in `test/local-echo-codex-gating.test.ts`); if pi's live composer turns out to fight it the way codex's did, the fallback is one `'off'` branch. Tests: `test/pi-mode.test.ts`, `test/routes/external-cli-bypass-clamp.test.ts` (first-ever coverage of the clamp).
|
||||
**Pi specifics** (#206, `docs/pi-integration.md`): command built by `buildPiCommand()` (`--model` — the only builder whose model regex admits `:` and `/`, for `sonnet:high` and `openai/gpt-4o` — plus `--provider`, `--thinking`, `--session <id>` / `-c`, and the TRI-STATE `--approve`/`--no-approve`). ⚠️ **Pi has no permission prompts and no sandbox**, so there is no `--dangerously-skip-permissions` analog and Codeman must not invent one; the privilege-shaped knob is `approveProjectTrust`, which makes pi LOAD AND EXECUTE repo-local `.pi/extensions` TypeScript and npm-install missing project packages. It therefore joins `clampExternalCliBypassForOwner()`'s **materialize** branch (gemini's, not codex/antigravity's only-if-sent one): an absent config still yields `--no-approve` for a non-granted owner, because pi's own default is an interactive prompt the session user could answer themselves. ⚠️ `--api-key` is NEVER wired — it would put a provider secret on the spawn command line. ⚠️ Pi stays **out** of `isAltScreenStripMode()`: it takes the mouse strip instead, because its TUI is fullscreen by default since 1.0.0 (`tuiMode`) and inline with terminal-owned scrollback under `tuiMode: regular`, the default below 1.0.0, so the full strip's `3J` removal would have no owner in the second shape. See "Pi is the same shape" in the scrollback section. ⚠️ Only the `PI_*` env prefix was added; pi's ~34 provider keys share no prefix and `ALLOWED_ENV_PREFIXES` is a single GLOBAL list with no mode context, so admitting them would widen the allowlist for every mode at once (a mode-aware allowlist is the tracked follow-up). ⚠️ `pi` is a short, GENERIC binary name, so unlike the sibling resolvers `pi-cli-resolver.ts` sanity-probes `pi --version` (cached, vitest-skipped) and requires semver-shaped output; `GET /api/pi/status` carries `version` on top of the sibling `{available, path}` shape so a misresolution is diagnosable. Local echo: pi lands on the `'buffer'` overlay via the fallthrough in `_updateLocalEchoState` (pinned in `test/local-echo-codex-gating.test.ts`); if pi's live composer turns out to fight it the way codex's did, the fallback is one `'off'` branch. Tests: `test/pi-mode.test.ts`, `test/routes/external-cli-bypass-clamp.test.ts` (first-ever coverage of the clamp).
|
||||
|
||||
**OMP (`omp`, Oh My Pi) specifics** (`docs/omp-integration.md`): architecturally the simplest of the family — omp owns its own auth, provider routing, and trust decisions entirely in `~/.omp` config files (default `tools.approvalMode: yolo`), so `buildOmpCommand()` only ever emits `--model`/`--resume <id>`/`--continue`, and there is no bypass-permissions flag for Codeman to wire or clamp. ⚠️ **That does NOT make the multi-user clamp a no-op**: `OMP_*` is an allowlisted `envOverrides` prefix and admits `OMP_AUTH_BROKER_URL`/`OMP_AUTH_BROKER_TOKEN` (where omp resolves credentials from), both dropped for a non-granted owner in `clampEnvOverridesForOwner()` — the same shape as `DEEPSEEK_BASE_URL` — even though, unlike DeepSeek, Codeman forwards no operator-held key into an omp pane today (found in Ark0N/Codeman#353 review). `--continue` alone is ambiguous the moment any other omp conversation has touched the same working directory more recently, since it just picks the newest session file on disk — `resolveAndClaimOmpSessionId()` (`src/utils/omp-session-resolver.ts`) resolves and PINS the real id instead, verifying each candidate's own file header (`{"type":"session","id",cwd"}`, not just the mangled-directory match) and tracking already-claimed ids in a process-wide registry so two omp tabs in the same case dir can't alias onto each other's conversation. ⚠️ Resolution/pinning happens ONLY at the point a respawn is actually confirmed (`_pinOmpRespawnId()`, called from `_setupOrAttachMuxSession()`'s dead-pane branch and `reattachRemote()`) — earlier code resolved eagerly while merely building respawn options, which could mis-pin a still-ALIVE session's id purely from boot-recovery timing. `src/omp-transcript.ts` independently scans `~/.omp/agent/sessions/**/*.jsonl` for Past Sessions history, the omp analog of Claude's own transcript scan, so a conversation survives even a full "Kill Tmux". ⚠️ omp's own env knobs are mostly `PI_*`, not `OMP_*` (`PI_CONFIG_DIR`, `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, `PI_SUBPROCESS_CMD`, `PI_SHELL_PREFIX`), and `PI_*` is already allowlisted globally for pi — so a redirected `PI_CONFIG_DIR` silently moves the `~/.omp` tree the resolver and transcript scanner hardcode, degrading pinning/history with no error; a known gap shared with pi, not fixed here. ⚠️ Docker: `appendResumeFlag()`'s `case 'omp'` keys off the top-level `resumeSessionId`, which Docker panes never receive for omp (built from `defaultDockerCommandForMode`, with no `ompConfig` threaded through) — host-side history recovery and pinning work through the shared `sessions/` mount, but `--resume` does not currently reach an in-container omp process on respawn (flagged in review, not yet fixed). Stays out of `isAltScreenStripMode()` (narrow scrollback strip, alt-screen toggles only) and lands on the `'buffer'` local-echo policy via the `_updateLocalEchoState` fallthrough, same as grok and pi. Resolver: `omp-cli-resolver.ts` version-probes like pi/grok (`omp` is a short, generic name) and requires `omp/<semver>`-shaped output; `OMP_SEARCH_DIRS` leads with `~/.local/bin` (omp.sh's installer targets `$HOME/.local/bin` with no `--dir` override — verified against a real `--no-cache` Docker build, `~/.omp/bin` was the wrong first guess). Tests: `test/omp-mode.test.ts`, `test/omp-cli-resolver.test.ts`, `test/omp-session-resolver.test.ts`, `test/omp-fresh-run-no-resume.test.ts`.
|
||||
|
||||
@@ -239,7 +239,7 @@ Further detail: the `<prefix>: <title>` form (`w3-myapp: fix the login redirect`
|
||||
|
||||
**Copilot also receives forwarded reports** (1.0.95 measured in tmux: alt screen, `mouse_any_flag=1`): its mouse DECSETs are stripped so a drag selects, which also stops xterm encoding the wheel for it, so `_shouldForwardWheelToApp` sends the same SGR reports by hand while `cliMouseTracking` is true; six `\x1b[<64;…M` reports moved its transcript 18 lines, a phone swipe takes the same gate, and there is NO version gate.
|
||||
|
||||
**Pi is the same shape, and the reason it moved to the mouse strip** (fullscreen is the default `tuiMode` since 1.0.0; 1.0.0, 1.1.0, 1.4.0 and 1.5.0 measured in tmux with default settings: `alternate_on=1`, `history_size=0`, `mouse_any_flag=1`, SGR): pi enables mouse tracking ONCE at start. Left on the narrow strip, the wheel worked only until Codeman replayed the buffer (tab switch, reload): the replay carries no DECSETs, xterm's `mouseTrackingMode` fell to `'none'` with `baseY` 0, and the wheel hit an empty local buffer until a resize made pi redraw. Measured on a fresh session: `drag` right after the first select, `none` after a tab switch and after a reload. With the mouse strip the server records the tracking (`cliMouseTracking`), which survives the replay; `_shouldForwardWheelToApp` forwards by hand while it is true. One SGR wheel report moves pi's transcript one row at a low rate, and pi's `fullscreenWheelScrollLines` default accelerates a fast spin. Under `tuiMode: regular` and below 1.0.0 pi is inline and emits no mouse DECSET, so the strip finds nothing and the flag stays false. ⚠️ Pi deliberately has no `transcriptPageKeys`: fullscreen pi does page on PageUp/PageDown, but pi binds those keys to its editor in regular mode too, where they page nothing and move the caret of a multi-line draft, so the hollow-buffer fallback sent the next keystroke to the wrong line (measured on 1.1.0, 1.4.0 and 1.5.0). The fallback would need a capability to tell the two modes apart, since `_localScrollbackIsHollow` takes no id. The cost: a fullscreen pi with "Wheel scrolls local history" on has no paging fallback.
|
||||
**Pi is the same shape, and the reason it moved to the mouse strip** (fullscreen is the default `tuiMode` since 1.0.0; 1.0.0, 1.1.0, 1.4.0 and 1.5.0 measured in tmux with default settings: `alternate_on=1`, `history_size=0`, `mouse_any_flag=1`, SGR): pi enables mouse tracking ONCE at start. Left on the narrow strip, the wheel worked only until Codeman replayed the buffer (tab switch, reload): the replay carries no DECSETs, xterm's `mouseTrackingMode` fell to `'none'` with `baseY` 0, and the wheel hit an empty local buffer until a resize made pi redraw. Measured on a fresh session: `drag` right after the first select, `none` after a tab switch and after a reload. With the mouse strip the server records the tracking (`cliMouseTracking`), which survives the replay; `_shouldForwardWheelToApp` forwards by hand while it is true. One SGR wheel report moves pi's transcript one row at a low rate, and pi's `fullscreenWheelScrollLines` default accelerates a fast spin. Under `tuiMode: regular`, the default below 1.0.0, pi is inline and emits no mouse DECSET, so the strip finds nothing and the flag stays false; a pi from 0.84.0 set to fullscreen does emit them (0.84.1 and 0.99.2 with `--tui-mode fullscreen` measured: `alternate_on=1`, `mouse_any_flag=1`, `mouse_sgr_flag=1`; default settings all 0), and the flag covers that case. ⚠️ Pi deliberately has no `transcriptPageKeys`: fullscreen pi does page on PageUp/PageDown, but pi binds those keys to its editor in regular mode too, where they page nothing and move the caret of a multi-line draft, so the hollow-buffer fallback sent the next keystroke to the wrong line (measured on 1.1.0, 1.4.0 and 1.5.0). The fallback would need a capability to tell the two modes apart, since `_localScrollbackIsHollow` takes no id. The cost: a fullscreen pi with "Wheel scrolls local history" on has no paging fallback, so the wheel does nothing there. That is an accepted exception to "A false gate on a hollow pane must not mean a DEAD gesture" below.
|
||||
|
||||
Codex 0.163.0 approval check in that isolated read-only/on-request session: the "Would you like to run the following command?" prompt remained on `1. Yes, proceed` after SGR wheel-up and wheel-down over both the transcript and approval option rows 36/37; the requested marker file was not created. The background transcript can scroll while the approval prompt remains fixed. Codeman does alert on this prompt: `src/web/screen-blocked.ts` reads Codex dialogs off the pane (`capabilities.workDetect.blockedLine`) and broadcasts `hook:permission_prompt`, which `_onHookPermissionPrompt` (settings-ui.js) records and `updateTabAlertFromHooks` (app.js) turns into the `'action'` tab alert, and both page-key fallbacks check it. The SGR wheel path deliberately does not consult that alert, because this measurement showed wheel reports leave the approval selection unchanged. Native Codex owns its approval selection; do not mistake a hook-only unit fixture for approval-dialog evidence.
|
||||
|
||||
@@ -247,7 +247,7 @@ Native wheel travel retains upstream pixel/line/page units (100 px = 4 lines; Fi
|
||||
|
||||
**Wheel/touch forwarding is NOT gated on viewport-at-bottom** (#205, `terminal-ui.js:_shouldForwardWheelToApp`): for sessions verified to scroll their own transcript on SGR wheel reports (Claude ≥ 2.1.187 or tracked Codex, with observed `cliMouseTracking`; see the native measurement above), the plain wheel AND touch drags forward as coalesced SGR reports (`_forwardScrollToApp` → `_sendSyntheticSgrWheel`, 40ms batches after the first native report, 5-tick per-event cap; no aggregate byte/tick limit). It used to gate on the viewport being at the bottom so both scrollbacks stayed reachable, but a repaint-mode CLI keeps NO terminal scrollback of its own — xterm's buffer holds only replayed repaint frames, so local scrolling drags the CLI's pinned prompt box up the screen over stale frames; and `scrollToLastNonEmptyLine()` routinely parked the viewport off-bottom, silently pinning the wheel to local. Forwarding now snaps the viewport home first (SGR coordinates address the LIVE screen — a report computed from a scrolled-up viewport would hit-test the wrong row). Local scrollback remains on Shift+wheel and the `terminalWheelLocalScrollback` opt-out (both also cover touch via the shared gate; touch has no Shift, so the setting is its only local pin). Touch asks `_shouldForwardTouchToApp()`, which is that gate plus one branch: in any session mode, a drag and its fling momentum also forward as SGR wheel reports while xterm itself tracks the mouse (`modes.mouseTrackingMode` other than `none`/`x10`) with SGR encoding (`_core.coreMouseService.activeEncoding === 'SGR'`, an xterm private pinned by `test/xterm-private-api.test.ts`; DEFAULT, X10 and SGR-pixel apps fail closed). xterm encodes a native wheel for such an app itself, but the touch listeners consume the drag, and a local scroll shows nothing useful: a direct-PTY app is on xterm's alternate screen, and in a tmux-backed session xterm stays in the normal buffer holding stale tmux repaints. `_wheelScrollLinesFloat()` normalizes `deltaMode` (Firefox fires LINE deltas ≈3/notch — read as pixels that rounded to 0 and fell to the ±1 fallback, ~4× too slow; PAGE deltas scale by `terminal.rows`) while keeping the #154 Shift-axis trap (macOS trackpads put Shift+scroll magnitude on deltaX). Tests: `test/terminal-touch-tap.test.ts`.
|
||||
|
||||
**A false gate on a hollow pane must not mean a DEAD gesture** (#205 round 2, `_maybePageCliTranscript`): every way `_shouldForwardWheelToApp()` returns false leaves a repaint-mode pane scrolling a buffer that has nothing in it (`baseY === 0`) — the version probe came back empty, the CLI really is older than 2.1.187, the `cliMouseTracking` flag is unset (inline claude, or fullscreen right after a server restart), or the user turned on `terminalWheelLocalScrollback`. **opencode is the fifth case, and the gate is false there by design**: its TUI runs on the ALTERNATE SCREEN (1.18.31 measured: tmux `alternate_on=1`, `history_size=0`), so the buffer is hollow, and it IGNORES SGR wheel reports entirely (six `\x1b[<64;…M` reports against an idle pane left the capture byte-identical) while still paging its transcript on PageUp/PageDown (`messages_page_up/down`) — so paging is the only gesture that can reach it, and without it the wheel was silently dead in every opencode tab. The 1.12.0 retest reported exactly that shape for Claude: a wheel that did nothing at all while Fn+Up (PageUp) paged back through intact text, which is the proof that the CLI's own history and the PTY input path were both fine. So under the guard (`_localScrollbackIsHollow()`: the `transcriptPageKeys` capability, gate false, `baseY === 0`) wheel and touch travel is translated into coalesced `\x1b[5~` / `\x1b[6~` through the same 40ms queue as the SGR reports, at half a screen of travel per page key (the key jumps a whole screen; a 1:1 mapping was unusably slow with a discrete wheel). ⚠️ **`baseY === 0` with a pane that still REPORTS scrollback is not hollow, it is unloaded** (`paneHistoryLines` > 0, the last capture's `#{history_size}`): after a server restart the byte recording a tab switch replays (`?tail=`) holds only the tmux client's attach repaint, which scrolls nothing, so an inline Claude session landed here and the page keys moved its composer cursor instead of the transcript (measured 2026-10-21 on the second select after a restart, until a page reload). That case falls through to local scrolling, whose top-of-buffer pull (`_maybeLoadMoreHistoryOnScroll` to `_maybeRefetchFullHistory`) fetches `full=1`, and the gesture scrolls on from there once the pull lands (a slow response can outlive the gesture; the next one scrolls); 0 and unknown counts keep paging (fullscreen Claude, codex, opencode; a remote pane reports no count and a Docker pane 0). Page keys already queued and the gesture debt are dropped on the way out, since they belong to the abandoned route. The `[scroll]` line prints `paneHistoryLines` next to `localScrollbackRows`, and a count turning positive re-logs, so the two states read apart. The trade-off is accepted: while that pull is in flight or has failed, the gesture scrolls an empty buffer and the next upward gesture after the 4 s cooldown retries it, where paging would have moved the composer. An error envelope (`success: false`) records no count, in the pull, the refresh path and the tab switch alike, so one failed capture does not hand the gesture back to paging. ⚠️ The first event of a gesture pages AT ONCE, for any opening event under one page, and the travel it skipped is owed back by the rest of the gesture (one page per half screen still). Waiting for half a screen first meant an ordinary trackpad flick (well under 19 rows on a 38-row pane) sent nothing at all, and a mouse sent nothing for its first 5 notches (100 px is 4 rows; 7 on Firefox's 3-line notches, issue #697). An opener of `PAGE_KEY_IMMEDIATE_MAX_ROWS` (2) rows or more is treated as a mouse-wheel notch and tagged `notch`: its debt lasts `PAGE_KEY_NOTCH_DEBT_MS` (3 s) instead of `PAGE_KEY_DEBT_MS`, so slow notches repay one page instead of each paging a full screen; past 3 s of quiet each notch pages. The signal is the opener's size, not the device (a touch contact whose first touchmove is 2 rows or more, or a brisk flick that opens large, is treated the same). A new opener keeps same-direction travel left over from the gesture before, so a run of notches still pages once per half screen of travel. ⚠️ What starts a NEW gesture (and so may pre-pay) is narrow, because every boundary is another free page: (1) a TOUCH CONTACT is one gesture, never split by time: the touch paths tag every call, touchmove and momentum frames alike, with `touchId` (one per touchstart), and a different id (or a wheel event after a touch) is a boundary. touchmove only calls once per whole cell of finger travel, so a slow drag arrives as single lines further apart than any gap; on the gap alone 12 lines at 170 ms each sent 12 PageUps (measured through the real listeners, `test/terminal-touch-paging.browser.test.ts`). (2) Outside a contact, 150 ms of quiet (`PAGE_KEY_GESTURE_GAP_MS`), stretched to one second (`PAGE_KEY_DEBT_MS`), or `PAGE_KEY_NOTCH_DEBT_MS` (3 s) for a notch-sized opener, while a pre-paid page is still owed in the SAME direction, so slow smalLine truncated
|
||||
**A false gate on a hollow pane must not mean a DEAD gesture** (#205 round 2, `_maybePageCliTranscript`): every way `_shouldForwardWheelToApp()` returns false leaves a repaint-mode pane scrolling a buffer that has nothing in it (`baseY === 0`) — the version probe came back empty, the CLI really is older than 2.1.187, the `cliMouseTracking` flag is unset (inline claude, or fullscreen right after a server restart), or the user turned on `terminalWheelLocalScrollback`. ⚠️ Pi is an accepted exception: it declares no `transcriptPageKeys`, so a false gate on a fullscreen pi stays dead, for the reason in "Pi is the same shape" above. **opencode is the fifth case, and the gate is false there by design**: its TUI runs on the ALTERNATE SCREEN (1.18.31 measured: tmux `alternate_on=1`, `history_size=0`), so the buffer is hollow, and it IGNORES SGR wheel reports entirely (six `\x1b[<64;…M` reports against an idle pane left the capture byte-identical) while still paging its transcript on PageUp/PageDown (`messages_page_up/down`) — so paging is the only gesture that can reach it, and without it the wheel was silently dead in every opencode tab. The 1.12.0 retest reported exactly that shape for Claude: a wheel that did nothing at all while Fn+Up (PageUp) paged back through intact text, which is the proof that the CLI's own history and the PTY input path were both fine. So under the guard (`_localScrollbackIsHollow()`: the `transcriptPageKeys` capability, gate false, `baseY === 0`) wheel and touch travel is translated into coalesced `\x1b[5~` / `\x1b[6~` through the same 40ms queue as the SGR reports, at half a screen of travel per page key (the key jumps a whole screen; a 1:1 mapping was unusably slow with a discrete wheel). ⚠️ **`baseY === 0` with a pane that still REPORTS scrollback is not hollow, it is unloaded** (`paneHistoryLines` > 0, the last capture's `#{history_size}`): after a server restart the byte recording a tab switch replays (`?tail=`) holds only the tmux client's attach repaint, which scrolls nothing, so an inline Claude session landed here and the page keys moved its composer cursor instead of the transcript (measured 2026-10-21 on the second select after a restart, until a page reload). That case falls through to local scrolling, whose top-of-buffer pull (`_maybeLoadMoreHistoryOnScroll` to `_maybeRefetchFullHistory`) fetches `full=1`, and the gesture scrolls on from there once the pull lands (a slow response can outlive the gesture; the next one scrolls); 0 and unknown counts keep paging (fullscreen Claude, codex, opencode; a remote pane reports no count and a Docker pane 0). Page keys already queued and the gesture debt are dropped on the way out, since they belong to the abandoned route. The `[scroll]` line prints `paneHistoryLines` next to `localScrollbackRows`, and a count turning positive re-logs, so the two states read apart. The trade-off is accepted: while that pull is in flight or has failed, the gesture scrolls an empty buffer and the next upward gesture after the 4 s cooldown retries it, where paging would have moved the composer. An error envelope (`success: false`) records no count, in the pull, the refresh path and the tab switch alike, so one failed capture does not hand the gesture back to paging. ⚠️ The first event of a gesture pages AT ONCE, for any opening event under one page, and the travel it skipped is owed back by the rest of the gesture (one page per half screen still). Waiting for half a screen first meant an ordinary trackpad flick (well under 19 rows on a 38-row pane) sent nothing at all, and a mouse sent nothing for its first 5 notches (100 px is 4 rows; 7 on Firefox's 3-line notches, issue #697). An opener of `PAGE_KEY_IMMEDIATE_MAX_ROWS` (2) rows or more is treated as a mouse-wheel notch and tagged `notch`: its debt lasts `PAGE_KEY_NOTCH_DEBT_MS` (3 s) instead of `PAGE_KEY_DEBT_MS`, so slow notches repay one page instead of each paging a full screen; past 3 s of quiet each notch pages. The signal is the opener's size, not the device (a touch contact whose first touchmove is 2 rows or more, or a brisk flick that opens large, is treated the same). A new opener keeps same-direction travel left over from the gesture before, so a run of notches still pages once per half screen of travel. ⚠️ What starts a NEW gesture (and so may pre-pay) is narrow, because every boundary is another free page: (1) a TOUCH CONTACT is one gesture, never split by time: the touch paths tag every call, touchmove and momentum frames alike, with `touchId` (one per touchstart), and a different id (or a wheel event after a touch) is a boundary. touchmove only calls once per whole cell of finger travel, so a slow drag arrives as single lines further apart than any gap; on the gap alone 12 lines at 170 ms each sent 12 PageUps (measured through the real listeners, `test/terminal-touch-paging.browser.test.ts`). (2) Outside a contact, 150 ms of quiet (`PAGE_KEY_GESTURE_GAP_MS`), stretchedLine truncated
|
||||
|
||||
**Why the wheel went where it went is LOGGED** (`_logScrollRouting`): one console line per session per distinct decision — `[scroll] <id> → forward-sgr|page-keys|page-keys-dialog-open|page-keys-menu-open|local-scrollback|repull-refused-downgrade (mode=…, cliVersion=…, localScrollbackOptOut=…, mouseTracking=…, localScrollbackRows=…)`. #205 ran two rounds of remote guesswork over questions this line answers directly; keep it when touching the routing.
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
Tracking issue: [#206 "Plans to support pi.dev?"](https://github.com/Ark0N/Codeman/issues/206)
|
||||
|
||||
**SUPERSEDED in part (PR #771, 2026-11):** what this record says about pi's default screen and
|
||||
the strips no longer holds. Fullscreen became pi's default in 1.0.0, and pi now takes the mouse
|
||||
strip (`altScreen: 'strip-mux-and-mouse'`). Each affected passage carries a SUPERSEDED note;
|
||||
current behaviour is in `docs/pi-integration.md` and the "Pi is the same shape" paragraph of
|
||||
`docs/architecture-invariants.md`.
|
||||
|
||||
Status: **IMPLEMENTED 2026-08-13** (see `docs/pi-integration.md` for the user-facing
|
||||
guide). Everything below is the design record; the open questions were resolved
|
||||
empirically against pi 0.84.1 and the answers are recorded inline as **RESULT**
|
||||
@@ -29,7 +35,7 @@ against the upstream docs in `packages/coding-agent/docs/`.
|
||||
| Config dir | `~/.pi/agent` (override: `PI_CODING_AGENT_DIR`). Holds `auth.json`, `trust.json`, `settings.json`, `models.json` (user-defined providers), `models-store.json` (cached catalogs), `keybindings.json`, `extensions/`, `skills/`, `prompts/`, `themes/`, `AGENTS.md`, `SYSTEM.md`, and the package trees `npm/` + `git/` |
|
||||
| Sessions | `~/.pi/agent/sessions/--<cwd with / replaced by ->--/<timestamp>_<uuid>.jsonl`, tree-structured (`id`/`parentId`), format v3. Overrides: `PI_CODING_AGENT_SESSION_DIR`, `--session-dir` |
|
||||
| Credentials | `~/.pi/agent/auth.json` (OAuth subscriptions + API keys, auto-refresh), plus ~34 provider env vars with **no common prefix**. 0.84.1 adds `pi auth check` (auth preflight with optional credential output) |
|
||||
| TUI | Default: **main screen with terminal-owned scrollback**. Since **0.84.0** an experimental fullscreen mode exists, selectable via `--tui-mode fullscreen` **or at runtime through `/settings`**; the default remains the main-screen mode |
|
||||
| TUI | Default: **main screen with terminal-owned scrollback**. Since **0.84.0** an experimental fullscreen mode exists, selectable via `--tui-mode fullscreen` **or at runtime through `/settings`**; the default remains the main-screen mode. **SUPERSEDED (PR #771, 2026-11):** fullscreen is the default since pi 1.0.0, and `tuiMode: regular` keeps the main screen |
|
||||
| Providers | 15+ (Anthropic, OpenAI, Google, Azure, Bedrock, Mistral, Groq, xAI, OpenRouter, Copilot, Baseten since 0.84.0, ...). OAuth subscription login via `/login` for six: ChatGPT Plus/Pro, Claude Pro/Max, GitHub Copilot, xAI, OpenRouter, Radius |
|
||||
| Permission model | **No permission prompts at all.** No built-in sandbox, no MCP (none planned), no sub-agents, no plan mode, no to-dos, no background bash. Tools run with the user's own permissions |
|
||||
| Trust model | "Project trust" gates **loading** of project-local `.pi/` config/extensions/skills and **installing missing project packages**, not tool execution. Triggered only when the cwd (or an ancestor) contains `.pi/settings.json`, `.pi/extensions\|skills\|prompts\|themes`, `.pi/SYSTEM.md`/`.pi/APPEND_SYSTEM.md`, or `.agents/skills`; a bare `.pi/` directory does NOT prompt. Global `defaultProjectTrust`: `ask` (default) / `always` / `never` |
|
||||
@@ -88,6 +94,12 @@ for pi, i.e. pi appears in none of them): the replay-side strip in `session-rout
|
||||
live-stream twin in `session.ts`, and the frontend `_sessionUsesServerMouseStrip()` in
|
||||
`terminal-ui.js` (usages `:3432`, `:3697`).
|
||||
|
||||
**SUPERSEDED (PR #771, 2026-11):** pi still stays out of `isAltScreenStripMode()`, but it is no
|
||||
longer outside every strip: it takes the mouse strip (`isMuxMouseStripMode()`), which removes the
|
||||
alt-screen toggles and the mouse enables and keeps `3J`. Fullscreen is its default since 1.0.0, the
|
||||
enables are recorded as `cliMouseTracking`, and the wheel is forwarded from that flag, so it survives
|
||||
a tab switch or reload. A `/settings` flip of `tuiMode` is followed in both directions.
|
||||
|
||||
### 2.3 tmux required, no direct-PTY fallback, no per-mode configurator
|
||||
|
||||
Same rule as the other external CLIs: `pi` mode throws if tmux is unavailable. Add a fourth block to
|
||||
@@ -340,6 +352,7 @@ because the result lands in a `bash -c "..."` string):
|
||||
- `--tui-mode` (released in 0.84.0): never passed by Codeman. The main-screen default is the
|
||||
friendly case for the browser terminal, and fullscreen remains the user's own runtime choice via
|
||||
`/settings` (§2.2 is designed for that). `--use-theme` (still unreleased) likewise.
|
||||
**SUPERSEDED (PR #771, 2026-11):** still never passed, but fullscreen is the default since pi 1.0.0.
|
||||
- `--name <name>` (`-n`): nice for `/resume` readability, but names contain spaces and would be the
|
||||
first user-controlled value needing real shell quoting in `buildSpawnCommand`. Defer.
|
||||
- `--no-session`: ephemeral mode fights respawn/resume. Defer.
|
||||
@@ -671,7 +684,7 @@ follow-up surfaces in from the start (login-shell routing in Phase 1, availabili
|
||||
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||
| `pi` resolves to an unrelated binary | `pi --version` + semver-shape check in the resolver (§2.6); path and version shown in `/api/pi/status` |
|
||||
| Pi's TUI repaints in a way the browser terminal handles badly | Test scrollback and repaint early (step 3 of §7); pi's default is main-screen with terminal-owned scrollback, which is the friendly case |
|
||||
| Fullscreen TUI mode (shipped 0.84.0, runtime-switchable) | Already designed for: pi stays OUT of the strip list, so a user flipping `/settings` to fullscreen gets opencode-like alt-screen behavior, not corruption. §7 step 7 tests the flip explicitly |
|
||||
| Fullscreen TUI mode (shipped 0.84.0, runtime-switchable) | Already designed for: pi stays OUT of the strip list, so a user flipping `/settings` to fullscreen gets opencode-like alt-screen behavior, not corruption. §7 step 7 tests the flip explicitly. **SUPERSEDED (PR #771, 2026-11):** fullscreen is the default since pi 1.0.0 and pi now takes the mouse strip, see §2.2 |
|
||||
| The buffer local-echo overlay fights pi's live composer | §2.10: explicit E2E gate (§7 step 4) with the one-line `'off'` fallback; predictive echo for pi is a tracked follow-up, not a v1 blocker |
|
||||
| Pi moves fast (pre-1.0; 9 releases in the 7 weeks before 0.84.1) | Keep the flag surface small; every flag validated and droppable; nothing pinned in the Dockerfile beyond the `--no-cache` rebuild cadence. Live example of the hazard: `--tui-mode` went from main-only docs to released between the two drafts of this plan |
|
||||
| Docker image grows | Pi is an npm package; the layer is modest next to the ~190MB `agy` binary |
|
||||
|
||||
@@ -91,8 +91,12 @@ bypass, and project trust is a decision the person at the terminal makes.
|
||||
- **`--tui-mode`.** Which TUI you get stays your choice, in pi's `tuiMode` setting.
|
||||
Fullscreen is the default since pi 1.0.0: the wheel and clicks reach pi, and because
|
||||
Codeman strips pi's mouse enables, a drag selects text in the browser rather than
|
||||
using pi's own selection or scrollbar drag. `tuiMode: regular`, and every pi below
|
||||
1.0.0, is inline with terminal-owned scrollback and emits no mouse enables at all.
|
||||
using pi's own selection or scrollbar drag. With **Wheel Scrolls Local History** on,
|
||||
the wheel does nothing in fullscreen pi: the setting pins it to a local buffer pi
|
||||
leaves empty, and pi has no paging fallback. `tuiMode: regular`, the default below
|
||||
1.0.0, is inline with terminal-owned scrollback and emits no mouse enables at all. A
|
||||
pi from 0.84.0 set to fullscreen enables mouse tracking the same way the 1.0.0
|
||||
default does, and Codeman treats it the same.
|
||||
- **`--name`, `--no-session`, `-p`/`--print`, `--mode json`, `--mode rpc`,
|
||||
`--tools`/`--exclude-tools`, `-e`/`--extension`, `--skill`,
|
||||
`--system-prompt`.** Tracked as follow-ups in the plan doc.
|
||||
|
||||
@@ -49,7 +49,7 @@ supervised by systemd or launchd; npm installs report as non-updatable. See
|
||||
| Keep Size Under Keyboard | On | For local Claude sessions, the on-screen keyboard no longer resizes the session: the terminal keeps its rows and the view scrolls so the prompt sits just above the keyboard. Resizing made Claude reprint its screen and drop transcript lines from history. Codex, Claude's fullscreen mode, remote and Docker sessions resize as before. See [Mobile Guide](Mobile-Guide). |
|
||||
| CJK Input | Off | IME composition through a dedicated text field. |
|
||||
| Extended Keyboard Bar | Per device | Which accessory bar phones get. Shell sessions override it while they are active. |
|
||||
| Wheel Scrolls Local History | Off | Keeps the wheel on the local buffer instead of forwarding it to the CLI. |
|
||||
| Wheel Scrolls Local History | Off | Keeps the wheel on the local buffer instead of forwarding it to the CLI. Fullscreen pi leaves that buffer empty and has no paging fallback, so with this on the wheel does nothing there. |
|
||||
| Auto Copy Selection | Off | Copies highlighted terminal text to the clipboard the moment you finish selecting it. Ctrl+C still copies on demand. |
|
||||
| Trim The Pane Margin On Copy | On | Takes the left margin a full-screen agent CLI paints down its own edge off a copy, so the text pastes flush. Each CLI declares its own width, and the strip never exceeds the indent every selected line shares, so nesting is kept. Claude Code and Codex declare a margin; a shell does not. |
|
||||
| Normal / Bold font weight | xterm defaults | Per device, each slot from 100 to 900. The bundled JetBrains Mono renders every step, so a lighter normal weight makes Claude's bold headings stand out. Applies live to the terminal, both echo overlays and open team panes. |
|
||||
|
||||
@@ -221,6 +221,17 @@ Things to try:
|
||||
Codex with mouse tracking on gets the wheel forwarded like fullscreen Claude; otherwise
|
||||
it pages its transcript or scrolls locally, while `Shift+Wheel` always uses local scrollback.
|
||||
|
||||
### The wheel does nothing in a pi session
|
||||
|
||||
Fullscreen pi, the default since pi 1.0.0, scrolls its own transcript, so the wheel is
|
||||
forwarded to it while it has mouse tracking on. If the wheel does nothing, turn off
|
||||
**Wheel Scrolls Local History**: that setting keeps the wheel on a local buffer pi leaves
|
||||
empty, and pi has no paging fallback. `PageUp` and `PageDown` on a keyboard still reach pi.
|
||||
|
||||
Codeman strips pi's mouse enables, so a drag selects text in the browser rather than using
|
||||
pi's own selection or scrollbar drag. With `tuiMode: regular` pi is inline and the wheel
|
||||
scrolls local history.
|
||||
|
||||
### Selected text is invisible on a light skin
|
||||
|
||||
Update. Every skin named its selection colour under a key xterm renamed in v5, so the four
|
||||
|
||||
Reference in new issue
Block a user