@zhuxixi/pi-agent-board 0.10.0 → 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 +19 -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 +125 -41
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Spec: #167 — middle-click paste in attach views under pi ≥1.0 fullscreen TUI
|
|
2
|
+
|
|
3
|
+
Status: DRAFT (awaiting user approval — github-issue-driven step 4 gate)
|
|
4
|
+
Issue: https://github.com/zhuxixi/pi-agent-board/issues/167
|
|
5
|
+
Date: 2026-10-09
|
|
6
|
+
|
|
7
|
+
## Problem (root cause, verified)
|
|
8
|
+
|
|
9
|
+
pi ≥1.0 defaults to fullscreen TUI. `TuiAltScreen` parses and consumes all SGR mouse
|
|
10
|
+
sequences itself; overlays receive mouse only through the normalized `handleMouse(event)`
|
|
11
|
+
API. `PtyAttachComponent` implements only the legacy `handleInput(data)` path, so its
|
|
12
|
+
middle-click paste (`pastePrimarySelection()`, commit a1c5f08) never runs in fullscreen.
|
|
13
|
+
Regular mode (`pi --tui-mode regular`) is unaffected. Evidence: pi-tui 1.1.0
|
|
14
|
+
`tui-alt-screen.js:520-526`, `tui.js:507-532`; research notes in
|
|
15
|
+
`~/.claude/github-issue-driven/zhuxixi/pi-agent-board/issue-167/research/`.
|
|
16
|
+
|
|
17
|
+
## Goal / non-goals
|
|
18
|
+
|
|
19
|
+
**Goal**: middle-click inside the attach view pastes the X11 PRIMARY selection into the
|
|
20
|
+
hosted session in fullscreen mode, with the same semantics and escape hatch as regular
|
|
21
|
+
mode; regular mode behavior unchanged.
|
|
22
|
+
|
|
23
|
+
**Non-goals** (explicitly deferred):
|
|
24
|
+
- X11 PRIMARY *mirroring* of selections made in fullscreen (pi's own fullscreen selection
|
|
25
|
+
writes CLIPBOARD, not PRIMARY). Taking over left-button events would displace pi's
|
|
26
|
+
fullscreen selection — needs its own design (follow-up).
|
|
27
|
+
- #169 (attach teardown disabling outer-terminal mouse reporting) — separate issue/PR.
|
|
28
|
+
- Any wheel behavior change (today's defer-to-overlay fallback keeps working; we return
|
|
29
|
+
`undefined` for wheel).
|
|
30
|
+
|
|
31
|
+
## Design
|
|
32
|
+
|
|
33
|
+
### 1. Pure decision helper (new, `src/core/pty-scroll.mjs` — same module that owns
|
|
34
|
+
`parseMouseInputChunk`)
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// Returns the attach-surface action for a normalized pi-tui mouse event.
|
|
38
|
+
// "paste-primary": read X11 PRIMARY and forward as input (middle-click paste).
|
|
39
|
+
// null: not ours — let the event fall through untouched (today's behavior).
|
|
40
|
+
resolveAttachMouseAction(event, { nativePasteEnabled }) → "paste-primary" | null
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Decision table (`event.type`, `event.button`, `nativePasteEnabled`):
|
|
44
|
+
|
|
45
|
+
| type | button | nativePasteEnabled | result |
|
|
46
|
+
| --- | --- | --- | --- |
|
|
47
|
+
| press | middle | true | `"paste-primary"` |
|
|
48
|
+
| press | middle | false | `null` (kill switch = today's silent drop) |
|
|
49
|
+
| press/release/click | left/right/none | * | `null` (pi fullscreen selection & default behavior untouched) |
|
|
50
|
+
| wheel | any | * | `null` (existing defer-to-`handleInput` path preserved) |
|
|
51
|
+
| move/drag | any | * | `null` |
|
|
52
|
+
|
|
53
|
+
`nativePasteEnabled` is computed once per call site from
|
|
54
|
+
`process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE !== "0"` (same rule as `pastePrimarySelection`).
|
|
55
|
+
|
|
56
|
+
### 2. Component wiring (`src/ui/pty-attach.ts`)
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
60
|
+
const action = resolveAttachMouseAction(event, {
|
|
61
|
+
nativePasteEnabled: process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE !== "0",
|
|
62
|
+
});
|
|
63
|
+
if (action === "paste-primary") {
|
|
64
|
+
this.clearPendingClick();
|
|
65
|
+
this.clearSelection();
|
|
66
|
+
this.pastePrimarySelection();
|
|
67
|
+
return { handled: true, render: false };
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `{ handled: true }` stops propagation and suppresses alt-screen fallback for that
|
|
74
|
+
press; `render: false` because a successful paste repaints via child output (async)
|
|
75
|
+
and a failed one (no xclip) needs no repaint.
|
|
76
|
+
- Legacy `handleInput` raw path stays exactly as is — regular mode and wheel keep working.
|
|
77
|
+
- No changes to outer-terminal mouse-mode writes (`MOUSE_ENABLE`/`MOUSE_DISABLE`) — #169.
|
|
78
|
+
|
|
79
|
+
### 3. devDependencies bump (enabling task)
|
|
80
|
+
|
|
81
|
+
Bump `@earendil-works/pi-tui` and `@earendil-works/pi-coding-agent` devDeps 0.79.8 → 1.1.0
|
|
82
|
+
(match the runtime the extension actually runs under). Gate: `npm run typecheck` +
|
|
83
|
+
`npm run verify` green. If unrelated type breakage from 0.79.8→1.1.0 turns out to be large,
|
|
84
|
+
fallback: keep 0.79.8 and declare local structural `TuiMouseEvent`/result types in the
|
|
85
|
+
component — decision point recorded in the plan; the bump is the preferred path.
|
|
86
|
+
|
|
87
|
+
## Acceptance matrix
|
|
88
|
+
|
|
89
|
+
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
90
|
+
|----|--------|----------|----------|----------|
|
|
91
|
+
| A1 | 决策表:哪些归一化鼠标事件触发中键粘贴 | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs`(新增用例直接调 `resolveAttachMouseAction`,覆盖决策表全部行 + `AGENT_BOARD_ATTACH_NATIVE_PASTE=0`) | 新增用例全绿,且现有点击/滚轮用例不回归 |
|
|
92
|
+
| A2 | 组件接线:`PtyAttachComponent.handleMouse` 中键 press → 读 PRIMARY → 作为 input 发往 attach socket;kill switch 时返回 undefined | 自动化验证(integration,组件级场景) | 新增 `test-support/pty-attach-mouse-scenario.ts`(仿 `pty-attach-protocol-scenario.ts`:fake tui + stub-`xclip` 目录 prepend PATH 打印 fixture 文本),由 `test/pty-attach-mouse.test.mjs` 以 `node --experimental-transform-types` 运行并断言 JSON 输出 | ① 中键 press 返回 `{handled:true}` 且 socket 收到 `{type:"input",data:fixture}`;② 同场景 legacy `handleInput` 注入 SGR 中键序列行为一致(regular 路径回归);③ `AGENT_BOARD_ATTACH_NATIVE_PASTE=0` 时 `handleMouse` 返回 undefined 且无 input 发出 |
|
|
93
|
+
| A3 | 类型与构建在 pi-tui 1.1.0 下成立 | 自动化验证(static/build) | `npm run typecheck`、`npm run verify`(含 pack:dry) | 全部通过,无 0.79.8→1.1.0 引入的未处理类型错误 |
|
|
94
|
+
| A4 | 全部既有行为不回归 | 自动化验证(unit+integration,既有套件) | `npm test` / `npm run test:coverage` | 既有测试全绿,覆盖阈值不降 |
|
|
95
|
+
| U1 | 真实终端里 fullscreen 中键粘贴生效 | 用户实测 | ① pi 1.1.0 fullscreen → `/agent-board` → attach;② 在别的 X11 应用选中一段文字;③ 在 attach 视图内中键 | 子 session 收到粘贴文本并有反应;同机 `pi --tui-mode regular` 行为不变;`AGENT_BOARD_ATTACH_NATIVE_PASTE=0` 后中键回到无反应 |
|
|
96
|
+
|
|
97
|
+
## Testability split design (hard constraint for implementation)
|
|
98
|
+
|
|
99
|
+
- **Pure core**: `resolveAttachMouseAction(event, {nativePasteEnabled})` — plain function,
|
|
100
|
+
no I/O, no env reads inside (env read at call site). Unit-tested decision table (A1).
|
|
101
|
+
Test boundary: `src/core/pty-scroll.mjs` exports; tests import only this.
|
|
102
|
+
- **I/O shell**: `PtyAttachComponent.handleMouse` maps action → `pastePrimarySelection()`
|
|
103
|
+
(existing xclip spawn) + result object. Tested at component level (A2) through public
|
|
104
|
+
seams only: `handleMouse`/`handleInput` in, fake tui terminal writes + attach-socket
|
|
105
|
+
messages out, `xclip` stubbed via PATH. No test-only constructor options.
|
|
106
|
+
- The pure function must not be re-coupled into the component (no inline decision logic).
|
|
107
|
+
|
|
108
|
+
## Risks / notes
|
|
109
|
+
|
|
110
|
+
- devDep bump may expose type breakage elsewhere (0.79.8 typings vs 1.1.0). Contained by
|
|
111
|
+
A3 as its own task; fallback documented above.
|
|
112
|
+
- `AGENT_BOARD_ATTACH_MOUSE=0` does not disable the new path (it governs outer-terminal
|
|
113
|
+
mode writes only); in fullscreen the alt screen owns reporting regardless. Documented
|
|
114
|
+
here as intended.
|
|
115
|
+
- Behavior when xclip is absent: unchanged silent no-op (existing contract), but the
|
|
116
|
+
event is still consumed (`handled:true`) — same as regular mode today.
|
|
117
|
+
|
|
118
|
+
## Deliverables
|
|
119
|
+
|
|
120
|
+
- `src/core/pty-scroll.mjs`: + `resolveAttachMouseAction`
|
|
121
|
+
- `src/ui/pty-attach.ts`: + `handleMouse`
|
|
122
|
+
- `package.json`: devDeps bump (pi-tui, pi-coding-agent → 1.1.0)
|
|
123
|
+
- `test/pty-scroll.test.mjs`: decision-table cases
|
|
124
|
+
- `test-support/pty-attach-mouse-scenario.ts` + `test/pty-attach-mouse.test.mjs`
|
|
125
|
+
- CHANGELOG entry via release tooling at release time (not in this PR)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Drafted: zai-coding-cn/glm-5.3 (selected) · zai-coding-cn/glm-5.3 (physical) @ 2026-10-09T12:49:41Z
|
|
2
|
+
|
|
3
|
+
# Spec: #169 — detaching from a PTY attach view kills outer-terminal mouse reporting for the rest of the fullscreen session
|
|
4
|
+
|
|
5
|
+
Status: DRAFT (awaiting spec-cross-review, then user approval — github-issue-driven step 4 gates)
|
|
6
|
+
Issue: https://github.com/zhuxixi/pi-agent-board/issues/169
|
|
7
|
+
Repo state baseline: main @ `fee345c` (post-#171)
|
|
8
|
+
|
|
9
|
+
## Problem (root cause, verified from source)
|
|
10
|
+
|
|
11
|
+
In fullscreen mode (pi ≥1.0 default) `TuiAltScreen` owns outer-terminal mouse reporting:
|
|
12
|
+
it writes the enable set `?1000h?1002h?1003h?1004h?1006h` exactly once in
|
|
13
|
+
`beforeTerminalStart` (pi-tui 1.1.0 `tui-alt-screen.js:194`) and disables it only at
|
|
14
|
+
`beforeTerminalStop` (:196-206); there is no re-assert path. `PtyAttachComponent` nonetheless
|
|
15
|
+
manages the outer terminal's mouse mode itself — ctor writes
|
|
16
|
+
`MOUSE_DISABLE`+`MOUSE_ENABLE` (downgrading `?1003h`/`?1004h`), and `close()` writes
|
|
17
|
+
`MOUSE_DISABLE` unconditionally (src/ui/pty-attach.ts:278-280, :1524 via
|
|
18
|
+
`disableMouseScroll` :882 — which, unlike `enableMouseScroll`, has no env guard). After
|
|
19
|
+
the first detach, pi-tui still believes mouse is on but the terminal stopped sending
|
|
20
|
+
events: fullscreen selection / copy-on-select / wheel / click regions are dead until pi
|
|
21
|
+
restarts. In regular mode the same writes are correct (board is the sole mouse owner).
|
|
22
|
+
|
|
23
|
+
Full evidence: `research/2026-10-09-fullscreen-mouse-ownership.md`,
|
|
24
|
+
`research/2026-10-09-repo-state-and-testability.md` in this directory.
|
|
25
|
+
|
|
26
|
+
## Goal / non-goals
|
|
27
|
+
|
|
28
|
+
**Goal**: in fullscreen mode the attach component performs **zero** outer-terminal
|
|
29
|
+
mouse-mode writes for its entire lifecycle (ctor, refresh timers, connect, resize,
|
|
30
|
+
close) — `TuiAltScreen` owns the mode and its full `?1003h/?1004h` set survives
|
|
31
|
+
attach AND detach. Regular-mode behavior (including the
|
|
32
|
+
disable→enable→refresh pairing and the kill-switch semantics) is unchanged. Old pi
|
|
33
|
+
runtimes without `tui.mode` keep today's (regular) behavior.
|
|
34
|
+
|
|
35
|
+
**Non-goals**:
|
|
36
|
+
- X11 PRIMARY mirroring of the fullscreen-native selection (deferred with #167).
|
|
37
|
+
- Any pi-tui change or new re-assert API (unnecessary).
|
|
38
|
+
- Wheel/scroll/selection behavior inside attach views (unchanged by design: deferred
|
|
39
|
+
wheel still reaches `handleInput`; clicks via `handleMouse` from #171).
|
|
40
|
+
- #170 (reply guard), #168 (session resume) — unrelated paths.
|
|
41
|
+
|
|
42
|
+
## Design
|
|
43
|
+
|
|
44
|
+
### 1. Pure decision helper (new, `src/core/pty-scroll.mjs` — mouse helpers live here)
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
/**
|
|
48
|
+
* Issue #169: whether the attach surface should manage the OUTER terminal's mouse
|
|
49
|
+
* mode. Only in regular TUI mode — in fullscreen, TuiAltScreen owns mouse reporting
|
|
50
|
+
* (asserted once at startup, never re-asserted), so any write from here can only
|
|
51
|
+
* downgrade or kill it. Old runtimes without tui.mode behave as regular.
|
|
52
|
+
* Pure: mode and the env-derived flag are resolved at the call site.
|
|
53
|
+
*/
|
|
54
|
+
shouldOwnOuterMouseMode(mode, mouseEnabled) → boolean // mode !== "fullscreen" && mouseEnabled
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 2. Component gating (`src/ui/pty-attach.ts`)
|
|
58
|
+
|
|
59
|
+
All outer-terminal mouse writes funnel through exactly two private methods —
|
|
60
|
+
`enableMouseScroll()` (:852) and `disableMouseScroll()` (:882). Gate both:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
private ownsOuterMouseMode(): boolean {
|
|
64
|
+
return shouldOwnOuterMouseMode(this.tui.mode, this.mouseScrollEnabled());
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- `enableMouseScroll()`: early-return `if (!this.ownsOuterMouseMode())` (replaces the
|
|
69
|
+
current `mouseScrollEnabled()` check; also gates the `XTSHIFTESCAPE_SELECT` write).
|
|
70
|
+
- `disableMouseScroll()`: early-return `if (!this.ownsOuterMouseMode())` (new guard —
|
|
71
|
+
fixes both the fullscreen teardown kill and the flagged unguarded disable under
|
|
72
|
+
`AGENT_BOARD_ATTACH_MOUSE=0`).
|
|
73
|
+
- `refreshMouseScrollMode()` keeps its `mouseScrollEnabled()` early-return (cheap) and
|
|
74
|
+
its timers call the now-gated `enableMouseScroll` — no other call-site changes
|
|
75
|
+
(ctor :278-280, connect :487, resize :1203, close :1524 all route through the two
|
|
76
|
+
choke points).
|
|
77
|
+
- Comment on `ownsOuterMouseMode()` noting `tui.mode` is typed non-optional but may be
|
|
78
|
+
absent on old runtimes — strict inequality handles it.
|
|
79
|
+
|
|
80
|
+
### Resulting behavior matrix
|
|
81
|
+
|
|
82
|
+
| tui.mode | AGENT_BOARD_ATTACH_MOUSE | ctor/connect/resize writes | close() write | fullscreen session after detach |
|
|
83
|
+
| --- | --- | --- | --- | --- |
|
|
84
|
+
| fullscreen | any | none | none | mouse alive (BUG FIXED) |
|
|
85
|
+
| regular / undefined (old runtime) | on (default) | today's pairing | MOUSE_DISABLE | n/a (unchanged) |
|
|
86
|
+
| regular / undefined | off | none | none | n/a (also fixes unguarded-disable defect) |
|
|
87
|
+
| fullscreen (host built with TuiAltScreenOptions.mouse: false) | n/a | none | none | consistent: host explicitly disabled mouse; nothing owns it — attach-view mouse features inert by host choice |
|
|
88
|
+
|
|
89
|
+
## Acceptance matrix
|
|
90
|
+
|
|
91
|
+
| ID | 功能点 | 验收方式 | 具体验证 | 通过标准 |
|
|
92
|
+
|----|--------|----------|----------|----------|
|
|
93
|
+
| A1 | 鼠标模式所有权决策表(mode × env 开关) | 自动化验证(unit) | `node --test test/pty-scroll.test.mjs`(新增用例直接调 `shouldOwnOuterMouseMode`,覆盖 6 格:fullscreen×on/off、regular×on/off、undefined×on/off) | 新增用例全绿,现有点击/滚轮/中键用例不回归 |
|
|
94
|
+
| A2 | fullscreen 全生命周期零外层鼠标写;regular 配对保留;kill switch 零写 | 自动化验证(integration,组件级场景) | 新增 `test-support/pty-attach-mouse-mode-scenario.ts`(fake tui 的 `terminal.write` 采集字节 + `mode` 可控 + 不可达 socketPath),由 `test/pty-attach-mouse-mode.test.mjs` 以 `node --experimental-transform-types` 运行断言 JSON 输出 | ① `mode:"fullscreen"`:ctor+250ms 刷新窗+close 全程 0 个 `?1000/?1002/?1003/?1004/?1006/XTSHIFTESCAPE` 写;② `mode:"regular"`:ctor 有 `MOUSE_ENABLE`、close 有 `MOUSE_DISABLE`;③ regular+`AGENT_BOARD_ATTACH_MOUSE=0`:全程 0 写(含 close);④ 无 `mode` 字段的 fake tui 行为同 regular |
|
|
95
|
+
| A3 | 类型与构建 | 自动化验证(static/build) | `npm run typecheck`、`npm run verify`(含 pack:dry) | 全部通过 |
|
|
96
|
+
| A4 | 既有行为不回归(含 #171 中键粘贴场景) | 自动化验证(unit+integration,既有套件) | `npm test` / `npm run test:coverage` | 既有测试全绿,覆盖阈值不降 |
|
|
97
|
+
| U1 | 真机:detach 后外层鼠标存活 | 用户实测 | ① pi ≥1.0 fullscreen → `/agent-board` → attach → detach(ctrl+\);② dashboard 里拖选文字 → 应高亮且 copy-on-select 生效;③ 滚轮/点击区域正常;④ attach 中 pi 自身全屏选中/复制正常(`?1003h/?1004h` 不再被降级);⑤ `pi --tui-mode regular` 下 attach→detach 行为与从前一致 | ①–⑤ 全部符合;修复前 ①③④ 在 detach 后失效 |
|
|
98
|
+
|
|
99
|
+
## Testability split design (hard constraint for implementation)
|
|
100
|
+
|
|
101
|
+
- **Pure core**: `shouldOwnOuterMouseMode(mode, mouseEnabled)` — plain function, no env
|
|
102
|
+
reads inside (both args resolved at the call site). Unit-tested decision table (A1).
|
|
103
|
+
Test boundary: `src/core/pty-scroll.mjs` exports; tests import only this.
|
|
104
|
+
- **I/O shell**: `PtyAttachComponent.ownsOuterMouseMode()` reads `this.tui.mode` +
|
|
105
|
+
`this.mouseScrollEnabled()` and delegates to the pure helper; the two write methods
|
|
106
|
+
keep ALL terminal writes. Component-level tests (A2) drive only public seams
|
|
107
|
+
(constructor, time, `dispose()`) and observe `terminal.write` bytes of a fake tui —
|
|
108
|
+
no test-only constructor options, no reading private state.
|
|
109
|
+
- The pure helper must not be re-inlined into the component; the two choke-point methods
|
|
110
|
+
must remain the only places that write mouse-mode/XTSHIFTESCAPE sequences.
|
|
111
|
+
|
|
112
|
+
## Risks / notes
|
|
113
|
+
|
|
114
|
+
- A2 uses an unreachable socketPath to keep the scenario runner-free; if the ctor's
|
|
115
|
+
connect-error path proves non-deterministic for the write-spy assertions, fall back to
|
|
116
|
+
the #171-style real-runner scenario asserting the same facts around a real
|
|
117
|
+
attach/detach (decision at plan time; record whichever is used).
|
|
118
|
+
- Existing scenarios' fake tuis have no `mode` field → they stay on the regular path and
|
|
119
|
+
don't assert terminal bytes → unaffected by the gating.
|
|
120
|
+
- `AGENT_BOARD_ATTACH_MOUSE` semantics in fullscreen become moot (no writes either way);
|
|
121
|
+
document in the behavior matrix rather than redefining the switch.
|
|
122
|
+
|
|
123
|
+
## Deliverables
|
|
124
|
+
|
|
125
|
+
- `src/core/pty-scroll.mjs`: + `shouldOwnOuterMouseMode`
|
|
126
|
+
- `src/ui/pty-attach.ts`: + `ownsOuterMouseMode()` private method; guards in
|
|
127
|
+
`enableMouseScroll` / `disableMouseScroll`
|
|
128
|
+
- `test/pty-scroll.test.mjs`: decision-table cases
|
|
129
|
+
- `test-support/pty-attach-mouse-mode-scenario.ts` + `test/pty-attach-mouse-mode.test.mjs`
|
|
130
|
+
- CHANGELOG entry via release tooling at release time (not in this PR)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhuxixi/pi-agent-board",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Agent-board dashboard for Pi: dispatch, monitor, peek/reply, and attach to background Pi sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@earendil-works/pi-coding-agent": "
|
|
77
|
-
"@earendil-works/pi-tui": "
|
|
76
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
77
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
78
78
|
"@types/node": "^25.9.1",
|
|
79
79
|
"c8": "^12.0.0",
|
|
80
80
|
"typescript": "^5.9.3"
|
|
@@ -11,6 +11,7 @@ import { requestDashboardRender } from "../core/dashboard-render.mjs";
|
|
|
11
11
|
import { createService } from "../runtime/service.mjs";
|
|
12
12
|
import { planAttachPrelude, planAttachResolved } from "./attach-decision.mjs";
|
|
13
13
|
import { screenLogPath } from "../core/paths.mjs";
|
|
14
|
+
import { appendDiagnostic } from "../core/diagnostics.mjs";
|
|
14
15
|
import { DashboardComponent, type DashboardResult } from "../ui/dashboard.js";
|
|
15
16
|
import { PtyAttachComponent, type PtyAttachResult } from "../ui/pty-attach.js";
|
|
16
17
|
|
|
@@ -241,6 +242,16 @@ async function openPtyAttach(
|
|
|
241
242
|
socketPath,
|
|
242
243
|
screenLogPath: root ? screenLogPath(root, viewId) : undefined,
|
|
243
244
|
title: name,
|
|
245
|
+
onDiagnostic: root
|
|
246
|
+
? (event) => {
|
|
247
|
+
// Best effort, like every other diagnostics writer.
|
|
248
|
+
try {
|
|
249
|
+
appendDiagnostic(root, viewId, event as never);
|
|
250
|
+
} catch {
|
|
251
|
+
/* diagnostics must never break attach */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
: undefined,
|
|
244
255
|
}),
|
|
245
256
|
{
|
|
246
257
|
overlay: true,
|
|
@@ -13,6 +13,7 @@ import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
13
13
|
import { createService } from "../runtime/service.mjs";
|
|
14
14
|
import { planAttachPrelude, planAttachResolved } from "./attach-decision.mjs";
|
|
15
15
|
import { screenLogPath } from "../core/paths.mjs";
|
|
16
|
+
import { appendDiagnostic } from "../core/diagnostics.mjs";
|
|
16
17
|
import { PtyAttachComponent, type PtyAttachResult } from "../ui/pty-attach.js";
|
|
17
18
|
import type { DashboardResult } from "../ui/dashboard.js";
|
|
18
19
|
|
|
@@ -39,6 +40,16 @@ export async function openPtyAttach(
|
|
|
39
40
|
socketPath,
|
|
40
41
|
screenLogPath: root ? screenLogPath(root, viewId) : undefined,
|
|
41
42
|
title: name,
|
|
43
|
+
onDiagnostic: root
|
|
44
|
+
? (event) => {
|
|
45
|
+
// Best effort, like every other diagnostics writer.
|
|
46
|
+
try {
|
|
47
|
+
appendDiagnostic(root, viewId, event as never);
|
|
48
|
+
} catch {
|
|
49
|
+
/* diagnostics must never break attach */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
: undefined,
|
|
42
53
|
}),
|
|
43
54
|
{
|
|
44
55
|
overlay: true,
|
package/src/core/pty-scroll.mjs
CHANGED
|
@@ -94,6 +94,31 @@ export function parseMouseInputChunk(data) {
|
|
|
94
94
|
return events;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Issue #167: map a normalized pi-tui mouse event on the attach surface to an
|
|
99
|
+
* action. "paste-primary" = middle-click paste (read X11 PRIMARY, forward as
|
|
100
|
+
* input); null = not ours, the event falls through untouched (pi's fullscreen
|
|
101
|
+
* selection, the wheel defer path, and everything else keep working). Pure by
|
|
102
|
+
* contract: the env flag is resolved at the call site, never read here.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveAttachMouseAction(event, { nativePasteEnabled }) {
|
|
105
|
+
if (!nativePasteEnabled) return null;
|
|
106
|
+
if (event.type === "press" && event.button === "middle") return "paste-primary";
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Issue #169: whether the attach surface should manage the OUTER terminal's
|
|
112
|
+
* mouse mode. Only in regular TUI mode — in fullscreen, TuiAltScreen owns
|
|
113
|
+
* mouse reporting (asserted once at startup, never re-asserted), so any write
|
|
114
|
+
* from the attach surface can only downgrade or kill it. Old runtimes without
|
|
115
|
+
* tui.mode behave as regular. Pure: both arguments are resolved at the call
|
|
116
|
+
* site (mode from the TUI, the flag from the env-derived helper).
|
|
117
|
+
*/
|
|
118
|
+
export function shouldOwnOuterMouseMode(mode, mouseEnabled) {
|
|
119
|
+
return mode !== "fullscreen" && !!mouseEnabled;
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
/**
|
|
98
123
|
* Return +1 for wheel-up (scroll back), -1 for wheel-down, 0 for non-wheel input.
|
|
99
124
|
* Supports standard/passive SGR and X10/normal mouse encodings.
|
|
@@ -156,3 +156,84 @@ function toLinearChannel(channel) {
|
|
|
156
156
|
const value = channel / 255;
|
|
157
157
|
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
158
158
|
}
|
|
159
|
+
|
|
160
|
+
// --- Attach settle scheme probe (issue #161) -------------------------------
|
|
161
|
+
// pi-tui >= 0.99 renamed queryTerminalBackgroundColor -> queryTerminalColors.
|
|
162
|
+
// This repo compiles against pi-tui 0.79.8 but the extension runs against the
|
|
163
|
+
// host pi's pi-tui (peerDependencies "*"), so the probe feature-detects at
|
|
164
|
+
// runtime. Local structural typedefs only — TerminalColors does not exist on
|
|
165
|
+
// the 0.79.8 compile face and must not be imported.
|
|
166
|
+
|
|
167
|
+
/** @typedef {{ r: number, g: number, b: number }} SettleRgb */
|
|
168
|
+
/** @typedef {{ background?: SettleRgb, foreground?: SettleRgb, palette?: SettleRgb[] }} TerminalColorsLike */
|
|
169
|
+
/** @typedef {{ queryTerminalColors?: (opts: { timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void }) => Promise<TerminalColorsLike>, queryTerminalBackgroundColor?: (opts: { timeoutMs: number }) => Promise<SettleRgb|undefined> }} SettleProbeSurface */
|
|
170
|
+
/** @typedef {{ rgb?: SettleRgb, anyColors: boolean }} SettleProbeResult */
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Issue #161: detect which terminal color query API a TUI surface exposes.
|
|
174
|
+
* Detection is pure; the returned adapter only calls the passed-in surface.
|
|
175
|
+
* @param {SettleProbeSurface} tuiLike
|
|
176
|
+
* @returns {{ api: "colors"|"background"|"none", invoke: (tui: SettleProbeSurface, timeoutMs: number, onLateReply?: (colors: TerminalColorsLike) => void) => Promise<SettleProbeResult> }}
|
|
177
|
+
*/
|
|
178
|
+
export function resolveProbeApi(tuiLike) {
|
|
179
|
+
if (tuiLike && typeof tuiLike.queryTerminalColors === "function") {
|
|
180
|
+
return {
|
|
181
|
+
api: "colors",
|
|
182
|
+
invoke: async (tui, timeoutMs, onLateReply) => {
|
|
183
|
+
const colors = await tui.queryTerminalColors({ timeoutMs, onLateReply });
|
|
184
|
+
return { rgb: backgroundRgbFromTerminalColors(colors), anyColors: hasAnyColor(colors) };
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (tuiLike && typeof tuiLike.queryTerminalBackgroundColor === "function") {
|
|
189
|
+
return {
|
|
190
|
+
api: "background",
|
|
191
|
+
invoke: async (tui, timeoutMs) => {
|
|
192
|
+
const rgb = await tui.queryTerminalBackgroundColor({ timeoutMs });
|
|
193
|
+
return { rgb, anyColors: rgb !== undefined };
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return { api: "none", invoke: async () => ({ rgb: undefined, anyColors: false }) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** @param {TerminalColorsLike} colors @returns {SettleRgb|undefined} */
|
|
201
|
+
export function backgroundRgbFromTerminalColors(colors) {
|
|
202
|
+
return colors && typeof colors === "object" ? colors.background : undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {TerminalColorsLike} colors @returns {boolean} */
|
|
206
|
+
function hasAnyColor(colors) {
|
|
207
|
+
if (!colors || typeof colors !== "object") return false;
|
|
208
|
+
if (colors.background !== undefined || colors.foreground !== undefined) return true;
|
|
209
|
+
return Array.isArray(colors.palette) && colors.palette.some((c) => c !== undefined);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const SETTLE_SCHEME_WARN_OUTCOMES = new Set(["timeout", "error", "no_background", "no_probe_api", "dropped_disconnected"]);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Issue #161: build the DiagnosticEvent-compatible patch for the settle scheme
|
|
216
|
+
* flow. Field names MUST stay inside normalizeDiagnostic's whitelist
|
|
217
|
+
* (level/code/runId/source/message/details) — anything else is silently
|
|
218
|
+
* dropped. Warn only when delivery was expected but failed; routine outcomes
|
|
219
|
+
* (dedup, quick detach, kill switch) stay info so warningCount stays meaningful.
|
|
220
|
+
* @param {{ probeApi?: "colors"|"background"|"none", outcome: "reported"|"timeout"|"error"|"no_background"|"no_probe_api"|"dropped_closed"|"dropped_disconnected"|"duplicate_skipped"|"suppressed", report?: string, late?: boolean }} details
|
|
221
|
+
* @returns {{ source: string, level: "info"|"warn", code: string, message: string, details: { probeApi?: string, outcome: string, report?: string, late?: boolean } }}
|
|
222
|
+
*/
|
|
223
|
+
export function buildSettleSchemePatch(details) {
|
|
224
|
+
const parts = [details.outcome];
|
|
225
|
+
if (details.probeApi) parts.push(`via ${details.probeApi}`);
|
|
226
|
+
if (details.report) parts.push(details.report);
|
|
227
|
+
if (details.late) parts.push("late");
|
|
228
|
+
const patch = {
|
|
229
|
+
source: "attach",
|
|
230
|
+
level: SETTLE_SCHEME_WARN_OUTCOMES.has(details.outcome) ? "warn" : "info",
|
|
231
|
+
code: "attach_settle_scheme",
|
|
232
|
+
message: `attach settle scheme: ${parts.join(" ")}`,
|
|
233
|
+
details: { outcome: details.outcome },
|
|
234
|
+
};
|
|
235
|
+
if (details.probeApi !== undefined) patch.details.probeApi = details.probeApi;
|
|
236
|
+
if (details.report !== undefined) patch.details.report = details.report;
|
|
237
|
+
if (details.late !== undefined) patch.details.late = details.late;
|
|
238
|
+
return patch;
|
|
239
|
+
}
|