@zhuxixi/pi-agent-board 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/docs/superpowers/plans/2026-10-06-attach-settle-scheme-delivery.md +751 -0
- package/docs/superpowers/plans/2026-10-09-issue-167-attach-middle-paste.md +509 -0
- package/docs/superpowers/plans/2026-10-09-issue-169-fullscreen-mouse-ownership.md +346 -0
- package/docs/superpowers/specs/2026-10-06-attach-settle-scheme-delivery-design.md +196 -0
- package/docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md +125 -0
- package/docs/superpowers/specs/2026-10-09-issue-169-fullscreen-mouse-ownership-design.md +130 -0
- package/package.json +3 -3
- package/src/commands/agent-board.ts +11 -0
- package/src/commands/attach-flow.ts +11 -0
- package/src/core/pty-scroll.mjs +25 -0
- package/src/core/terminal-query-sequences.mjs +81 -0
- package/src/ui/pty-attach.ts +122 -41
package/src/ui/pty-attach.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
5
5
|
import { createConnection, type Socket } from "node:net";
|
|
6
|
-
import type { Component, KeybindingsManager, RgbColor, TUI, TerminalColorScheme } from "@earendil-works/pi-tui";
|
|
6
|
+
import type { Component, KeybindingsManager, RgbColor, TUI, TerminalColorScheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
7
7
|
import { CURSOR_MARKER, Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
8
|
import { findHttpUrlAtCells, findWordRangeAtCells } from "../core/pty-links.mjs";
|
|
9
9
|
import { createAttachOutputRenderScheduler, detectCursorDesync, isPtyCursorHidden, nextAttachRender, projectPtyCursor, shouldScheduleAttachRenderForMessage } from "../core/pty-attach-render.mjs";
|
|
@@ -11,8 +11,8 @@ import { evaluateAttachReconnect, shouldEscapeAttach } from "../core/pty-attach-
|
|
|
11
11
|
import { installImeCursorCoalesce } from "../core/ime-cursor-coalesce.mjs";
|
|
12
12
|
import { createJiggleRetryController } from "../core/pty-attach-jiggle-controller.mjs";
|
|
13
13
|
import { createTerminalAttachClient } from "../core/terminal-attach-client.mjs";
|
|
14
|
-
import { colorSchemeForBackgroundRgb, extractOscQuerySequences, toColorSchemeReport } from "../core/terminal-query-sequences.mjs";
|
|
15
|
-
import { clampInt, parseMouseInputChunk, resolveWheelLines, scrollViewportTop, selectionDragScrollLines } from "../core/pty-scroll.mjs";
|
|
14
|
+
import { backgroundRgbFromTerminalColors, buildSettleSchemePatch, colorSchemeForBackgroundRgb, extractOscQuerySequences, resolveProbeApi, toColorSchemeReport } from "../core/terminal-query-sequences.mjs";
|
|
15
|
+
import { clampInt, parseMouseInputChunk, resolveAttachMouseAction, resolveWheelLines, scrollViewportTop, selectionDragScrollLines, shouldOwnOuterMouseMode } from "../core/pty-scroll.mjs";
|
|
16
16
|
|
|
17
17
|
export type PtyAttachResult = { action: "detached" } | { action: "closed"; exitCode?: number | null };
|
|
18
18
|
|
|
@@ -21,10 +21,21 @@ type ThemeLike = {
|
|
|
21
21
|
bold(text: string): string;
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
+
/** Diagnostic patch emitted at attach settle (DiagnosticEvent-compatible; built by buildSettleSchemePatch). */
|
|
25
|
+
export interface AttachSettleDiagnosticPatch {
|
|
26
|
+
source: string;
|
|
27
|
+
level: "info" | "warn";
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
details: { probeApi?: string; outcome: string; report?: string; late?: boolean };
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
export interface PtyAttachOptions {
|
|
25
34
|
socketPath: string;
|
|
26
35
|
screenLogPath?: string;
|
|
27
36
|
title: string;
|
|
37
|
+
/** Issue #161: receives settle-scheme diagnostic patches. Default: no-op. */
|
|
38
|
+
onDiagnostic?: (event: AttachSettleDiagnosticPatch) => void;
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
const require = createRequire(import.meta.url);
|
|
@@ -225,6 +236,10 @@ export class PtyAttachComponent implements Component {
|
|
|
225
236
|
private attaching = true;
|
|
226
237
|
private attachSettleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
227
238
|
private attachHardTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
239
|
+
// Last scheme delivered to the child at settle (issue #161). Starts null:
|
|
240
|
+
// null never matches the duplicate check, so a timeout-then-late sequence
|
|
241
|
+
// still delivers. Single-shot per attach — settle runs at most once.
|
|
242
|
+
private sentScheme: TerminalColorScheme | null = null;
|
|
228
243
|
// Issue #128 D2: unsubscriber for the color-scheme bridge registered at
|
|
229
244
|
// attach settle; nulled on every terminal path so the TUI never keeps a
|
|
230
245
|
// listener pointing at a dead attach surface.
|
|
@@ -353,6 +368,22 @@ export class PtyAttachComponent implements Component {
|
|
|
353
368
|
this.send({ type: "input", data });
|
|
354
369
|
}
|
|
355
370
|
|
|
371
|
+
/** Issue #167: fullscreen TUI (pi ≥1.0) consumes raw SGR mouse sequences in
|
|
372
|
+
* TuiAltScreen and delivers normalized events to overlays here instead — the
|
|
373
|
+
* legacy handleInput mouse path never runs in that mode. Only the
|
|
374
|
+
* middle-click paste is ours; every other event returns undefined so pi's
|
|
375
|
+
* own fullscreen selection and the wheel defer path keep working. */
|
|
376
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
377
|
+
const action = resolveAttachMouseAction(event, {
|
|
378
|
+
nativePasteEnabled: process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE !== "0",
|
|
379
|
+
});
|
|
380
|
+
if (action !== "paste-primary") return undefined;
|
|
381
|
+
this.clearPendingClick();
|
|
382
|
+
this.clearSelection();
|
|
383
|
+
this.pastePrimarySelection();
|
|
384
|
+
return { handled: true, render: false };
|
|
385
|
+
}
|
|
386
|
+
|
|
356
387
|
render(width: number): string[] {
|
|
357
388
|
this.resizeIfNeeded(width);
|
|
358
389
|
const height = this.tui.terminal?.rows ?? 24;
|
|
@@ -598,7 +629,10 @@ export class PtyAttachComponent implements Component {
|
|
|
598
629
|
// Issue #128: the settle transition is the single point where `attaching`
|
|
599
630
|
// flips false (never re-armed), so these hooks run at most once per attach.
|
|
600
631
|
this.attachColorSchemeBridge();
|
|
601
|
-
|
|
632
|
+
// Use only the tracked TUI probe. An extra terminal.write(OSC 11) has
|
|
633
|
+
// no local query owner, so its reply falls through handleInput into the
|
|
634
|
+
// child PTY. Settle can precede the child's raw-mode startup, making
|
|
635
|
+
// the line discipline echo that reply as visible ^[]11;rgb:... text.
|
|
602
636
|
this.reportRealTerminalColorScheme();
|
|
603
637
|
}
|
|
604
638
|
|
|
@@ -634,52 +668,89 @@ export class PtyAttachComponent implements Component {
|
|
|
634
668
|
}
|
|
635
669
|
}
|
|
636
670
|
|
|
637
|
-
/** Issue #
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
private replayBackgroundQuery(): void {
|
|
643
|
-
if (process.env.AGENT_BOARD_FORWARD_TERMINAL_QUERIES === "0") return;
|
|
644
|
-
try {
|
|
645
|
-
this.tui.terminal.write("\x1b]11;?\x07");
|
|
646
|
-
} catch {
|
|
647
|
-
/* best-effort: enhancement, never critical */
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/** Issue #148: #128's replay only helps while the child still has a PENDING
|
|
652
|
-
* OSC 11 query. That precondition does not hold here: the child's probe fires
|
|
653
|
-
* at spawn (long before attach), and Pi's leaked pending state — a timeout
|
|
654
|
-
* settles the query but leaves it queued and keeps the reply counter up —
|
|
655
|
-
* makes the next arriving reply get consumed by that stale entry and
|
|
656
|
-
* discarded, resolving nothing. So the client stops relying on the child's
|
|
657
|
-
* probe entirely: it asks the REAL terminal itself (pi-tui's public query API)
|
|
658
|
-
* and hands the child a 997 color-scheme report, which Pi consumes
|
|
659
|
-
* unconditionally (no pending-query precondition — see
|
|
660
|
-
* consumeTerminalColorSchemeReport). Same kill switch as #128 D1-D3.
|
|
661
|
-
*
|
|
662
|
-
* One probe only (CR round-1 advisory): the local pi-tui has the same leak, so
|
|
663
|
-
* when a stale entry sits in ITS queue the reply is swallowed before this
|
|
664
|
-
* promise can see it, and retrying would append another stale entry per
|
|
665
|
-
* attempt — failing identically while poisoning the host's own detection. The
|
|
666
|
-
* degraded case stays the documented pre-#148 silence. */
|
|
671
|
+
/** Issue #148/#161: at settle, ask the REAL terminal itself (feature-detected
|
|
672
|
+
* public query API — pi-tui >= 0.99 renamed it to queryTerminalColors) and
|
|
673
|
+
* hand the child an unconditional 997 color-scheme report. One probe per
|
|
674
|
+
* attach; the pre-#148 silence on failure is now observable via
|
|
675
|
+
* onDiagnostic. Same kill switch as #128 D1-D3. */
|
|
667
676
|
private reportRealTerminalColorScheme(): void {
|
|
668
|
-
if (process.env.AGENT_BOARD_FORWARD_TERMINAL_QUERIES === "0")
|
|
677
|
+
if (process.env.AGENT_BOARD_FORWARD_TERMINAL_QUERIES === "0") {
|
|
678
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ outcome: "suppressed" }));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
669
681
|
void this.probeAndReportRealTerminalColorScheme();
|
|
670
682
|
}
|
|
671
683
|
|
|
672
684
|
private async probeAndReportRealTerminalColorScheme(): Promise<void> {
|
|
673
|
-
|
|
685
|
+
const probe = resolveProbeApi(this.tui as never);
|
|
686
|
+
if (probe.api === "none") {
|
|
687
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: "none", outcome: "no_probe_api" }));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
let result: { rgb?: RgbColor; anyColors: boolean };
|
|
674
691
|
try {
|
|
675
|
-
|
|
692
|
+
result = await probe.invoke(
|
|
693
|
+
this.tui as never,
|
|
694
|
+
REAL_TERMINAL_SCHEME_PROBE_TIMEOUT_MS,
|
|
695
|
+
(colors) => this.handleLateSettleColors(probe.api, colors),
|
|
696
|
+
);
|
|
676
697
|
} catch {
|
|
677
|
-
/* best-effort: a failed probe keeps the pre-#148 behavior */
|
|
698
|
+
/* best-effort: a failed probe keeps the pre-#148 behavior, but recorded */
|
|
699
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: probe.api, outcome: "error" }));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
const rgb = result?.rgb;
|
|
703
|
+
if (!rgb) {
|
|
704
|
+
// Outcome rule (spec §2.3): any color answered but no background =>
|
|
705
|
+
// no_background; nothing answered at all => timeout. The old API only
|
|
706
|
+
// queries OSC 11, so its undefined is always a timeout.
|
|
707
|
+
const outcome = result && result.anyColors ? "no_background" : "timeout";
|
|
708
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: probe.api, outcome }));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
this.deliverSettleScheme(colorSchemeForBackgroundRgb(rgb) ?? undefined, probe.api, false);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** pi-tui >= 0.99 late replies: deliver only when the scheme differs from
|
|
715
|
+
* what was already sent (sentScheme starts null and never matches, so the
|
|
716
|
+
* timeout-then-late path delivers). Closed => dropped, but recorded. */
|
|
717
|
+
private handleLateSettleColors(api: "colors" | "background" | "none", colors: unknown): void {
|
|
718
|
+
if (this.closed) {
|
|
719
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: api, outcome: "dropped_closed" }));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const rgb = backgroundRgbFromTerminalColors(colors as never);
|
|
723
|
+
const scheme = rgb ? colorSchemeForBackgroundRgb(rgb) ?? undefined : undefined;
|
|
724
|
+
if (!scheme) return;
|
|
725
|
+
if (this.sentScheme === scheme) {
|
|
726
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: api, outcome: "duplicate_skipped" }));
|
|
727
|
+
return;
|
|
678
728
|
}
|
|
679
|
-
|
|
729
|
+
this.deliverSettleScheme(scheme, api, true);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private deliverSettleScheme(scheme: TerminalColorScheme | undefined, api: "colors" | "background" | "none", late: boolean): void {
|
|
680
733
|
if (!scheme || this.closed) return;
|
|
681
734
|
const data = toColorSchemeReport(scheme);
|
|
682
|
-
if (data)
|
|
735
|
+
if (!data) return;
|
|
736
|
+
// CR round-1 advisory: send() silently no-ops while the control socket is
|
|
737
|
+
// disconnected (hard-timeout settle before the host finished connecting).
|
|
738
|
+
// Claiming "reported" there would be a lie the dedup then acts on, so
|
|
739
|
+
// record the drop and leave sentScheme unset — a later same-scheme late
|
|
740
|
+
// reply re-attempts instead of being swallowed as duplicate_skipped.
|
|
741
|
+
if (!this.connected) {
|
|
742
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch({ probeApi: api, outcome: "dropped_disconnected" }));
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
this.sentScheme = scheme;
|
|
746
|
+
this.send({ type: "input", data });
|
|
747
|
+
const details: { probeApi: "colors" | "background" | "none"; outcome: "reported"; report: string; late?: boolean } = {
|
|
748
|
+
probeApi: api,
|
|
749
|
+
outcome: "reported",
|
|
750
|
+
report: scheme === "light" ? "997;2" : "997;1",
|
|
751
|
+
};
|
|
752
|
+
if (late) details.late = true;
|
|
753
|
+
this.opts.onDiagnostic?.(buildSettleSchemePatch(details));
|
|
683
754
|
}
|
|
684
755
|
|
|
685
756
|
private startDesyncProbe(): void {
|
|
@@ -779,7 +850,7 @@ export class PtyAttachComponent implements Component {
|
|
|
779
850
|
}
|
|
780
851
|
|
|
781
852
|
private enableMouseScroll(): void {
|
|
782
|
-
if (!this.
|
|
853
|
+
if (!this.ownsOuterMouseMode()) return;
|
|
783
854
|
try {
|
|
784
855
|
this.tui.terminal.write(XTSHIFTESCAPE_SELECT);
|
|
785
856
|
this.tui.terminal.write(MOUSE_ENABLE);
|
|
@@ -788,6 +859,15 @@ export class PtyAttachComponent implements Component {
|
|
|
788
859
|
}
|
|
789
860
|
}
|
|
790
861
|
|
|
862
|
+
/** Issue #169: in fullscreen mode TuiAltScreen owns outer-terminal mouse
|
|
863
|
+
* reporting (asserted once at startup, never re-asserted) — writing any
|
|
864
|
+
* mouse-mode sequence from here can only downgrade or kill it. tui.mode is
|
|
865
|
+
* typed non-optional but old pi runtimes lack it; the strict inequality
|
|
866
|
+
* then keeps today's (regular) behavior. */
|
|
867
|
+
private ownsOuterMouseMode(): boolean {
|
|
868
|
+
return shouldOwnOuterMouseMode(this.tui.mode, this.mouseScrollEnabled());
|
|
869
|
+
}
|
|
870
|
+
|
|
791
871
|
private mouseScrollEnabled(): boolean {
|
|
792
872
|
const mode = (process.env.AGENT_BOARD_ATTACH_MOUSE ?? "").trim().toLowerCase();
|
|
793
873
|
if (mode === "0" || mode === "off" || mode === "false") return false;
|
|
@@ -809,6 +889,7 @@ export class PtyAttachComponent implements Component {
|
|
|
809
889
|
}
|
|
810
890
|
|
|
811
891
|
private disableMouseScroll(): void {
|
|
892
|
+
if (!this.ownsOuterMouseMode()) return;
|
|
812
893
|
try {
|
|
813
894
|
this.tui.terminal.write(MOUSE_DISABLE);
|
|
814
895
|
} catch {
|