mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
Merge #771 fix(pi): keep the mouse wheel working after a tab switch or reload
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
4d20b9c898
17 files changed
+172
-49
No files matched your search
@@ -1180,7 +1180,23 @@ const PI: CliEntry = {
|
||||
// (hasTrustRequiringProjectResources, pi 1.1.0), which would stall a spawned worker on
|
||||
// a dialog. pi reads the user-level `~/.agents/skills` without that prompt, so a pi
|
||||
// user installs it there (`npx skills add Ark0N/Codeman --skill codeman -g`).
|
||||
altScreen: 'preserve', // pi's TUI renders into the main screen with terminal-owned scrollback
|
||||
// Fullscreen is pi's default `tuiMode` since 1.0.0 (measured 1.0.0, 1.1.0, 1.4.0, 1.5.0 in
|
||||
// tmux: alternate_on=1, history_size=0, mouse on, SGR): a TUI that enables tracking once at
|
||||
// start. Codeman's replay (tab switch, reload) drops that mode in xterm, so the wheel hit an
|
||||
// empty local buffer and went dead until a redraw. Same shape as copilot: strip the DECSETs
|
||||
// (a drag selects locally), publish `cliMouseTracking`, and forward the wheel by hand from
|
||||
// that flag. pi scrolls on SGR wheel reports, one row each at a low rate, faster on a spin.
|
||||
// `tuiMode: regular`, the default below 1.0.0, 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 measured: alternate_on=1, mouse_any_flag=1, SGR) and the flag
|
||||
// covers it.
|
||||
// ⚠️ No `transcriptPageKeys`, though fullscreen pi does page on PageUp/PageDown. 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 a wheel over a fresh regular pane sent the next keystroke to the
|
||||
// wrong line (measured). The cost: a fullscreen pi with "Wheel scrolls local history" on
|
||||
// has no paging fallback.
|
||||
altScreen: 'strip-mux-and-mouse',
|
||||
wheelForward: { mode: 'always' },
|
||||
echo: { policy: 'buffer', anchor: { kind: 'cursor' } },
|
||||
// Measured on a live pi 1.1.0 pane (capture-pane every 250 ms through a turn,
|
||||
// 2026-10-09): pi has no composer glyph. Its composer sits between two `─` rules, and
|
||||
|
||||
@@ -546,7 +546,7 @@ export interface CliCapabilities {
|
||||
* `strip-full` is `isAltScreenStripMode`; `strip-mux-and-mouse` is `isMuxMouseStripMode`;
|
||||
* every other value takes `isMuxAltScreenOnlyStripMode`, so at runtime `preserve` and
|
||||
* `strip-mux-only` are the same row — `preserve` only says what such a CLI's pane
|
||||
* holds (terminal-owned scrollback: a shell, pi), not a different strip.
|
||||
* holds (terminal-owned scrollback: a shell), not a different strip.
|
||||
*
|
||||
* - alt-screen: the tmux CLIENT emits `smcup` as its first bytes at attach, parking
|
||||
* xterm in the scrollback-less alternate buffer; a pane program's own toggles never
|
||||
@@ -560,8 +560,8 @@ export interface CliCapabilities {
|
||||
* own mouse support must work in the pane (htop/vim in a shell).
|
||||
*
|
||||
* Stock CLIs: `strip-full` = claude, codex, gemini (Ink TUIs); `strip-mux-and-mouse` =
|
||||
* opencode and copilot (full-screen TUIs that enable tracking themselves); `strip-mux-only` =
|
||||
* antigravity, grok, deepseek, omp; `preserve` = shell, pi.
|
||||
* opencode, copilot and pi (full-screen TUIs that enable tracking themselves); `strip-mux-only` =
|
||||
* antigravity, grok, deepseek, omp; `preserve` = shell.
|
||||
*
|
||||
* A fourth combination is the point to split this into flags; three is still cheaper
|
||||
* as an enum.
|
||||
|
||||
+11
-13
@@ -304,23 +304,21 @@ function cliExportsTruecolor(mode: SessionMode): boolean {
|
||||
* Codex, Claude Code, and Gemini are known, controlled (Ink/React) TUIs that
|
||||
* repaint via cursor positioning, so dropping the alt-screen switch is safe —
|
||||
* content stays in the normal buffer. Excluded: `shell` (arbitrary programs like
|
||||
* vim/less/htop legitimately need the alt screen), `opencode` (its own MIDDLE strip,
|
||||
* isMuxMouseStripMode), `pi` (below) and `grok` (a fullscreen alt-screen TUI with
|
||||
* vim/less/htop legitimately need the alt screen), `opencode`, `copilot` and `pi` (their own
|
||||
* MIDDLE strip, isMuxMouseStripMode) and `grok` (a fullscreen alt-screen TUI with
|
||||
* mouse support). Keep parity with the replay-side strip (`stripReplayBuffer` in
|
||||
* session-routes.ts); the table in `CliCapabilities.altScreen` is pinned for every
|
||||
* stock CLI in test/claude-scrollback-strip.test.ts.
|
||||
*
|
||||
* ⚠️ Being excluded here does NOT preserve the alt screen. Every excluded mode
|
||||
* falls through to isMuxAltScreenOnlyStripMode(), which strips the alt-screen
|
||||
* toggles too whenever the session is tmux-backed, and pi/opencode ALWAYS are
|
||||
* (both refuse the direct-PTY fallback). What exclusion actually buys is the rest
|
||||
* of the full strip: `\x1b[3J` and the mouse-tracking DECSETs survive. That is the
|
||||
* real reason pi is out: its default TUI renders into the MAIN screen with
|
||||
* terminal-owned scrollback and is mouse-aware, so it is a `3J`/mouse consumer in
|
||||
* a way an Ink TUI repainting in place is not. Consequence to know before
|
||||
* debugging it: pi's runtime-switchable fullscreen TUI (`/settings`, 0.84.0+)
|
||||
* still gets its `?1049h` stripped and paints into the main buffer, exactly like
|
||||
* vim inside a tmux `shell` session.
|
||||
* falls through to isMuxMouseStripMode() or isMuxAltScreenOnlyStripMode(), which
|
||||
* strip the alt-screen toggles too whenever the session is tmux-backed, and
|
||||
* pi/opencode ALWAYS are (both refuse the direct-PTY fallback). What exclusion
|
||||
* actually buys is the rest of the full strip: `\x1b[3J` survives, and for the
|
||||
* narrow strip the mouse-tracking DECSETs too. pi (fullscreen by default since
|
||||
* 1.0.0) is a mouse-aware TUI like opencode and copilot, so it takes the mouse
|
||||
* strip (isMuxMouseStripMode): a drag selects locally and `cliMouseTracking`
|
||||
* keeps the wheel working across a buffer replay.
|
||||
*/
|
||||
export function isAltScreenStripMode(mode: SessionMode): boolean {
|
||||
return getCli(mode)?.capabilities.altScreen === 'strip-full';
|
||||
@@ -3003,7 +3001,7 @@ export class Session extends EventEmitter {
|
||||
// redraws overwrite only the cells they target, so non-erased rows keep
|
||||
// their content. Gated to Codex/Claude/Gemini (isAltScreenStripMode).
|
||||
//
|
||||
// Every OTHER mode (shell/antigravity/pi/grok/deepseek/omp) gets the NARROW strip
|
||||
// Every OTHER mode (shell/antigravity/grok/deepseek/omp) gets the NARROW strip
|
||||
// when it is tmux-backed: alt-screen toggles only, because the sequence that breaks
|
||||
// scrollback there is tmux's own client-side smcup at attach, not anything the
|
||||
// program in the pane emitted (issue #205, see isMuxAltScreenOnlyStripMode).
|
||||
|
||||
@@ -956,8 +956,8 @@ export interface SessionState {
|
||||
/**
|
||||
* True while the CLI in the pane has a mouse-tracking DECSET on, as observed
|
||||
* by the server on its way out of the stream (those sequences are stripped for
|
||||
* the strip-full and strip-mux-and-mouse modes, claude/codex/gemini and opencode
|
||||
* under tmux, so the browser can never see them itself). The browser
|
||||
* the strip-full and strip-mux-and-mouse modes under tmux, so the browser can
|
||||
* never see them itself). The browser
|
||||
* hand-encodes a click report ONLY when this is true; without it, every click
|
||||
* sent mouse reports to a CLI that never asked for them.
|
||||
*/
|
||||
|
||||
@@ -1149,8 +1149,8 @@
|
||||
|
||||
// A plain left-click reported to the CLI, the primary pane's desktop click
|
||||
// (terminal-ui.js _handleDesktopTerminalClick) aimed at this pane. The
|
||||
// server strips the mouse DECSETs of some modes (opencode's since #555, so a
|
||||
// drag selects text), which leaves this xterm's own mouse encoder idle for
|
||||
// server strips the mouse DECSETs of some modes (so a drag selects text),
|
||||
// which leaves this xterm's own mouse encoder idle for
|
||||
// them; without this a click in such a pane never reached the CLI. Only
|
||||
// while this pane's session has tracking on (cliMouseTracking), through the
|
||||
// same skips as the primary pane. Bubble phase, as there. The target is
|
||||
|
||||
@@ -1572,8 +1572,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
// A real mouse click normally reaches the PTY through xterm's own mouse
|
||||
// encoder, but that encoder only runs while mouseTrackingMode is ON — and
|
||||
// the server strips the enabling DECSETs from claude/codex/gemini output
|
||||
// (isAltScreenStripMode, session.ts) and from opencode's (isMuxMouseStripMode,
|
||||
// so a drag selects text) so the wheel keeps scrolling
|
||||
// (isAltScreenStripMode, session.ts) and from the mouse-strip modes'
|
||||
// (isMuxMouseStripMode, so a drag selects text) so the wheel keeps scrolling
|
||||
// scrollback. Desktop clicks therefore stopped reporting entirely (the
|
||||
// same breakage the mobile touchend tap branch above works around).
|
||||
// Hand-encode the SGR report for plain left-clicks on those sessions.
|
||||
@@ -5942,8 +5942,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
* literal text (`[<0;88;20M`) and they garble the next line typed.
|
||||
*
|
||||
* Fails toward silence: an unknown or stale flag reports nothing rather than
|
||||
* injecting bytes. After a server restart the flag is false until the CLI
|
||||
* re-emits its DECSET, which closing and reopening a dialog does.
|
||||
* injecting bytes. After a server restart the flag is false until the DECSET
|
||||
* passes again, which tmux does itself when the server re-attaches to the pane.
|
||||
*
|
||||
* `sessionId` defaults to the primary pane's session; a TerminalTile passes
|
||||
* its own (through _handleDesktopTerminalClick's target), never the active one.
|
||||
@@ -6059,18 +6059,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
// option, but it would override an explicit user choice; instead the caller
|
||||
// falls through to _maybePageCliTranscript, so the gesture still pages the
|
||||
// CLI's transcript (or loads unloaded tmux scrollback, see there) and the
|
||||
// setting keeps meaning exactly what it says.
|
||||
// setting keeps meaning exactly what it says. Pi is the accepted exception:
|
||||
// it declares no `transcriptPageKeys`, so with this on its wheel stays dead.
|
||||
if (this.loadAppSettingsFromStorage?.()?.terminalWheelLocalScrollback) return false;
|
||||
const mode = (target.terminal || this.terminal)?.modes?.mouseTrackingMode;
|
||||
if (mode && mode !== 'none') return false;
|
||||
const session = this.sessions?.get(target.sessionId || this.activeSessionId);
|
||||
const sessionMode = session?.mode || 'claude';
|
||||
// GitHub Copilot CLI scrolls its own transcript on SGR wheel reports (checked with xterm
|
||||
// encoding them natively). Its mouse DECSETs are stripped server-side so a drag selects
|
||||
// locally (strip-mux-and-mouse), which also means xterm no longer encodes the wheel: send the
|
||||
// same reports by hand, but only while the CLI is actually tracking the mouse. Not gated on a
|
||||
// version; a stale-false flag falls through to the PageUp/PageDown fallback like claude's.
|
||||
if (sessionMode === 'copilot') return session?.cliMouseTracking === true;
|
||||
// GitHub Copilot CLI (and pi, fullscreen by default) scrolls its own transcript on SGR wheel
|
||||
// reports (checked with xterm encoding them natively). Their mouse DECSETs are stripped
|
||||
// server-side so a drag selects locally (strip-mux-and-mouse), which also means xterm no longer
|
||||
// encodes the wheel: send the same reports by hand, but only while the CLI is actually tracking
|
||||
// the mouse. Not gated on a version. On Copilot a stale-false flag falls through to the
|
||||
// PageUp/PageDown fallback like claude's; pi has none on purpose (the `transcriptPageKeys` note
|
||||
// in stock.ts), so there the wheel does nothing until the flag is back.
|
||||
if (sessionMode === 'copilot' || sessionMode === 'pi') return session?.cliMouseTracking === true;
|
||||
if (sessionMode !== 'claude' && sessionMode !== 'codex') return false;
|
||||
if (sessionMode === 'claude' && !this._cliVersionAtLeast(session?.cliVersion, '2.1.187')) return false;
|
||||
// Only while Claude or Codex is actually listening for the mouse. Claude's
|
||||
@@ -6408,8 +6411,8 @@ Object.assign(CodemanApp.prototype, {
|
||||
* The reason is that the habit and xterm's Shift mean different things once
|
||||
* the DECSETs are stripped. xterm reads Shift as "force selection" ONLY while
|
||||
* the app actually has mouse tracking on; the server strips those DECSETs for
|
||||
* claude/codex/gemini (isAltScreenStripMode) and opencode (isMuxMouseStripMode),
|
||||
* so xterm's mouseTrackingMode is
|
||||
* claude/codex/gemini (isAltScreenStripMode) and the mouse-strip modes
|
||||
* (isMuxMouseStripMode), so xterm's mouseTrackingMode is
|
||||
* permanently `none`, that branch is unreachable, and Shift instead falls into
|
||||
* `_onIncrementalClick` — EXTEND an existing selection. Extending is a no-op
|
||||
* when `selectionStart` is null, so the drag never anchors and no selection is
|
||||
|
||||
@@ -332,7 +332,7 @@ const MOUSE_TRACKING_PATTERN = /\x1b\[\?(?:1000|1001|1002|1003|1005|1006|1007)[h
|
||||
* - `strip-full` (claude/codex/gemini): alt-screen toggles, `3J` and mouse DECSETs.
|
||||
* xterm obeys the toggles by switching to its scrollback-less alt buffer and wiping
|
||||
* saved lines, so history disappeared on tab switch.
|
||||
* - `strip-mux-and-mouse` (opencode, tmux-backed): alt-screen toggles and mouse DECSETs.
|
||||
* - `strip-mux-and-mouse` (tmux-backed): alt-screen toggles and mouse DECSETs.
|
||||
* A replayed tracking enable parks xterm in report mode, where a drag goes to the CLI
|
||||
* instead of selecting text (and Ctrl+C without a selection is opencode's app_exit).
|
||||
* `3J` stays: a TUI is not a `clear` consumer.
|
||||
|
||||
Reference in new issue
Block a user