@hienlh/ppm 0.18.11 → 0.18.12
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 +5 -0
- package/README.md +81 -23
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/package.json +1 -1
- package/.spike/cdp-client.ts +0 -116
- package/.spike/cleanup-ppm-workspace.mjs +0 -44
- package/.spike/group-protected-probe.ts +0 -21
- package/.spike/pip-eval.ts +0 -17
- package/.spike/pip-page-helpers.js +0 -119
- package/.spike/pip-results-editor-glue.json +0 -105
- package/.spike/pip-results-editor.json +0 -158
- package/.spike/pip-results-input-isolation.json +0 -28
- package/.spike/pip-results-key-probe.json +0 -58
- package/.spike/pip-results-listener-survival.json +0 -98
- package/.spike/pip-results-listeners.json +0 -36
- package/.spike/pip-results-pagehide.json +0 -92
- package/.spike/pip-results-resize.json +0 -104
- package/.spike/pip-results-terminal-debug.json +0 -99
- package/.spike/pip-results-terminal-v2.json +0 -191
- package/.spike/pip-results-terminal.json +0 -220
- package/.spike/pip-results-xterm-trace.json +0 -73
- package/.spike/pip-spike-common.ts +0 -83
- package/.spike/pip-spike-editor-glue.ts +0 -82
- package/.spike/pip-spike-editor.ts +0 -129
- package/.spike/pip-spike-input-isolation.ts +0 -38
- package/.spike/pip-spike-key-probe.ts +0 -34
- package/.spike/pip-spike-listener-survival.ts +0 -54
- package/.spike/pip-spike-listeners.ts +0 -19
- package/.spike/pip-spike-pagehide.ts +0 -59
- package/.spike/pip-spike-resize.ts +0 -42
- package/.spike/pip-spike-setup.ts +0 -63
- package/.spike/pip-spike-terminal-debug.ts +0 -81
- package/.spike/pip-spike-terminal-v2.ts +0 -139
- package/.spike/pip-spike-terminal.ts +0 -169
- package/.spike/pip-spike-xterm-trace.ts +0 -36
- package/.spike/probe-drag-memo-error.mjs +0 -67
- package/.spike/read-dev-db.ts +0 -15
- package/.spike/s.err +0 -63
- package/.spike/scratch-editor-target.ts +0 -7
- package/.spike/smoke-unified-chrome-pip.mjs +0 -353
- package/.spike/tunnel.err +0 -63
- package/.spike/vite-restart.err +0 -663
- package/.spike/w.err +0 -571
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Stage 3: move the live Monaco editor tab wrapper into a Document PiP window and test it.
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const TAB = "editor:.spike/scratch-editor-target.ts";
|
|
5
|
-
const Q = `document.querySelector('[data-tab-pool-id="${TAB}"]')`;
|
|
6
|
-
const W = `window.__spike.ew`;
|
|
7
|
-
const E = `window.__spike.ed`;
|
|
8
|
-
const R: Record<string, unknown> = {};
|
|
9
|
-
const h = await connectApp();
|
|
10
|
-
const { ev, cdp, sid } = h;
|
|
11
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, sid);
|
|
12
|
-
|
|
13
|
-
await ev(`(() => { const S = window.__spike; if (S.pip && !S.pip.closed) { if (S.ew && S.ew.ownerDocument !== document) S.moveBack(S.ew); S.pip.close(); } S.pip = null; S.results = {}; return 'reset'; })()`);
|
|
14
|
-
await h.openViaUrl("/project/ppm/editor/.spike/scratch-editor-target.ts");
|
|
15
|
-
await h.waitFor(`${Q} && ${Q}.style.display !== 'none' && ${Q}.querySelector('.monaco-editor .view-lines') && window.__spike.findEditor(${Q})`, 30000, "editor visible");
|
|
16
|
-
await ev(`window.__spike.ew = ${Q}; window.__spike.ed = window.__spike.findEditor(window.__spike.ew); 'held'`);
|
|
17
|
-
const wordWrapExpr = `${E}.getOption(window.monaco.editor.EditorOption.wordWrap)`;
|
|
18
|
-
|
|
19
|
-
async function typeAt(targetSid: string, text: string) {
|
|
20
|
-
await cdp.typeText(targetSid, text);
|
|
21
|
-
await sleep(300);
|
|
22
|
-
}
|
|
23
|
-
// Baseline in main: put cursor at 1:1, type marker (trusted)
|
|
24
|
-
await ev(`${E}.setPosition({ lineNumber: 1, column: 1 }); ${E}.focus(); 'f'`);
|
|
25
|
-
await typeAt(sid, "MAIN1 ");
|
|
26
|
-
await sleep(2000); // let the 1s autosave + dirty-title re-render settle before the move
|
|
27
|
-
R.baseline = await ev(`({ line1: ${E}.getModel().getLineContent(1), layoutW: ${E}.getLayoutInfo().width, layoutH: ${E}.getLayoutInfo().height, wordWrap: ${wordWrapExpr}, minimapCanvases: ${W}.querySelectorAll('.minimap canvas').length })`);
|
|
28
|
-
|
|
29
|
-
R.move = await h.runGesture("pip-editor", `async () => {
|
|
30
|
-
const S = window.__spike; const w = ${W}; const ed = ${E};
|
|
31
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 900, height: 600 }); S.pip = pip;
|
|
32
|
-
const styles = S.copyStyles(pip.document); const moved = S.moveToPip(pip, w); S.pinToPip(w); S.installClickProbes(pip, w);
|
|
33
|
-
await new Promise(r => setTimeout(r, 800));
|
|
34
|
-
const vl = w.querySelector('.view-lines'); const mm = w.querySelector('.minimap canvas');
|
|
35
|
-
return { styles, moved, pipSize: [pip.innerWidth, pip.innerHeight], viewLines: vl.children.length, firstLineText: vl.children[0]?.textContent, layout: [ed.getLayoutInfo().width, ed.getLayoutInfo().height],
|
|
36
|
-
minimap: mm ? { w: mm.width, h: mm.height } : null, editorRect: (() => { const r = ed.getDomNode().getBoundingClientRect(); return [r.width, r.height]; })(), monacoTheme: pip.getComputedStyle(w.querySelector('.monaco-editor')).backgroundColor, mainBg: getComputedStyle(document.body).backgroundColor };
|
|
37
|
-
}`);
|
|
38
|
-
const pipSid = await h.attachPip();
|
|
39
|
-
R.pipAttached = !!pipSid;
|
|
40
|
-
if (!pipSid) throw new Error("no pip target");
|
|
41
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, pipSid);
|
|
42
|
-
await sleep(600);
|
|
43
|
-
R.shotInPip = await h.pipShot(pipSid, "spike-02-editor-in-pip.png");
|
|
44
|
-
|
|
45
|
-
// Mouse: trusted click at line 3 in the PiP → cursor moves? then drag-select on line 2
|
|
46
|
-
R.mouseInPip = await (async () => {
|
|
47
|
-
const geo = await cdp.eval<any>(pipSid, `(() => { const lines = [...document.querySelectorAll('.view-line')]; const l3 = lines.find(l => l.textContent.includes('spikeLine3')) || lines[2]; if (!l3) return null; const r = l3.getBoundingClientRect(); return { x: r.x + 40, y: r.y + r.height / 2, text: l3.textContent }; })()`);
|
|
48
|
-
if (!geo) return { error: "no .view-line in pip doc", inPip: await ev(`${W}.ownerDocument !== document`) };
|
|
49
|
-
await cdp.click(pipSid, geo.x, geo.y);
|
|
50
|
-
await sleep(300);
|
|
51
|
-
const pos = await ev<any>(`(() => { const p = ${E}.getPosition(); return [p.lineNumber, p.column]; })()`);
|
|
52
|
-
const g2 = await cdp.eval<any>(pipSid, `(() => { const l = [...document.querySelectorAll('.view-line')].find(l => l.textContent.includes('spikeLine2')); const r = l.getBoundingClientRect(); return { x: r.x + 4, y: r.y + r.height / 2 }; })()`);
|
|
53
|
-
await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x: g2.x, y: g2.y, button: "left", clickCount: 1 }, pipSid);
|
|
54
|
-
for (let i = 1; i <= 8; i++) await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: g2.x + i * 20, y: g2.y, button: "left", buttons: 1 }, pipSid);
|
|
55
|
-
await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: g2.x + 160, y: g2.y, button: "left", clickCount: 1 }, pipSid);
|
|
56
|
-
await sleep(300);
|
|
57
|
-
const sel = await ev<string>(`${E}.getModel().getValueInRange(${E}.getSelection())`);
|
|
58
|
-
await h.pipShot(pipSid, "spike-02b-editor-in-pip-selection.png");
|
|
59
|
-
return { clickedLineText: geo.text, positionAfterClick: pos, dragSelection: sel, dragPass: sel.length > 0 };
|
|
60
|
-
})();
|
|
61
|
-
|
|
62
|
-
// Keyboard: trusted typing in PiP at 1:1
|
|
63
|
-
await ev(`${E}.setPosition({ lineNumber: 1, column: 1 }); 'p'`);
|
|
64
|
-
await cdp.eval(pipSid, `document.querySelector('.monaco-editor textarea').focus(); document.activeElement.className`);
|
|
65
|
-
await typeAt(pipSid, "PIP2 ");
|
|
66
|
-
R.typingInPip = await ev(`({ line1: ${E}.getModel().getLineContent(1), pass: ${E}.getModel().getLineContent(1).startsWith('PIP2 MAIN1 ') })`);
|
|
67
|
-
R.shotTyped = await h.pipShot(pipSid, "spike-02c-editor-in-pip-typed.png");
|
|
68
|
-
|
|
69
|
-
// Resize: wrapper style (automaticLayout RO path) + pip resizeTo via trusted click
|
|
70
|
-
R.resizeViaStyle = await ev(`(async () => { const w = ${W}; const ed = ${E}; const before = [ed.getLayoutInfo().width, ed.getLayoutInfo().height]; w.style.height = '50%'; w.style.width = '70%';
|
|
71
|
-
await new Promise(r => setTimeout(r, 800)); const after = [ed.getLayoutInfo().width, ed.getLayoutInfo().height]; w.style.height = '100%'; w.style.width = '100%'; await new Promise(r => setTimeout(r, 800));
|
|
72
|
-
return { before, after, restored: [ed.getLayoutInfo().width, ed.getLayoutInfo().height], relayout: before[0] !== after[0] }; })()`);
|
|
73
|
-
R.resizeViaWindow = await (async () => {
|
|
74
|
-
const pt = await cdp.eval<any>(pipSid, `(() => { const b = document.createElement('button'); b.id = 'rz'; b.textContent = 'rz'; b.style.cssText = 'position:fixed;right:4px;bottom:4px;z-index:99999'; b.onclick = () => window.resizeTo(700, 450); document.body.append(b); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()`);
|
|
75
|
-
const before = await ev<any>(`[${E}.getLayoutInfo().width, ${E}.getLayoutInfo().height]`);
|
|
76
|
-
await cdp.click(pipSid, pt.x, pt.y);
|
|
77
|
-
const s: unknown[] = [];
|
|
78
|
-
for (const ms of [300, 1000, 3000]) { await sleep(ms); s.push(await ev(`({ afterMs: ${ms}, layout: [${E}.getLayoutInfo().width, ${E}.getLayoutInfo().height], pip: [window.__spike.pip.innerWidth, window.__spike.pip.innerHeight] })`)); }
|
|
79
|
-
await cdp.eval(pipSid, `document.getElementById('rz').remove(); 'x'`);
|
|
80
|
-
return { before, samples: s };
|
|
81
|
-
})();
|
|
82
|
-
R.shotResized = await h.pipShot(pipSid, "spike-02d-editor-in-pip-resized.png");
|
|
83
|
-
|
|
84
|
-
// React onClick in PiP: the toolbar "Wrap" button toggles wordWrap through a zustand store → Monaco option
|
|
85
|
-
R.reactClickInPip = await (async () => {
|
|
86
|
-
const pt = await cdp.eval<any>(pipSid, `(() => { const b = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === 'Wrap'); if (!b) return null; const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()`);
|
|
87
|
-
if (!pt) return "no Wrap button in pip";
|
|
88
|
-
const before = await ev(wordWrapExpr);
|
|
89
|
-
await ev(`window.__spike.probe = { mainDocClick: 0, pipDocClick: 0, wrapperClick: 0, reactRootClick: 0 }; 'r'`);
|
|
90
|
-
await cdp.click(pipSid, pt.x, pt.y);
|
|
91
|
-
await sleep(600);
|
|
92
|
-
return { wordWrapBefore: before, wordWrapAfter: await ev(wordWrapExpr), probe: await ev(`({ ...window.__spike.probe })`) };
|
|
93
|
-
})();
|
|
94
|
-
|
|
95
|
-
// Widgets: trigger suggest (Ctrl+Space) in PiP and see where the widget renders
|
|
96
|
-
R.suggestWidget = await (async () => {
|
|
97
|
-
await ev(`${E}.setPosition({ lineNumber: 7, column: 1 }); 'p'`);
|
|
98
|
-
await cdp.eval(pipSid, `document.querySelector('.monaco-editor textarea').focus(); 'f'`);
|
|
99
|
-
await cdp.typeText(pipSid, "spi");
|
|
100
|
-
await sleep(1200);
|
|
101
|
-
const r = await ev(`(() => { const w = ${W}; const sw = w.querySelector('.suggest-widget'); const mainSw = document.querySelector('.suggest-widget');
|
|
102
|
-
return { inWrapper: !!sw, visible: sw ? sw.classList.contains('visible') : null, inMainDoc: !!mainSw, overflowContainer: !!w.querySelector('.overflowingContentWidgets'), fixedOverflow: ${E}.getOption(window.monaco.editor.EditorOption.fixedOverflowWidgets) }; })()`);
|
|
103
|
-
await h.pipShot(pipSid, "spike-02e-editor-in-pip-suggest.png");
|
|
104
|
-
await cdp.pressKey(pipSid, "Escape", "Escape", 27);
|
|
105
|
-
for (let i = 0; i < 3; i++) await cdp.pressKey(pipSid, "Backspace", "Backspace", 8);
|
|
106
|
-
return r;
|
|
107
|
-
})();
|
|
108
|
-
|
|
109
|
-
// Move back, then undo history: Ctrl+Z x2 should remove "PIP2 " then "MAIN1 "
|
|
110
|
-
R.pullbackAttemptsWhileInPip = await ev(`({ attempts: window.__spike.pullbackAttempts, stillInPip: ${W}.ownerDocument !== document })`);
|
|
111
|
-
R.back = await ev(`(async () => { const S = window.__spike; const w = ${W}; const r = S.moveBack(w); await new Promise(res => setTimeout(res, 800)); return { ...r, layout: [${E}.getLayoutInfo().width, ${E}.getLayoutInfo().height], line1: ${E}.getModel().getLineContent(1) }; })()`);
|
|
112
|
-
await ev(`window.__spike.pip.close(); 'c'`);
|
|
113
|
-
await sleep(500);
|
|
114
|
-
await ev(`${E}.focus(); 'f'`);
|
|
115
|
-
await cdp.pressKey(sid, "z", "KeyZ", 90, undefined, 2); // Ctrl+Z
|
|
116
|
-
await sleep(300);
|
|
117
|
-
const afterUndo1 = await ev(`${E}.getModel().getLineContent(1)`);
|
|
118
|
-
await cdp.pressKey(sid, "z", "KeyZ", 90, undefined, 2);
|
|
119
|
-
await sleep(300);
|
|
120
|
-
const afterUndo2 = await ev(`${E}.getModel().getLineContent(1)`);
|
|
121
|
-
await ev(`${E}.setPosition({ lineNumber: 1, column: 1 }); ${E}.focus(); 'f'`);
|
|
122
|
-
await typeAt(sid, "BACK3 ");
|
|
123
|
-
R.afterReturn = await ev(`(async () => { const S = window.__spike; const w = ${W}; const ed = ${E}; const btn = [...w.querySelectorAll('button')].find(b => b.textContent.trim() === 'Wrap'); const ww0 = ${wordWrapExpr}; btn && btn.click(); await new Promise(r => setTimeout(r, 500));
|
|
124
|
-
return { afterUndo1: ${JSON.stringify(afterUndo1)}, afterUndo2: ${JSON.stringify(afterUndo2)}, undoPass: ${JSON.stringify(afterUndo2)}.startsWith('export const spikeLine1'), line1AfterTyping: ed.getModel().getLineContent(1), typingPass: ed.getModel().getLineContent(1).startsWith('BACK3 '), reactWrapToggle: { before: ww0, after: ${wordWrapExpr} } }; })()`);
|
|
125
|
-
// restore wordWrap + undo the BACK3 edit so the scratch file autosaves clean-ish
|
|
126
|
-
await ev(`(async () => { const w = ${W}; const btn = [...w.querySelectorAll('button')].find(b => b.textContent.trim() === 'Wrap'); if (${wordWrapExpr} !== 'off' && btn) btn.click(); ${E}.trigger('spike', 'undo', null); return 'ok'; })()`);
|
|
127
|
-
await cdp.screenshot(sid, "C:/Users/PC/ppm/plans/reports/screenshots/spike-04-editor-after-return.png");
|
|
128
|
-
await saveResults("pip-results-editor.json", R);
|
|
129
|
-
cdp.close();
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Debug: isolate "keys ignored" — round-tripped terminal vs fresh terminal, term.input() vs keyboard.
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const R: Record<string, unknown> = {};
|
|
5
|
-
const h = await connectApp();
|
|
6
|
-
const { ev, cdp, sid } = h;
|
|
7
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, sid);
|
|
8
|
-
|
|
9
|
-
const T1 = `window.__spike.findTerminal(window.__spike.tw)`;
|
|
10
|
-
// 1. term.input on the round-tripped terminal
|
|
11
|
-
await h.openViaUrl("/project/ppm/terminal/1");
|
|
12
|
-
await sleep(500);
|
|
13
|
-
R.inputApiRoundTripped = await ev(`(async () => { const S = window.__spike; const t = ${T1}; S.dataLog = []; t.input('echo VIA-INPUT-API\\r', true);
|
|
14
|
-
await new Promise(r => setTimeout(r, 1500)); return { dataLog: S.dataLog, echoed: S.termText(t, 80).includes('VIA-INPUT-API'), tail: S.termText(t, 3), rows: t.rows, cols: t.cols, visible: t.element.getBoundingClientRect().height }; })()`);
|
|
15
|
-
|
|
16
|
-
// 2. Fresh terminal tab (never moved): trusted keys
|
|
17
|
-
await h.openViaUrl("/project/ppm/terminal/2");
|
|
18
|
-
const Q2 = `document.querySelector('[data-tab-pool-id^="terminal:2"]')`;
|
|
19
|
-
await h.waitFor(`${Q2} && ${Q2}.querySelector('.xterm-helper-textarea')`, 15000, "terminal 2");
|
|
20
|
-
await sleep(3000); // shell boot
|
|
21
|
-
await ev(`window.__spike.tw2 = ${Q2}; window.__spike.t2 = window.__spike.findTerminal(window.__spike.tw2); window.__spike.dataLog2 = []; window.__spike.t2.onData(d => window.__spike.dataLog2.push(d)); 'ok'`);
|
|
22
|
-
await ev(`window.__spike.tw2.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
23
|
-
await cdp.typeText(sid, "echo FRESH-TERM");
|
|
24
|
-
await cdp.pressKey(sid, "Enter", "Enter", 13, "\r");
|
|
25
|
-
await sleep(1500);
|
|
26
|
-
R.freshTerminalTrustedKeys = await ev(`(() => { const S = window.__spike; const t = S.t2; return { id: S.tw2.dataset.tabPoolId, dataLog: S.dataLog2.join(''), echoed: S.termText(t, 80).includes('FRESH-TERM'), tail: S.termText(t, 3), rows: t.rows, cols: t.cols }; })()`);
|
|
27
|
-
|
|
28
|
-
// 3. Back to the round-tripped terminal: trusted keys with focus emulation on
|
|
29
|
-
await h.openViaUrl("/project/ppm/terminal/1");
|
|
30
|
-
await sleep(500);
|
|
31
|
-
await ev(`window.__spike.dataLog = []; window.__spike.tw.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
32
|
-
await cdp.typeText(sid, "echo RT-KEYS");
|
|
33
|
-
await cdp.pressKey(sid, "Enter", "Enter", 13, "\r");
|
|
34
|
-
await sleep(1500);
|
|
35
|
-
R.roundTrippedTrustedKeys = await ev(`(() => { const S = window.__spike; const t = ${T1}; return { dataLog: S.dataLog.join(''), echoed: S.termText(t, 80).includes('RT-KEYS'), tail: S.termText(t, 3),
|
|
36
|
-
textareaIsTermTextarea: t.textarea === S.tw.querySelector('.xterm-helper-textarea'), textareaDoc: t.textarea.ownerDocument === document, elementDoc: t.element.ownerDocument === document }; })()`);
|
|
37
|
-
await saveResults("pip-results-input-isolation.json", R);
|
|
38
|
-
cdp.close();
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Debug: which keyboard events reach the xterm textarea (main doc) under CDP input; effect of focus emulation.
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const TAB = "terminal:1@panel-8281c2e3";
|
|
5
|
-
const Q = `document.querySelector('[data-tab-pool-id="${TAB}"]')`;
|
|
6
|
-
const R: Record<string, unknown> = {};
|
|
7
|
-
const h = await connectApp();
|
|
8
|
-
const { ev, cdp, sid } = h;
|
|
9
|
-
|
|
10
|
-
await ev(`(() => { const S = window.__spike; if (S.pip && !S.pip.closed) { if (S.tw && S.tw.ownerDocument !== document) S.moveBack(S.tw); S.pip.close(); } S.pip = null; return 'reset'; })()`);
|
|
11
|
-
await h.openViaUrl("/project/ppm/terminal/1");
|
|
12
|
-
await h.waitFor(`${Q} && ${Q}.style.display !== 'none'`, 15000, "terminal visible");
|
|
13
|
-
await ev(`window.__spike.tw = ${Q}; (() => { const S = window.__spike; const ta = S.tw.querySelector('.xterm-helper-textarea'); S.keyLog = [];
|
|
14
|
-
if (!S.keyProbe) { for (const t of ['keydown','keypress','input','beforeinput','keyup']) ta.addEventListener(t, e => S.keyLog.push(t + ':' + (e.key ?? e.data ?? '') + ':' + e.isTrusted)); S.keyProbe = true; }
|
|
15
|
-
return 'probe'; })()`);
|
|
16
|
-
|
|
17
|
-
async function probe(label: string) {
|
|
18
|
-
await ev(`window.__spike.keyLog = []; window.__spike.dataLog = []; window.__spike.tw.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
19
|
-
await cdp.typeText(sid, "ab");
|
|
20
|
-
await sleep(300);
|
|
21
|
-
R[label] = await ev(`({ hasFocus: document.hasFocus(), vis: document.visibilityState, active: document.activeElement && document.activeElement.className, keyLog: window.__spike.keyLog, dataLog: window.__spike.dataLog })`);
|
|
22
|
-
}
|
|
23
|
-
await probe("noEmulation");
|
|
24
|
-
await cdp.send("Page.bringToFront", {}, sid);
|
|
25
|
-
await sleep(300);
|
|
26
|
-
await probe("afterBringToFront");
|
|
27
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, sid);
|
|
28
|
-
await sleep(300);
|
|
29
|
-
await probe("withFocusEmulation");
|
|
30
|
-
// Clean the typed junk from the shell line
|
|
31
|
-
await cdp.pressKey(sid, "Escape", "Escape", 27);
|
|
32
|
-
for (let i = 0; i < 8; i++) await cdp.pressKey(sid, "Backspace", "Backspace", 8);
|
|
33
|
-
await saveResults("pip-results-key-probe.json", R);
|
|
34
|
-
cdp.close();
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// Verify: do node-level event listeners survive adoption into a Document PiP window? Minimal, controlled.
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const R: Record<string, unknown> = {};
|
|
5
|
-
const h = await connectApp();
|
|
6
|
-
const { ev, cdp, sid } = h;
|
|
7
|
-
|
|
8
|
-
async function listenersOf(expr: string) {
|
|
9
|
-
const r = await cdp.send("Runtime.evaluate", { expression: expr }, sid);
|
|
10
|
-
if (!r.result?.objectId) return "no object";
|
|
11
|
-
const l = await cdp.send("DOMDebugger.getEventListeners", { objectId: r.result.objectId }, sid);
|
|
12
|
-
return (l.listeners as any[]).map((x) => `${x.type}${x.useCapture ? "(c)" : ""}`).sort();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Fresh probe nodes in the main doc with listeners; a plain iframe-less popup for comparison is not possible w/o gesture, so PiP only.
|
|
16
|
-
await ev(`(() => { const S = window.__spike; S.probeDiv = document.createElement('div'); S.probeTa = document.createElement('textarea'); S.probeDiv.append(S.probeTa);
|
|
17
|
-
S.probeHits = { div: 0, ta: 0 }; S.probeDiv.addEventListener('click', () => S.probeHits.div++, true); S.probeTa.addEventListener('keydown', () => S.probeHits.ta++); S.probeTa.addEventListener('input', () => S.probeHits.ta++, true);
|
|
18
|
-
document.body.append(S.probeDiv); return 'ok'; })()`);
|
|
19
|
-
R.before = { div: await listenersOf("window.__spike.probeDiv"), ta: await listenersOf("window.__spike.probeTa") };
|
|
20
|
-
|
|
21
|
-
// Also take the fresh terminal wrapper (tw2) along, checking its listeners right after adoption while PiP is open
|
|
22
|
-
R.tw2Before = { ta: await listenersOf("window.__spike.tw2.querySelector('.xterm-helper-textarea')"), xterm: await listenersOf("window.__spike.tw2.querySelector('.xterm')") };
|
|
23
|
-
|
|
24
|
-
R.move = await h.runGesture("pip-listener-test", `async () => {
|
|
25
|
-
const S = window.__spike; if (S.pip && !S.pip.closed) S.pip.close();
|
|
26
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 800, height: 500 }); S.pip = pip; S.copyStyles(pip.document);
|
|
27
|
-
pip.document.body.append(S.probeDiv);
|
|
28
|
-
const moved = S.moveToPip(pip, S.tw2);
|
|
29
|
-
await new Promise(r => setTimeout(r, 300));
|
|
30
|
-
// Fire synthetic events on the adopted probe nodes: do the listeners run?
|
|
31
|
-
S.probeHits = { div: 0, ta: 0 }; S.probeDiv.dispatchEvent(new MouseEvent('click', { bubbles: true })); S.probeTa.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true })); S.probeTa.dispatchEvent(new InputEvent('input', { data: 'a', inputType: 'insertText', bubbles: true }));
|
|
32
|
-
return { moved, probeDivDoc: S.probeDiv.ownerDocument === pip.document, hitsAfterAdoption: { ...S.probeHits } };
|
|
33
|
-
}`);
|
|
34
|
-
R.afterAdoption = { div: await listenersOf("window.__spike.probeDiv"), ta: await listenersOf("window.__spike.probeTa") };
|
|
35
|
-
R.tw2AfterAdoption = { ta: await listenersOf("window.__spike.tw2.querySelector('.xterm-helper-textarea')"), xterm: await listenersOf("window.__spike.tw2.querySelector('.xterm')") };
|
|
36
|
-
|
|
37
|
-
// Type into the adopted fresh terminal via its PiP target (trusted)
|
|
38
|
-
const pipSid = await h.attachPip();
|
|
39
|
-
if (pipSid) {
|
|
40
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, pipSid);
|
|
41
|
-
await cdp.eval(pipSid, `document.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
42
|
-
await ev(`window.__spike.dataLog2 = []; 'r'`);
|
|
43
|
-
await cdp.typeText(pipSid, "echo PIP-FRESH");
|
|
44
|
-
await cdp.pressKey(pipSid, "Enter", "Enter", 13, "\r");
|
|
45
|
-
await sleep(1200);
|
|
46
|
-
R.tw2TypingInPip = await ev(`({ dataLog: window.__spike.dataLog2.join(''), echoed: window.__spike.termText(window.__spike.t2, 60).includes('PIP-FRESH') })`);
|
|
47
|
-
}
|
|
48
|
-
// Move back while the PiP is still open, then check listeners again
|
|
49
|
-
R.back = await ev(`(() => { const S = window.__spike; const r = S.moveBack(S.tw2); document.body.append(S.probeDiv); S.probeHits = { div: 0, ta: 0 }; S.probeDiv.dispatchEvent(new MouseEvent('click', { bubbles: true })); S.probeTa.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true })); return { ...r, hitsAfterReturn: { ...S.probeHits } }; })()`);
|
|
50
|
-
R.afterReturn = { div: await listenersOf("window.__spike.probeDiv"), ta: await listenersOf("window.__spike.probeTa") };
|
|
51
|
-
R.tw2AfterReturn = { ta: await listenersOf("window.__spike.tw2.querySelector('.xterm-helper-textarea')") };
|
|
52
|
-
await ev(`window.__spike.pip.close(); 'c'`);
|
|
53
|
-
await saveResults("pip-results-listener-survival.json", R);
|
|
54
|
-
cdp.close();
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// Debug: compare registered DOM listeners on the round-tripped vs fresh xterm textarea (DOMDebugger.getEventListeners).
|
|
2
|
-
import { connectApp, saveResults } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const R: Record<string, unknown> = {};
|
|
5
|
-
const h = await connectApp();
|
|
6
|
-
const { cdp, sid } = h;
|
|
7
|
-
|
|
8
|
-
async function listeners(label: string, expr: string) {
|
|
9
|
-
const r = await cdp.send("Runtime.evaluate", { expression: expr }, sid);
|
|
10
|
-
if (!r.result?.objectId) { R[label] = "no object: " + JSON.stringify(r.result); return; }
|
|
11
|
-
const l = await cdp.send("DOMDebugger.getEventListeners", { objectId: r.result.objectId }, sid);
|
|
12
|
-
R[label] = (l.listeners as any[]).map((x) => `${x.type}${x.useCapture ? "(capture)" : ""}${x.passive ? "(passive)" : ""} @${x.lineNumber}:${x.columnNumber}`).sort();
|
|
13
|
-
}
|
|
14
|
-
await listeners("roundTrippedTextarea", `window.__spike.tw.querySelector('.xterm-helper-textarea')`);
|
|
15
|
-
await listeners("freshTextarea", `window.__spike.tw2.querySelector('.xterm-helper-textarea')`);
|
|
16
|
-
await listeners("roundTrippedXtermEl", `window.__spike.tw.querySelector('.xterm')`);
|
|
17
|
-
await listeners("freshXtermEl", `window.__spike.tw2.querySelector('.xterm')`);
|
|
18
|
-
await saveResults("pip-results-listeners.json", R);
|
|
19
|
-
cdp.close();
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
// Debug: does a synchronous moveBack inside the PiP 'pagehide' handler preserve listeners? Plus canvas inventory.
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const R: Record<string, unknown> = {};
|
|
5
|
-
const h = await connectApp();
|
|
6
|
-
const { ev, cdp, sid } = h;
|
|
7
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, sid);
|
|
8
|
-
|
|
9
|
-
async function listenersOf(expr: string) {
|
|
10
|
-
const r = await cdp.send("Runtime.evaluate", { expression: expr }, sid);
|
|
11
|
-
if (!r.result?.objectId) return "no object";
|
|
12
|
-
const l = await cdp.send("DOMDebugger.getEventListeners", { objectId: r.result.objectId }, sid);
|
|
13
|
-
return (l.listeners as any[]).map((x) => `${x.type}${x.useCapture ? "(c)" : ""}`).sort();
|
|
14
|
-
}
|
|
15
|
-
R.t2TextareaListenersNow = await listenersOf("window.__spike.tw2.querySelector('.xterm-helper-textarea')");
|
|
16
|
-
R.t2Canvases = await ev(`[...window.__spike.tw2.querySelectorAll('canvas')].map(c => ({ cls: c.className, parent: c.parentElement.className, w: c.width, h: c.height, gl: !!c.getContext('webgl2') }))`);
|
|
17
|
-
|
|
18
|
-
// Fresh terminal 3 for a clean pagehide test
|
|
19
|
-
await h.openViaUrl("/project/ppm/terminal/3");
|
|
20
|
-
const Q3 = `document.querySelector('[data-tab-pool-id^="terminal:3"]')`;
|
|
21
|
-
await h.waitFor(`${Q3} && ${Q3}.style.display !== 'none' && ${Q3}.querySelector('.xterm-helper-textarea')`, 15000, "terminal 3");
|
|
22
|
-
await sleep(2500);
|
|
23
|
-
await ev(`window.__spike.tw3 = ${Q3}; window.__spike.t3 = window.__spike.findTerminal(window.__spike.tw3); 'ok'`);
|
|
24
|
-
R.t3Before = await listenersOf("window.__spike.tw3.querySelector('.xterm-helper-textarea')");
|
|
25
|
-
async function typeMarker(marker: string) {
|
|
26
|
-
await ev(`window.__spike.tw3.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
27
|
-
await cdp.typeText(sid, `echo ${marker}`);
|
|
28
|
-
await cdp.pressKey(sid, "Enter", "Enter", 13, "\r");
|
|
29
|
-
await sleep(1300);
|
|
30
|
-
return ev(`window.__spike.termText(window.__spike.t3, 60).includes(${JSON.stringify(marker)})`);
|
|
31
|
-
}
|
|
32
|
-
R.t3Baseline = await typeMarker("T3-BASE");
|
|
33
|
-
|
|
34
|
-
// A: pagehide handler moves back synchronously, then pip.close()
|
|
35
|
-
R.pagehideA = await h.runGesture("pip-pagehide-a", `async () => {
|
|
36
|
-
const S = window.__spike; const w = S.tw3; S.phLog = [];
|
|
37
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 800, height: 500 }); S.pip = pip; S.copyStyles(pip.document); S.moveToPip(pip, w);
|
|
38
|
-
pip.addEventListener('pagehide', (e) => { S.phLog.push('pagehide owner=' + (w.ownerDocument === pip.document ? 'pip' : 'other') + ' persisted=' + e.persisted); S.moveBack(w); S.phLog.push('moved back, owner main=' + (w.ownerDocument === document)); });
|
|
39
|
-
pip.addEventListener('unload', () => S.phLog.push('unload'));
|
|
40
|
-
await new Promise(r => setTimeout(r, 400));
|
|
41
|
-
const beforeClose = { owner: w.ownerDocument === pip.document ? 'pip' : 'main' };
|
|
42
|
-
pip.close();
|
|
43
|
-
await new Promise(r => setTimeout(r, 400));
|
|
44
|
-
return { beforeClose, phLog: S.phLog, ownerAfter: w.ownerDocument === document ? 'main' : 'other', closed: pip.closed };
|
|
45
|
-
}`);
|
|
46
|
-
R.t3AfterA = await listenersOf("window.__spike.tw3.querySelector('.xterm-helper-textarea')");
|
|
47
|
-
R.t3TypingAfterA = await typeMarker("T3-AFTER-A");
|
|
48
|
-
|
|
49
|
-
// B: explicit moveBack BEFORE close (the known-good path) as control
|
|
50
|
-
R.controlB = await h.runGesture("pip-pagehide-b", `async () => {
|
|
51
|
-
const S = window.__spike; const w = S.tw3;
|
|
52
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 800, height: 500 }); S.pip = pip; S.copyStyles(pip.document); S.moveToPip(pip, w);
|
|
53
|
-
await new Promise(r => setTimeout(r, 400)); S.moveBack(w); pip.close(); await new Promise(r => setTimeout(r, 400));
|
|
54
|
-
return { ownerAfter: w.ownerDocument === document ? 'main' : 'other' };
|
|
55
|
-
}`);
|
|
56
|
-
R.t3AfterB = await listenersOf("window.__spike.tw3.querySelector('.xterm-helper-textarea')");
|
|
57
|
-
R.t3TypingAfterB = await typeMarker("T3-AFTER-B");
|
|
58
|
-
await saveResults("pip-results-pagehide.json", R);
|
|
59
|
-
cdp.close();
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
// Debug: ResizeObserver (created in main window) vs PiP window resizes; does pip 'resize' + manual fit() work?
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const R: Record<string, unknown> = {};
|
|
5
|
-
const h = await connectApp();
|
|
6
|
-
const { ev, cdp, sid } = h;
|
|
7
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, sid);
|
|
8
|
-
await h.openViaUrl("/project/ppm/terminal/3");
|
|
9
|
-
await sleep(500);
|
|
10
|
-
const W = `window.__spike.tw3`, T = `window.__spike.t3`;
|
|
11
|
-
|
|
12
|
-
R.move = await h.runGesture("pip-resize", `async () => {
|
|
13
|
-
const S = window.__spike; const w = ${W}; const t = ${T};
|
|
14
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 900, height: 600 }); S.pip = pip; S.copyStyles(pip.document); S.moveToPip(pip, w);
|
|
15
|
-
S.roLog = []; const ro = new ResizeObserver(es => S.roLog.push('main-RO ' + Math.round(es[0].contentRect.width) + 'x' + Math.round(es[0].contentRect.height))); ro.observe(w);
|
|
16
|
-
const pipRo = new pip.ResizeObserver(es => S.roLog.push('pip-RO ' + Math.round(es[0].contentRect.width) + 'x' + Math.round(es[0].contentRect.height))); pipRo.observe(w);
|
|
17
|
-
pip.addEventListener('resize', () => S.roLog.push('pip-resize-event ' + pip.innerWidth + 'x' + pip.innerHeight));
|
|
18
|
-
await new Promise(r => setTimeout(r, 500));
|
|
19
|
-
return { pipSize: [pip.innerWidth, pip.innerHeight], rowsCols: [t.rows, t.cols], roLog: S.roLog.slice() };
|
|
20
|
-
}`);
|
|
21
|
-
const pipSid = await h.attachPip();
|
|
22
|
-
await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true }, pipSid!);
|
|
23
|
-
// resizeTo inside a trusted click in the PiP
|
|
24
|
-
const pt = await cdp.eval<any>(pipSid!, `(() => { const b = document.createElement('button'); b.id = 'rz'; b.textContent = 'rz'; b.style.cssText = 'position:fixed;right:4px;bottom:4px;z-index:99999'; b.onclick = () => window.resizeTo(700, 450); document.body.append(b); const r = b.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + r.height / 2 }; })()`);
|
|
25
|
-
await ev(`window.__spike.roLog = []; 'r'`);
|
|
26
|
-
await cdp.click(pipSid!, pt.x, pt.y);
|
|
27
|
-
const samples: unknown[] = [];
|
|
28
|
-
for (const ms of [300, 1000, 3000]) {
|
|
29
|
-
await sleep(ms);
|
|
30
|
-
samples.push(await ev(`({ afterMs: ${ms}, pip: [window.__spike.pip.innerWidth, window.__spike.pip.innerHeight], wrapper: [${W}.offsetWidth, ${W}.offsetHeight], rowsCols: [${T}.rows, ${T}.cols], roLog: window.__spike.roLog.slice() })`));
|
|
31
|
-
}
|
|
32
|
-
R.afterResizeTo = samples;
|
|
33
|
-
// Nudge: force a main-document rendering update; does the main RO now deliver?
|
|
34
|
-
R.afterMainNudge = await ev(`(async () => { document.body.style.outline = '1px solid transparent'; document.body.offsetWidth; await new Promise(r => setTimeout(r, 600)); document.body.style.outline = ''; return { rowsCols: [${T}.rows, ${T}.cols], roLog: window.__spike.roLog.slice() }; })()`);
|
|
35
|
-
// Glue candidate: manual fit via the FitAddon reached through the fiber (fitRef) — or xterm's proposeDimensions
|
|
36
|
-
R.manualFit = await ev(`(() => { const S = window.__spike; const t = ${T}; const w = ${W}; let fit = null; let f = S.fiberOf(w.querySelector('.xterm').parentElement);
|
|
37
|
-
for (let d = 0; f && d < 40 && !fit; d++, f = f.return) { let hk = f.memoizedState; for (let i = 0; hk && i < 200; i++, hk = hk.next) { const c = hk.memoizedState && hk.memoizedState.current; if (c && typeof c.fit === 'function' && typeof c.proposeDimensions === 'function') { fit = c; break; } } }
|
|
38
|
-
if (!fit) return 'no fitAddon'; const before = [t.rows, t.cols]; const proposed = fit.proposeDimensions(); fit.fit(); return { before, proposed, after: [t.rows, t.cols] }; })()`);
|
|
39
|
-
await h.pipShot(pipSid, "spike-01e-terminal-in-pip-after-manual-fit.png");
|
|
40
|
-
await ev(`(() => { const S = window.__spike; S.moveBack(${W}); S.pip.close(); return 'done'; })()`);
|
|
41
|
-
await saveResults("pip-results-resize.json", R);
|
|
42
|
-
cdp.close();
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// Stage 1: authenticate the spike Chrome, open terminal + editor tabs, verify instances reachable.
|
|
2
|
-
import { Database } from "bun:sqlite";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { CdpClient, sleep } from "./cdp-client";
|
|
6
|
-
|
|
7
|
-
const PORT = 9333;
|
|
8
|
-
const APP = "http://localhost:5173";
|
|
9
|
-
const helpers = await Bun.file(join(import.meta.dir, "pip-page-helpers.js")).text();
|
|
10
|
-
|
|
11
|
-
const db = new Database(join(homedir(), ".ppm", "ppm.dev.db"), { readonly: true });
|
|
12
|
-
const token = JSON.parse((db.query("SELECT value FROM config WHERE key='auth'").get() as any).value).token as string;
|
|
13
|
-
|
|
14
|
-
const cdp = await CdpClient.connect(PORT);
|
|
15
|
-
const page = (await cdp.targets()).find((t) => t.type === "page" && t.url.startsWith(APP));
|
|
16
|
-
if (!page) throw new Error("no app page target: " + JSON.stringify(await cdp.targets()));
|
|
17
|
-
const sid = await cdp.attach(page.targetId);
|
|
18
|
-
await cdp.send("Page.enable", {}, sid);
|
|
19
|
-
await cdp.send("Runtime.enable", {}, sid);
|
|
20
|
-
|
|
21
|
-
async function waitFor(expr: string, timeoutMs = 20000, label = expr) {
|
|
22
|
-
const t0 = Date.now();
|
|
23
|
-
while (Date.now() - t0 < timeoutMs) {
|
|
24
|
-
if (await cdp.eval<boolean>(sid, `!!(${expr})`)) return;
|
|
25
|
-
await sleep(250);
|
|
26
|
-
}
|
|
27
|
-
throw new Error(`timeout waiting: ${label}`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Auth
|
|
31
|
-
const hasToken = await cdp.eval<boolean>(sid, `localStorage.getItem('ppm-auth-token') === ${JSON.stringify(token)}`);
|
|
32
|
-
if (!hasToken) {
|
|
33
|
-
await cdp.eval(sid, `localStorage.setItem('ppm-auth-token', ${JSON.stringify(token)}); 'set'`);
|
|
34
|
-
await cdp.send("Page.navigate", { url: `${APP}/project/ppm` }, sid);
|
|
35
|
-
await sleep(1500);
|
|
36
|
-
}
|
|
37
|
-
await waitFor(`document.querySelector('[data-panel-drop-zone]')`, 30000, "app shell");
|
|
38
|
-
console.log("app shell ready, url:", await cdp.eval(sid, "location.href"));
|
|
39
|
-
|
|
40
|
-
// Open terminal via URL routing (pushState + popstate → autoOpenFromUrl)
|
|
41
|
-
async function openViaUrl(path: string) {
|
|
42
|
-
await cdp.eval(sid, `history.pushState(null, '', ${JSON.stringify(path)}); dispatchEvent(new PopStateEvent('popstate')); 'ok'`);
|
|
43
|
-
}
|
|
44
|
-
await openViaUrl("/project/ppm/terminal/1");
|
|
45
|
-
await waitFor(`document.querySelector('[data-tab-pool-id^="terminal"] .xterm-helper-textarea')`, 20000, "xterm");
|
|
46
|
-
await openViaUrl("/project/ppm/editor/.spike/scratch-editor-target.ts");
|
|
47
|
-
await waitFor(`document.querySelector('[data-tab-pool-id^="editor"] .monaco-editor .view-lines')`, 30000, "monaco");
|
|
48
|
-
|
|
49
|
-
await cdp.eval(sid, helpers);
|
|
50
|
-
const info = await cdp.eval(sid, `(() => {
|
|
51
|
-
const S = window.__spike;
|
|
52
|
-
const tw = document.querySelector('[data-tab-pool-id^="terminal"]');
|
|
53
|
-
const ew = document.querySelector('[data-tab-pool-id^="editor"]');
|
|
54
|
-
const term = S.findTerminal(tw); const ed = S.findEditor(ew);
|
|
55
|
-
return {
|
|
56
|
-
wrappers: [...document.querySelectorAll('[data-tab-pool-id]')].map(w => ({ id: w.dataset.tabPoolId, display: w.style.display, parent: w.parentElement?.className })),
|
|
57
|
-
termFound: !!term, termSize: term && { rows: term.rows, cols: term.cols }, termTail: term && S.termText(term, 5),
|
|
58
|
-
editorFound: !!ed, editorLine1: ed && ed.getModel().getLineContent(1), layout: ed && ed.getLayoutInfo().width,
|
|
59
|
-
htmlClass: document.documentElement.className, sheets: document.styleSheets.length, monacoGlobal: !!window.monaco,
|
|
60
|
-
};
|
|
61
|
-
})()`);
|
|
62
|
-
console.log(JSON.stringify(info, null, 2));
|
|
63
|
-
cdp.close();
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// Debug stage: why does typing stop echoing after the PiP move, and does the DOM renderer survive?
|
|
2
|
-
import { connectApp, saveResults, sleep } from "./pip-spike-common";
|
|
3
|
-
|
|
4
|
-
const TAB = "terminal:1@panel-8281c2e3";
|
|
5
|
-
const Q = `document.querySelector('[data-tab-pool-id="${TAB}"]')`;
|
|
6
|
-
const W = `window.__spike.tw`;
|
|
7
|
-
const T = `window.__spike.findTerminal(${W})`;
|
|
8
|
-
const R: Record<string, unknown> = {};
|
|
9
|
-
const h = await connectApp();
|
|
10
|
-
const { ev, cdp, sid } = h;
|
|
11
|
-
|
|
12
|
-
R.reset = await ev(`(() => { const S = window.__spike; const r = { hadPip: !!(S.pip && !S.pip.closed), twInPip: !!(S.tw && S.tw.ownerDocument !== document) };
|
|
13
|
-
if (r.twInPip) S.moveBack(S.tw); if (r.hadPip) S.pip.close(); S.pip = null; S.results = {}; return r; })()`);
|
|
14
|
-
await sleep(500);
|
|
15
|
-
await h.openViaUrl("/project/ppm/terminal/1");
|
|
16
|
-
await h.waitFor(`${Q} && ${Q}.style.display !== 'none' && ${Q}.querySelector('.xterm-helper-textarea')`, 15000, "terminal visible");
|
|
17
|
-
await ev(`window.__spike.tw = ${Q}; 'held'`);
|
|
18
|
-
|
|
19
|
-
// Instrument: xterm onData, WebSocket ref (fiber hook whose .current is a WebSocket), ws messages
|
|
20
|
-
R.instrument = await ev(`(() => {
|
|
21
|
-
const S = window.__spike; const w = ${W}; const t = ${T};
|
|
22
|
-
S.dataLog = []; S.wsMsgs = 0; S.writes = 0;
|
|
23
|
-
if (!S.instrumented) {
|
|
24
|
-
t.onData(d => S.dataLog.push(d));
|
|
25
|
-
const origWrite = t.write.bind(t); t.write = (d, cb) => { S.writes++; return origWrite(d, cb); };
|
|
26
|
-
const container = w.querySelector('.xterm').parentElement; let f = S.fiberOf(container); let ws = null;
|
|
27
|
-
for (let depth = 0; f && depth < 40 && !ws; depth++, f = f.return) {
|
|
28
|
-
let hook = f.memoizedState;
|
|
29
|
-
for (let i = 0; hook && i < 200; i++, hook = hook.next) { const c = hook.memoizedState && hook.memoizedState.current; if (c instanceof WebSocket) { ws = c; S.wsHook = hook.memoizedState; break; } }
|
|
30
|
-
}
|
|
31
|
-
if (ws) ws.addEventListener('message', () => S.wsMsgs++);
|
|
32
|
-
S.instrumented = true;
|
|
33
|
-
}
|
|
34
|
-
const ws = S.wsHook && S.wsHook.current;
|
|
35
|
-
return { wsFound: !!ws, wsState: ws && ws.readyState, wsUrl: ws && ws.url, rows: t.rows, cols: t.cols, textareaDoc: w.querySelector('.xterm-helper-textarea').ownerDocument === document };
|
|
36
|
-
})()`);
|
|
37
|
-
|
|
38
|
-
async function typeAndCheck(label: string, marker: string, targetSid: string, focusExpr: string) {
|
|
39
|
-
await cdp.eval(targetSid, focusExpr);
|
|
40
|
-
await ev(`window.__spike.dataLog = []; window.__spike.wsMsgs = 0; window.__spike.writes = 0; 'r'`);
|
|
41
|
-
await cdp.typeText(targetSid, `echo ${marker}`);
|
|
42
|
-
await cdp.pressKey(targetSid, "Enter", "Enter", 13, "\r");
|
|
43
|
-
await sleep(1500);
|
|
44
|
-
R[label] = await ev(`(() => { const S = window.__spike; const t = ${T}; const ws = S.wsHook && S.wsHook.current;
|
|
45
|
-
return { dataSent: S.dataLog.join(''), wsMsgs: S.wsMsgs, writes: S.writes, wsState: ws && ws.readyState, echoed: S.termText(t, 80).includes(${JSON.stringify(marker)}), tail: S.termText(t, 4) }; })()`);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
await typeAndCheck("mainBeforePip", "DBG-MAIN-1", sid, `window.__spike.tw.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
49
|
-
|
|
50
|
-
R.move = await h.runGesture("pip-terminal-dbg", `async () => {
|
|
51
|
-
const S = window.__spike; const w = ${W};
|
|
52
|
-
const pip = await documentPictureInPicture.requestWindow({ width: 900, height: 600 });
|
|
53
|
-
S.pip = pip; S.copyStyles(pip.document); const moved = S.moveToPip(pip, w);
|
|
54
|
-
await new Promise(r => setTimeout(r, 400));
|
|
55
|
-
const gl = [...w.querySelectorAll('canvas')].map(c => { const ctx = c.getContext('webgl2') || c.getContext('webgl'); return ctx ? { lost: ctx.isContextLost(), dbw: ctx.drawingBufferWidth, dbh: ctx.drawingBufferHeight } : { ctx2d: !!c.getContext('2d') }; });
|
|
56
|
-
return { moved, gl, pipSize: [pip.innerWidth, pip.innerHeight], screenBg: pip.getComputedStyle(w.querySelector('.xterm-viewport')).backgroundColor };
|
|
57
|
-
}`);
|
|
58
|
-
const pipSid = await h.attachPip();
|
|
59
|
-
R.pipAttached = !!pipSid;
|
|
60
|
-
if (pipSid) {
|
|
61
|
-
R.pipFocus = await cdp.eval(pipSid, `(() => { const ta = document.querySelector('.xterm-helper-textarea'); ta.focus(); return { active: document.activeElement === ta, hasFocus: document.hasFocus(), vis: document.visibilityState }; })()`);
|
|
62
|
-
await typeAndCheck("pipTrusted", "DBG-PIP-1", pipSid, `document.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
63
|
-
R.shotWebgl = await h.pipShot(pipSid, "spike-01c-terminal-pip-webgl-debug.png");
|
|
64
|
-
// Force GL context loss → app's onContextLoss disposes the WebGL addon → DOM renderer
|
|
65
|
-
R.forceLoss = await ev(`(async () => { const w = ${W}; const t = ${T}; const c = w.querySelector('canvas'); const gl = c && (c.getContext('webgl2') || c.getContext('webgl'));
|
|
66
|
-
if (!gl) return 'no gl canvas'; const ext = gl.getExtension('WEBGL_lose_context'); if (!ext) return 'no lose_context ext'; ext.loseContext();
|
|
67
|
-
await new Promise(r => setTimeout(r, 800)); t.refresh(0, t.rows - 1); await new Promise(r => setTimeout(r, 300));
|
|
68
|
-
return { canvasesLeft: w.querySelectorAll('canvas').length, rowsDom: w.querySelectorAll('.xterm-rows > div').length, rowsText: [...w.querySelectorAll('.xterm-rows > div')].slice(0, 3).map(r => r.textContent.trim()).filter(Boolean) }; })()`);
|
|
69
|
-
await sleep(500);
|
|
70
|
-
R.shotDom = await h.pipShot(pipSid, "spike-01d-terminal-pip-dom-renderer.png");
|
|
71
|
-
await typeAndCheck("pipTrustedAfterDomRenderer", "DBG-PIP-2", pipSid, `document.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
72
|
-
// Resize check with DOM renderer
|
|
73
|
-
R.resizeDom = await ev(`(async () => { const w = ${W}; const t = ${T}; const b = [t.rows, t.cols]; w.style.height = '50%'; await new Promise(r => setTimeout(r, 700)); const a = [t.rows, t.cols]; w.style.height = '100%'; await new Promise(r => setTimeout(r, 700)); return { before: b, after: a, restored: [t.rows, t.cols] }; })()`);
|
|
74
|
-
}
|
|
75
|
-
R.back = await ev(`(async () => { const S = window.__spike; const w = ${W}; const t = ${T}; const r = S.moveBack(w); await new Promise(res => setTimeout(res, 800)); return { ...r, rows: t.rows, cols: t.cols }; })()`);
|
|
76
|
-
await ev(`window.__spike.pip.close(); 'c'`);
|
|
77
|
-
await sleep(400);
|
|
78
|
-
await typeAndCheck("mainAfterReturn", "DBG-MAIN-2", sid, `window.__spike.tw.querySelector('.xterm-helper-textarea').focus(); 'f'`);
|
|
79
|
-
await cdp.screenshot(sid, "C:/Users/PC/ppm/plans/reports/screenshots/spike-03b-terminal-after-return-dom-renderer.png");
|
|
80
|
-
await saveResults("pip-results-terminal-debug.json", R);
|
|
81
|
-
cdp.close();
|