@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,509 @@
|
|
|
1
|
+
# Issue #167 Attach Middle-Click Paste (fullscreen TUI) Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make middle-click paste (X11 PRIMARY → session input) work in attach views under pi ≥1.0 fullscreen TUI by implementing `handleMouse` on `PtyAttachComponent`, keeping the legacy `handleInput` path and all current behaviors intact.
|
|
6
|
+
|
|
7
|
+
**Architecture:** pi-tui ≥1.0 fullscreen consumes raw SGR mouse sequences and delivers normalized events to overlays via `Component.handleMouse`. A pure decision function (`resolveAttachMouseAction` in `src/core/pty-scroll.mjs`) maps a normalized event to an action; `PtyAttachComponent.handleMouse` maps that action to the existing `pastePrimarySelection()` side effect and a `{handled:true}` result. devDependencies bump 0.79.8 → 1.1.0 makes the real API surface typecheckable.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript (tabs), node:test + assert/strict, node --experimental-transform-types for component scenarios, PATH-stubbed `xclip` for hermetic paste tests.
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-10-09-issue-167-attach-middle-paste-design.md` (acceptance IDs A1–A4, U1 referenced per task).
|
|
12
|
+
|
|
13
|
+
## Global Constraints
|
|
14
|
+
|
|
15
|
+
- Work ONLY inside the worktree `<WT>` (branch `issue-167-attach-middle-paste`); never touch main.
|
|
16
|
+
- Tabs for indentation; English comments and commit messages; conventional commit subjects (`fix:`/`test:`/`chore:`/`docs:`).
|
|
17
|
+
- devDependencies land on EXACTLY `"@earendil-works/pi-coding-agent": "1.1.0"` and `"@earendil-works/pi-tui": "1.1.0"` (no `^`).
|
|
18
|
+
- Pure-function split is a hard spec constraint: `resolveAttachMouseAction` must not read `process.env` (call site does) and must not be re-inlined into the component.
|
|
19
|
+
- `AGENT_BOARD_ATTACH_NATIVE_PASTE=0` must keep disabling the paste (decision function returns `null` → component returns `undefined`, no side effects).
|
|
20
|
+
- `handleMouse` must return `undefined` for EVERYTHING except `type:"press" && button:"middle"` with paste enabled — no capture, no focus, no wheel handling, no selection takeover (spec non-goals).
|
|
21
|
+
- No changes to outer-terminal mouse-mode writes (`MOUSE_ENABLE`/`MOUSE_DISABLE`, ctor, `close()`) — that is #169, out of scope.
|
|
22
|
+
- Tests use `node:test` with self-contained fixtures under temp dirs; never the host repository.
|
|
23
|
+
- `test/` and `test-support/` are EXCLUDED from tsc — scenario files are runtime-transformed only; `src/` must stay `npm run typecheck`-clean.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
### Task 1: devDependencies bump to pi 1.1.0 (A3 foundation)
|
|
28
|
+
|
|
29
|
+
**Files:**
|
|
30
|
+
- Modify: `package.json` (devDependencies)
|
|
31
|
+
- Modify: `package-lock.json` (via npm install)
|
|
32
|
+
|
|
33
|
+
**Interfaces:**
|
|
34
|
+
- Consumes: none.
|
|
35
|
+
- Produces: pi-tui 1.1.0 typings available to `src/` (`TuiMouseEvent`, `TuiMouseEventResult` importable from `@earendil-works/pi-tui`); Tasks 2–3 rely on this.
|
|
36
|
+
|
|
37
|
+
- [ ] **Step 1: Bump the two devDependencies**
|
|
38
|
+
|
|
39
|
+
In `package.json` `devDependencies`, change exactly:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
"@earendil-works/pi-coding-agent": "0.79.8",
|
|
43
|
+
"@earendil-works/pi-tui": "0.79.8",
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
to:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
"@earendil-works/pi-coding-agent": "1.1.0",
|
|
50
|
+
"@earendil-works/pi-tui": "1.1.0",
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- [ ] **Step 2: Install**
|
|
54
|
+
|
|
55
|
+
Run: `npm install` (runs `postinstall: node scripts/patch-vulns.mjs`)
|
|
56
|
+
Expected: completes without error; lockfile records 1.1.0 for both packages.
|
|
57
|
+
|
|
58
|
+
- [ ] **Step 3: Typecheck**
|
|
59
|
+
|
|
60
|
+
Run: `npm run typecheck`
|
|
61
|
+
Expected: PASS. If errors appear, they are 0.79.8→1.1.0 drift in existing `src/` code. Fix them minimally (type-level only — no behavior changes). If more than ~5 files need non-trivial fixes, STOP and report BLOCKED with the full error list (spec's fallback decision point: local structural types without the bump).
|
|
62
|
+
|
|
63
|
+
- [ ] **Step 4: Existing suite stays green**
|
|
64
|
+
|
|
65
|
+
Run: `npm test`
|
|
66
|
+
Expected: PASS (fake harnesses in test/test-support are excluded from tsc; runtime should be unaffected).
|
|
67
|
+
|
|
68
|
+
- [ ] **Step 5: Commit**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git add package.json package-lock.json
|
|
72
|
+
git commit -m "chore: bump pi-tui and pi-coding-agent devDeps to 1.1.0 for fullscreen TUI typings"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
(Include any src/ type-fix files in the same commit if Step 3 required them.)
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### Task 2: Pure decision function `resolveAttachMouseAction` (A1)
|
|
80
|
+
|
|
81
|
+
**Files:**
|
|
82
|
+
- Modify: `src/core/pty-scroll.mjs` (add export near the other mouse helpers, after `parseMouseInputChunk`)
|
|
83
|
+
- Test: `test/pty-scroll.test.mjs` (extend the existing import block and append tests)
|
|
84
|
+
|
|
85
|
+
**Interfaces:**
|
|
86
|
+
- Consumes: none.
|
|
87
|
+
- Produces: `resolveAttachMouseAction(event, { nativePasteEnabled }) → "paste-primary" | null`, where `event` is structurally `{ type: string, button: string }` (the pi-tui `TuiMouseEvent`). Task 3's component imports this from `../core/pty-scroll.mjs`.
|
|
88
|
+
|
|
89
|
+
- [ ] **Step 1: Write the failing tests**
|
|
90
|
+
|
|
91
|
+
In `test/pty-scroll.test.mjs`, extend the existing import from `../src/core/pty-scroll.mjs` with `resolveAttachMouseAction`, then append:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const ev = (type, button) => ({ type, button });
|
|
95
|
+
|
|
96
|
+
test("resolveAttachMouseAction: enabled middle press is the only paste trigger", () => {
|
|
97
|
+
assert.equal(resolveAttachMouseAction(ev("press", "middle"), { nativePasteEnabled: true }), "paste-primary");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("resolveAttachMouseAction: native paste kill switch disables the trigger", () => {
|
|
101
|
+
assert.equal(resolveAttachMouseAction(ev("press", "middle"), { nativePasteEnabled: false }), null);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("resolveAttachMouseAction: every other event falls through untouched", () => {
|
|
105
|
+
const cases = [
|
|
106
|
+
["press", "left"],
|
|
107
|
+
["press", "right"],
|
|
108
|
+
["press", "none"],
|
|
109
|
+
["release", "middle"],
|
|
110
|
+
["click", "middle"],
|
|
111
|
+
["move", "middle"],
|
|
112
|
+
["drag", "middle"],
|
|
113
|
+
["wheel", "left"],
|
|
114
|
+
["wheel", "middle"],
|
|
115
|
+
];
|
|
116
|
+
for (const [type, button] of cases) {
|
|
117
|
+
assert.equal(resolveAttachMouseAction(ev(type, button), { nativePasteEnabled: true }), null, `${type}/${button}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- [ ] **Step 2: Run tests to verify they fail**
|
|
123
|
+
|
|
124
|
+
Run: `node --test test/pty-scroll.test.mjs`
|
|
125
|
+
Expected: FAIL — `resolveAttachMouseAction` is not exported / not a function.
|
|
126
|
+
|
|
127
|
+
- [ ] **Step 3: Implement**
|
|
128
|
+
|
|
129
|
+
In `src/core/pty-scroll.mjs`, after `parseMouseInputChunk`:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
/**
|
|
133
|
+
* Issue #167: map a normalized pi-tui mouse event on the attach surface to an
|
|
134
|
+
* action. "paste-primary" = middle-click paste (read X11 PRIMARY, forward as
|
|
135
|
+
* input); null = not ours, the event falls through untouched (pi's fullscreen
|
|
136
|
+
* selection, the wheel defer path, and everything else keep working). Pure by
|
|
137
|
+
* contract: the env flag is resolved at the call site, never read here.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveAttachMouseAction(event, { nativePasteEnabled }) {
|
|
140
|
+
if (!nativePasteEnabled) return null;
|
|
141
|
+
if (event.type === "press" && event.button === "middle") return "paste-primary";
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- [ ] **Step 4: Run tests to verify they pass**
|
|
147
|
+
|
|
148
|
+
Run: `node --test test/pty-scroll.test.mjs`
|
|
149
|
+
Expected: PASS (new + all pre-existing cases).
|
|
150
|
+
|
|
151
|
+
- [ ] **Step 5: Commit**
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git add src/core/pty-scroll.mjs test/pty-scroll.test.mjs
|
|
155
|
+
git commit -m "feat(attach): pure mouse-action decision helper for middle-click paste"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Task 3: `PtyAttachComponent.handleMouse` + component-level scenario (A2)
|
|
161
|
+
|
|
162
|
+
**Files:**
|
|
163
|
+
- Modify: `src/ui/pty-attach.ts` (imports at lines 6 and 13; new `handleMouse` method placed right after the `handleInput` method)
|
|
164
|
+
- Create: `test-support/pty-attach-mouse-scenario.ts`
|
|
165
|
+
- Test: `test/pty-attach-mouse.test.mjs`
|
|
166
|
+
|
|
167
|
+
**Interfaces:**
|
|
168
|
+
- Consumes: `resolveAttachMouseAction(event, { nativePasteEnabled })` from Task 2; pi-tui 1.1.0 types `TuiMouseEvent`, `TuiMouseEventResult` (Task 1).
|
|
169
|
+
- Produces: public `PtyAttachComponent.handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined`.
|
|
170
|
+
|
|
171
|
+
- [ ] **Step 1: Write the failing integration test + scenario**
|
|
172
|
+
|
|
173
|
+
Create `test/pty-attach-mouse.test.mjs`:
|
|
174
|
+
|
|
175
|
+
```js
|
|
176
|
+
import assert from "node:assert/strict";
|
|
177
|
+
import test from "node:test";
|
|
178
|
+
import { execFileSync } from "node:child_process";
|
|
179
|
+
import { fileURLToPath } from "node:url";
|
|
180
|
+
import { join } from "node:path";
|
|
181
|
+
|
|
182
|
+
const ROOT_DIR = fileURLToPath(new URL("../", import.meta.url));
|
|
183
|
+
const SCENARIO = join(ROOT_DIR, "test-support", "pty-attach-mouse-scenario.ts");
|
|
184
|
+
|
|
185
|
+
// Component-level coverage (issue #167): the REAL PtyAttachComponent against a
|
|
186
|
+
// REAL runner socket, with a PATH-stubbed xclip printing a fixture string.
|
|
187
|
+
// Asserts the fullscreen handleMouse path pastes PRIMARY as input, the legacy
|
|
188
|
+
// handleInput SGR path still does (regular-mode regression), and the
|
|
189
|
+
// AGENT_BOARD_ATTACH_NATIVE_PASTE=0 kill switch turns both off.
|
|
190
|
+
test("attach mouse: middle-press handleMouse pastes PRIMARY; legacy path intact; kill switch honored", () => {
|
|
191
|
+
const out = execFileSync(process.execPath, ["--experimental-transform-types", SCENARIO], {
|
|
192
|
+
encoding: "utf8",
|
|
193
|
+
timeout: 60_000,
|
|
194
|
+
});
|
|
195
|
+
const parsed = JSON.parse(out.trim().split("\n").filter(Boolean).pop());
|
|
196
|
+
assert.equal(parsed.error, null, `scenario error: ${parsed.error}`);
|
|
197
|
+
assert.equal(parsed.sawContent, true, "session content must render before mouse dispatch");
|
|
198
|
+
assert.equal(parsed.fullscreenHandled, true, "handleMouse(middle press) must return {handled:true}");
|
|
199
|
+
assert.equal(parsed.fullscreenInput, "primary-paste-fixture", "PRIMARY fixture must reach the attach socket as input");
|
|
200
|
+
assert.equal(parsed.offUndefined, true, "kill switch: handleMouse must return undefined");
|
|
201
|
+
assert.equal(parsed.offNoInput, true, "kill switch: no input may be forwarded");
|
|
202
|
+
assert.equal(parsed.legacyInput, "primary-paste-fixture", "legacy SGR middle press must still paste");
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Create `test-support/pty-attach-mouse-scenario.ts` (modeled on `pty-attach-protocol-scenario.ts`):
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// Component-level mouse-dispatch coverage (issue #167): the REAL
|
|
210
|
+
// PtyAttachComponent against a REAL runner socket (fake pty child in
|
|
211
|
+
// steady-stream mode) with a fake TUI and a PATH-stubbed xclip that prints a
|
|
212
|
+
// fixture string. Verifies:
|
|
213
|
+
// 1. fullscreen path: handleMouse(press middle) → {handled:true} and the
|
|
214
|
+
// X11 PRIMARY fixture reaches the attach socket as {type:"input"};
|
|
215
|
+
// 2. kill switch: AGENT_BOARD_ATTACH_NATIVE_PASTE=0 → handleMouse returns
|
|
216
|
+
// undefined and forwards nothing;
|
|
217
|
+
// 3. legacy path (regular-mode regression): the SGR middle-press sequence
|
|
218
|
+
// via handleInput behaves identically.
|
|
219
|
+
// Run via `node --experimental-transform-types`.
|
|
220
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
221
|
+
import { Socket } from "node:net";
|
|
222
|
+
import { tmpdir } from "node:os";
|
|
223
|
+
import { join, resolve } from "node:path";
|
|
224
|
+
import { spawn } from "node:child_process";
|
|
225
|
+
import { atomicWriteJson } from "../src/core/atomic.mjs";
|
|
226
|
+
import * as P from "../src/core/paths.mjs";
|
|
227
|
+
import { createView, readHost } from "../src/core/store.mjs";
|
|
228
|
+
import { PtyAttachComponent } from "../src/ui/pty-attach.ts";
|
|
229
|
+
|
|
230
|
+
const root = mkdtempSync(join(tmpdir(), "agentview-attach-mouse-"));
|
|
231
|
+
const viewId = "mouse-e2e";
|
|
232
|
+
const FIXTURE = "primary-paste-fixture";
|
|
233
|
+
|
|
234
|
+
// Hermetic xclip stub: any invocation prints the fixture. Prepending binDir to
|
|
235
|
+
// PATH keeps the scenario off the host's real X11 clipboard.
|
|
236
|
+
const binDir = join(root, "bin");
|
|
237
|
+
mkdirSync(binDir);
|
|
238
|
+
writeFileSync(join(binDir, "xclip"), `#!/bin/sh\nprintf '%s' "${FIXTURE}"\n`);
|
|
239
|
+
chmodSync(join(binDir, "xclip"), 0o755);
|
|
240
|
+
process.env.PATH = `${binDir}:${process.env.PATH}`;
|
|
241
|
+
|
|
242
|
+
function isAlive(pid: number): boolean {
|
|
243
|
+
try {
|
|
244
|
+
process.kill(pid, 0);
|
|
245
|
+
return true;
|
|
246
|
+
} catch {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
252
|
+
|
|
253
|
+
async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
254
|
+
const start = Date.now();
|
|
255
|
+
while (Date.now() - start < timeoutMs) {
|
|
256
|
+
if (predicate()) return true;
|
|
257
|
+
await sleep(25);
|
|
258
|
+
}
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const meta = createView(root, { id: viewId, name: "mouse attach e2e", cwd: root });
|
|
263
|
+
const configPath = P.hostConfigPath(root, viewId);
|
|
264
|
+
atomicWriteJson(configPath, {
|
|
265
|
+
root,
|
|
266
|
+
viewId,
|
|
267
|
+
sessionFile: meta.sessionFile,
|
|
268
|
+
cwd: process.cwd(),
|
|
269
|
+
initialPrompt: null,
|
|
270
|
+
piCommand: process.execPath,
|
|
271
|
+
piArgsPrefix: [resolve("test-support/fake-pty-pi.mjs")],
|
|
272
|
+
model: null,
|
|
273
|
+
tools: null,
|
|
274
|
+
env: { AGENT_BOARD_ALLOW_PIPE_FALLBACK: "1", FAKE_PTY_STREAM_MODE: "steady" },
|
|
275
|
+
cols: 80,
|
|
276
|
+
rows: 22,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const runner = spawn(process.execPath, [resolve("runner/pty-runner.mjs"), configPath], {
|
|
280
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
281
|
+
});
|
|
282
|
+
runner.stderr.resume();
|
|
283
|
+
|
|
284
|
+
const result = {
|
|
285
|
+
ok: false,
|
|
286
|
+
sawContent: false,
|
|
287
|
+
fullscreenHandled: false,
|
|
288
|
+
fullscreenInput: null as string | null,
|
|
289
|
+
offUndefined: false,
|
|
290
|
+
offNoInput: false,
|
|
291
|
+
legacyInput: null as string | null,
|
|
292
|
+
error: null as string | null,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const ready = await waitFor(() => {
|
|
297
|
+
const host = readHost(root, viewId);
|
|
298
|
+
return !!host && host.state === "alive" && !!host.socketPath && !!host.childPid && isAlive(host.runnerPid) && isAlive(host.childPid);
|
|
299
|
+
}, 10_000);
|
|
300
|
+
if (!ready) throw new Error("host never became ready");
|
|
301
|
+
|
|
302
|
+
// Intercept component→runner messages to observe {type:"input"} pastes.
|
|
303
|
+
const sent: string[] = [];
|
|
304
|
+
const socketProto = Socket.prototype as any;
|
|
305
|
+
const origWrite = socketProto.write;
|
|
306
|
+
socketProto.write = function (data: any, ...rest: any[]) {
|
|
307
|
+
if (typeof data === "string") sent.push(data);
|
|
308
|
+
return origWrite.call(this, data, ...rest);
|
|
309
|
+
};
|
|
310
|
+
const pastedInputs = () =>
|
|
311
|
+
sent
|
|
312
|
+
.map((l) => {
|
|
313
|
+
try {
|
|
314
|
+
return JSON.parse(l);
|
|
315
|
+
} catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
.filter((m) => !!m && m.type === "input" && typeof m.data === "string")
|
|
320
|
+
.map((m) => m.data as string);
|
|
321
|
+
|
|
322
|
+
const tui = {
|
|
323
|
+
terminal: { rows: 24, cols: 80, columns: 80, write: () => {} },
|
|
324
|
+
requestRender: () => {},
|
|
325
|
+
};
|
|
326
|
+
const theme = { fg: (_c: string, t: string) => t, bold: (t: string) => t };
|
|
327
|
+
const component = new PtyAttachComponent(
|
|
328
|
+
tui as never,
|
|
329
|
+
theme,
|
|
330
|
+
{} as never,
|
|
331
|
+
() => {},
|
|
332
|
+
{
|
|
333
|
+
socketPath: readHost(root, viewId).socketPath,
|
|
334
|
+
screenLogPath: P.screenLogPath(root, viewId),
|
|
335
|
+
title: "mouse attach e2e",
|
|
336
|
+
} as never,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Wait for session content so the paste lands on a settled attach.
|
|
340
|
+
const deadline = Date.now() + 8000;
|
|
341
|
+
while (Date.now() < deadline && !result.sawContent) {
|
|
342
|
+
const lines = component.render(80) ?? [];
|
|
343
|
+
if (lines.join("\n").includes("steady-")) result.sawContent = true;
|
|
344
|
+
else await sleep(40);
|
|
345
|
+
}
|
|
346
|
+
if (!result.sawContent) throw new Error("session content never rendered");
|
|
347
|
+
|
|
348
|
+
// 1. Fullscreen path: normalized middle press via handleMouse.
|
|
349
|
+
const middlePress = {
|
|
350
|
+
type: "press",
|
|
351
|
+
button: "middle",
|
|
352
|
+
x: 3,
|
|
353
|
+
y: 3,
|
|
354
|
+
screenX: 3,
|
|
355
|
+
screenY: 3,
|
|
356
|
+
width: 80,
|
|
357
|
+
height: 22,
|
|
358
|
+
shift: false,
|
|
359
|
+
};
|
|
360
|
+
const before = pastedInputs().length;
|
|
361
|
+
const mouseResult = (component as any).handleMouse(middlePress);
|
|
362
|
+
result.fullscreenHandled = !!mouseResult && mouseResult.handled === true;
|
|
363
|
+
if (await waitFor(() => pastedInputs().length > before, 3000)) {
|
|
364
|
+
result.fullscreenInput = pastedInputs().at(-1) ?? null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 2. Kill switch: handleMouse returns undefined and forwards nothing.
|
|
368
|
+
process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE = "0";
|
|
369
|
+
const offBefore = pastedInputs().length;
|
|
370
|
+
const offResult = (component as any).handleMouse(middlePress);
|
|
371
|
+
result.offUndefined = offResult === undefined;
|
|
372
|
+
await sleep(600);
|
|
373
|
+
result.offNoInput = pastedInputs().length === offBefore;
|
|
374
|
+
delete process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE;
|
|
375
|
+
|
|
376
|
+
// 3. Legacy path (regular-mode regression): SGR middle press via handleInput.
|
|
377
|
+
const legacyBefore = pastedInputs().length;
|
|
378
|
+
component.handleInput("\x1b[<1;10;20M");
|
|
379
|
+
if (await waitFor(() => pastedInputs().length > legacyBefore, 3000)) {
|
|
380
|
+
result.legacyInput = pastedInputs().at(-1) ?? null;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
result.ok =
|
|
384
|
+
result.sawContent &&
|
|
385
|
+
result.fullscreenHandled &&
|
|
386
|
+
result.fullscreenInput === FIXTURE &&
|
|
387
|
+
result.offUndefined &&
|
|
388
|
+
result.offNoInput &&
|
|
389
|
+
result.legacyInput === FIXTURE;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
component.dispose();
|
|
393
|
+
} catch {}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
result.error = err instanceof Error ? err.message : String(err);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const pid = readHost(root, viewId)?.childPid;
|
|
400
|
+
if (pid) process.kill(pid, "SIGKILL");
|
|
401
|
+
} catch {}
|
|
402
|
+
try {
|
|
403
|
+
runner.kill("SIGKILL");
|
|
404
|
+
} catch {}
|
|
405
|
+
await sleep(50);
|
|
406
|
+
try {
|
|
407
|
+
rmSync(root, { recursive: true, force: true, maxRetries: 3, retryDelay: 30 });
|
|
408
|
+
} catch {}
|
|
409
|
+
|
|
410
|
+
console.log(JSON.stringify(result));
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- [ ] **Step 2: Run the integration test to verify it fails**
|
|
414
|
+
|
|
415
|
+
Run: `node --test test/pty-attach-mouse.test.mjs`
|
|
416
|
+
Expected: FAIL — `parsed.fullscreenHandled` false (and/or `fullscreenInput` null) because `component.handleMouse` does not exist yet. The `legacyInput` assertion may already pass; that is fine, the failure must be on the fullscreen fields.
|
|
417
|
+
|
|
418
|
+
- [ ] **Step 3: Implement `handleMouse` on the component**
|
|
419
|
+
|
|
420
|
+
In `src/ui/pty-attach.ts`:
|
|
421
|
+
|
|
422
|
+
Line 6 — extend the type import:
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
import type { Component, KeybindingsManager, RgbColor, TUI, TerminalColorScheme, TuiMouseEvent, TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Line 13 — extend the core import:
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { clampInt, parseMouseInputChunk, resolveAttachMouseAction, resolveWheelLines, scrollViewportTop, selectionDragScrollLines } from "../core/pty-scroll.mjs";
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Immediately after the existing `handleInput(data: string): void { ... }` method, add:
|
|
435
|
+
|
|
436
|
+
```ts
|
|
437
|
+
/** Issue #167: fullscreen TUI (pi ≥1.0) consumes raw SGR mouse sequences in
|
|
438
|
+
* TuiAltScreen and delivers normalized events to overlays here instead — the
|
|
439
|
+
* legacy handleInput mouse path never runs in that mode. Only the
|
|
440
|
+
* middle-click paste is ours; every other event returns undefined so pi's
|
|
441
|
+
* own fullscreen selection and the wheel defer path keep working. */
|
|
442
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
443
|
+
const action = resolveAttachMouseAction(event, {
|
|
444
|
+
nativePasteEnabled: process.env.AGENT_BOARD_ATTACH_NATIVE_PASTE !== "0",
|
|
445
|
+
});
|
|
446
|
+
if (action !== "paste-primary") return undefined;
|
|
447
|
+
this.clearPendingClick();
|
|
448
|
+
this.clearSelection();
|
|
449
|
+
this.pastePrimarySelection();
|
|
450
|
+
return { handled: true, render: false };
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
- [ ] **Step 4: Run the integration test to verify it passes**
|
|
455
|
+
|
|
456
|
+
Run: `node --test test/pty-attach-mouse.test.mjs`
|
|
457
|
+
Expected: PASS.
|
|
458
|
+
|
|
459
|
+
- [ ] **Step 5: Run the unit tests too**
|
|
460
|
+
|
|
461
|
+
Run: `node --test test/pty-scroll.test.mjs`
|
|
462
|
+
Expected: PASS.
|
|
463
|
+
|
|
464
|
+
- [ ] **Step 6: Commit**
|
|
465
|
+
|
|
466
|
+
```bash
|
|
467
|
+
git add src/ui/pty-attach.ts test-support/pty-attach-mouse-scenario.ts test/pty-attach-mouse.test.mjs
|
|
468
|
+
git commit -m "fix(attach): middle-click paste via handleMouse under fullscreen TUI (issue #167)"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### Task 4: Full verify + coverage gate (A3, A4)
|
|
474
|
+
|
|
475
|
+
**Files:**
|
|
476
|
+
- Modify: whatever Task 1–3 fallout requires (expected: none).
|
|
477
|
+
|
|
478
|
+
**Interfaces:**
|
|
479
|
+
- Consumes: everything from Tasks 1–3.
|
|
480
|
+
- Produces: green `npm run verify` — the PR-ready state.
|
|
481
|
+
|
|
482
|
+
- [ ] **Step 1: Run the full verify pipeline**
|
|
483
|
+
|
|
484
|
+
Run: `npm run verify` (= typecheck + perf gate + tests + coverage + pack:dry)
|
|
485
|
+
Expected: PASS end-to-end. Coverage thresholds: lines 85 / funcs 80 / branches 70 — the new pure function is fully covered by Task 2; `handleMouse` by Task 3's scenario is exercised at runtime (coverage counts only files it instruments via c8 — if the new component lines show as uncovered, extend `test/pty-scroll.test.mjs`-style unit reach only if c8 actually flags them; do not add coverage-only hacks).
|
|
486
|
+
|
|
487
|
+
- [ ] **Step 2: Fix any fallout, then re-run**
|
|
488
|
+
|
|
489
|
+
Run: `npm run verify`
|
|
490
|
+
Expected: PASS. If the perf gate (A11) or a pre-existing flaky real-process test blocks, re-run that single test once; if still red, STOP and report BLOCKED with the failure output (do not paper over CI-known flakes — see issue #95).
|
|
491
|
+
|
|
492
|
+
- [ ] **Step 3: Commit (only if Step 2 needed changes)**
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
git add <changed files>
|
|
496
|
+
git commit -m "test: verify fallout fixes for issue #167"
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
---
|
|
500
|
+
|
|
501
|
+
## Acceptance traceability
|
|
502
|
+
|
|
503
|
+
| Spec ID | Covered by |
|
|
504
|
+
| --- | --- |
|
|
505
|
+
| A1 (unit decision table) | Task 2 |
|
|
506
|
+
| A2 (component-level integration) | Task 3 |
|
|
507
|
+
| A3 (static/build) | Task 1 + Task 4 |
|
|
508
|
+
| A4 (existing suite regression) | Task 1 Step 4 + Task 4 |
|
|
509
|
+
| U1 (user manual: real terminal) | Post-implementation checklist for the user (fullscreen middle-click paste, regular mode unchanged, kill switch) — executed after the branch is merged or from the worktree install |
|