@pasko70/pibo 1.9.2 → 1.9.4
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/README.md +183 -183
- package/context/codex-base-prompt.md +148 -148
- package/context/compute-worker.md +23 -23
- package/context/pibo-compaction-prompt.md +100 -100
- package/context/pibo-native-tooling.md +18 -18
- package/context/pibo-system-prompt.md +77 -77
- package/dist/apps/chat/agent-store.js +248 -248
- package/dist/apps/chat/data/project-service.js +168 -168
- package/dist/apps/chat/data/read-state-service.js +18 -18
- package/dist/apps/chat/data/session-query-service.js +25 -25
- package/dist/apps/chat/data/timeline-query-service.js +14 -14
- package/dist/apps/chat/static-assets.js +854 -854
- package/dist/apps/chat/workflow-persistence.js +255 -255
- package/dist/apps/chat-ui/assets/{dist-CjYtD7ZT.js → dist-9a5adSX0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-9lsp1UpA.js → dist-BJftG-mN.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-2KdPXbMT.js → dist-BmIDFlGO.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BWbWIOcD.js → dist-CCNYUBAQ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Cge8JklW.js → dist-Cmr0ZAV7.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DFhhiR8M.js → dist-Crc-B1sZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-5GM30SQK.js → dist-DHPwmWcR.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-C-5u2QIS.js → dist-U3lqQ0aI.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Idz5kzy8.js → dist-_poCjCWK.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Etxmpyxg.js → dist-ffW7mw39.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BYKMZlI0.js → dist-w2d_3LSu.js} +1 -1
- package/dist/apps/chat-ui/assets/{index-BqD_bm7z.js → index-rXMhSu4U.js} +9 -9
- package/dist/apps/chat-ui/index.html +17 -17
- package/dist/apps/chat-ui/manifest.webmanifest +25 -25
- package/dist/apps/chat-ui/sw.js +47 -47
- package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +41 -0
- package/dist/apps/chat-vscode-web/index.html +13 -13
- package/dist/apps/cli-ui/cliSessionsCommand.js +23 -23
- package/dist/apps/context-files-ui/index.html +11 -11
- package/dist/cli.js +39 -39
- package/dist/compute/cli.js +54 -54
- package/dist/core/session-router.js +5 -2
- package/dist/cron/cli.js +15 -15
- package/dist/cron/store.js +49 -49
- package/dist/data/cli.js +23 -23
- package/dist/data/event-log.js +23 -23
- package/dist/data/message-store.js +20 -20
- package/dist/data/navigation-store.js +9 -9
- package/dist/data/observation-store.js +4 -4
- package/dist/data/payload-store.js +17 -17
- package/dist/data/schema.js +431 -431
- package/dist/data/session-store.js +4 -4
- package/dist/data/telemetry-queries.js +54 -54
- package/dist/data/telemetry.js +197 -197
- package/dist/debug/events.js +12 -12
- package/dist/debug/failures.js +6 -6
- package/dist/debug/index.js +227 -227
- package/dist/debug/messages.js +6 -6
- package/dist/debug/pty.js +124 -124
- package/dist/debug/session.js +29 -29
- package/dist/debug/tools.js +5 -5
- package/dist/debug/web-snapshot-browser-scripts.js +294 -294
- package/dist/debug/web-streaming-browser-library.js +925 -925
- package/dist/debug/web-streaming-browser-scripts.js +232 -232
- package/dist/debug/web-streaming-provider-telemetry.js +4 -4
- package/dist/debug/web.js +93 -93
- package/dist/gateway/cli.js +19 -19
- package/dist/mcp/config-command.js +53 -53
- package/dist/mcp/index.js +21 -21
- package/dist/mcp/registry.js +11 -11
- package/dist/pi-packages/cli.js +11 -11
- package/dist/plugins/context-files-store.js +110 -110
- package/dist/plugins/context-files.js +4 -4
- package/dist/ralph/cli.js +18 -18
- package/dist/ralph/templates.js +140 -140
- package/dist/reliability/store.js +226 -226
- package/dist/session-ui/sessionActivity.js +30 -0
- package/dist/sessions/pibo-data-store.js +16 -16
- package/dist/sessions/sqlite-store.js +53 -53
- package/dist/setup/cli.js +58 -58
- package/dist/signals/projector.js +26 -11
- package/dist/signals/registry.js +41 -0
- package/dist/tools/agent-browser-wrapper.js +80 -80
- package/dist/tools/browser-use-cdp.js +12 -12
- package/dist/tools/browser-use-wrapper.js +762 -762
- package/dist/tools/guides.js +538 -538
- package/dist/tools/index.js +99 -99
- package/dist/tools/runtime/node-worker-source.js +205 -205
- package/dist/tools/runtime/python-worker-source.js +177 -177
- package/dist/vscode/cli.js +9 -9
- package/dist/web-annotations/cdp.js +900 -900
- package/dist/web-annotations/store.js +96 -96
- package/docs/README.md +23 -23
- package/docs/ops/install-developer-host.md +112 -112
- package/docs/ops/install-user-host.md +96 -96
- package/docs/ops/upgrade-user-to-developer-host.md +69 -69
- package/docs/ops/vscode-extension-release.md +160 -160
- package/package.json +95 -95
- package/skills/builtin/graphify/SKILL.md +52 -52
- package/skills/builtin/pi-agent-harness/SKILL.md +319 -319
- package/skills/builtin/pi-agent-harness/agents/openai.yaml +4 -4
- package/skills/builtin/pibo-docker-system/SKILL.md +170 -170
- package/skills/builtin/pibo-spec-writing/SKILL.md +330 -330
- package/skills/builtin/prd/SKILL.md +143 -143
- package/skills/builtin/ralph-loop/SKILL.md +359 -359
- package/skills/builtin/ralph-prd-json/SKILL.md +123 -123
- package/skills/builtin/skill-creator/LICENSE.txt +201 -201
- package/skills/builtin/skill-creator/SKILL.md +513 -513
- package/skills/builtin/skill-creator/agents/analyzer.md +274 -274
- package/skills/builtin/skill-creator/agents/comparator.md +202 -202
- package/skills/builtin/skill-creator/agents/grader.md +223 -223
- package/skills/builtin/skill-creator/assets/eval_review.html +146 -146
- package/skills/builtin/skill-creator/eval-viewer/generate_review.py +471 -471
- package/skills/builtin/skill-creator/eval-viewer/viewer.html +1325 -1325
- package/skills/builtin/skill-creator/references/schemas.md +430 -430
- package/skills/builtin/skill-creator/scripts/aggregate_benchmark.py +401 -401
- package/skills/builtin/skill-creator/scripts/generate_report.py +326 -326
- package/skills/builtin/skill-creator/scripts/improve_description.py +247 -247
- package/skills/builtin/skill-creator/scripts/package_skill.py +136 -136
- package/skills/builtin/skill-creator/scripts/quick_validate.py +102 -102
- package/skills/builtin/skill-creator/scripts/run_eval.py +310 -310
- package/skills/builtin/skill-creator/scripts/run_loop.py +328 -328
- package/skills/builtin/skill-creator/scripts/utils.py +47 -47
- package/skills/builtin/web-annotations/SKILL.md +93 -93
- package/src/mcp/LICENSE.mcp-cli +21 -21
- package/dist/apps/chat-vscode-web/assets/index-Cst9OUkC.js +0 -41
- package/dist/session-ui/activeTurn.js +0 -126
|
@@ -1,303 +1,303 @@
|
|
|
1
1
|
export function buildSnapshotExpression(options) {
|
|
2
|
-
return `(() => {
|
|
3
|
-
const options = ${JSON.stringify(options)};
|
|
4
|
-
${browserSnapshotLibrary()}
|
|
5
|
-
return captureSnapshot(options);
|
|
2
|
+
return `(() => {
|
|
3
|
+
const options = ${JSON.stringify(options)};
|
|
4
|
+
${browserSnapshotLibrary()}
|
|
5
|
+
return captureSnapshot(options);
|
|
6
6
|
})()`;
|
|
7
7
|
}
|
|
8
8
|
export function buildWatchExpression(options) {
|
|
9
|
-
return `(async () => {
|
|
10
|
-
const options = ${JSON.stringify(options)};
|
|
11
|
-
${browserSnapshotLibrary()}
|
|
12
|
-
return await runWatch(options);
|
|
9
|
+
return `(async () => {
|
|
10
|
+
const options = ${JSON.stringify(options)};
|
|
11
|
+
${browserSnapshotLibrary()}
|
|
12
|
+
return await runWatch(options);
|
|
13
13
|
})()`;
|
|
14
14
|
}
|
|
15
15
|
function browserSnapshotLibrary() {
|
|
16
|
-
return String.raw `
|
|
17
|
-
function nowIso() { return new Date().toISOString(); }
|
|
18
|
-
function safeString(value) { return typeof value === 'string' ? value : ''; }
|
|
19
|
-
function short(value, limit) {
|
|
20
|
-
const text = safeString(value).replace(/\s+/g, ' ').trim();
|
|
21
|
-
return text.length > limit ? text.slice(0, Math.max(0, limit - 1)) + '…' : text;
|
|
22
|
-
}
|
|
23
|
-
function redactText(element, options) {
|
|
24
|
-
const tag = element.tagName.toLowerCase();
|
|
25
|
-
const debug = element.getAttribute('data-pibo-debug') || '';
|
|
26
|
-
if (tag === 'textarea' || tag === 'input' || debug === 'composer') {
|
|
27
|
-
const value = 'value' in element ? String(element.value || '') : '';
|
|
28
|
-
return value ? '[redacted:' + value.length + ' chars]' : '';
|
|
29
|
-
}
|
|
30
|
-
const text = element.innerText || element.textContent || '';
|
|
31
|
-
if (!options.includeText && /message|trace|terminal|composer/i.test(debug)) return text ? '[redacted]' : '';
|
|
32
|
-
return short(text, options.textLimit);
|
|
33
|
-
}
|
|
34
|
-
function classSummary(element) {
|
|
35
|
-
const value = safeString(element.getAttribute('class'));
|
|
36
|
-
if (!value) return undefined;
|
|
37
|
-
const parts = value.split(/\s+/).filter(Boolean);
|
|
38
|
-
const useful = parts.filter((part) => /selected|active|hidden|opacity|translate|animate|border|bg-|text-|ring|disabled|pointer|sr-only/.test(part));
|
|
39
|
-
return (useful.length ? useful : parts.slice(0, 6)).slice(0, 10).join(' ');
|
|
40
|
-
}
|
|
41
|
-
function attrMap(element) {
|
|
42
|
-
const attrs = {};
|
|
43
|
-
const allow = /^(id|role|aria-|data-pibo-|data-testid$|disabled$|checked$|selected$|hidden$|tabindex$|title$)/;
|
|
44
|
-
for (const attr of Array.from(element.attributes || [])) {
|
|
45
|
-
if (!allow.test(attr.name)) continue;
|
|
46
|
-
if (/token|cookie|authorization|secret|password/i.test(attr.name)) {
|
|
47
|
-
attrs[attr.name] = '[redacted]';
|
|
48
|
-
} else {
|
|
49
|
-
attrs[attr.name] = short(attr.value, 120);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
53
|
-
attrs.value = element.value ? '[redacted:' + element.value.length + ' chars]' : '';
|
|
54
|
-
attrs.disabled = Boolean(element.disabled);
|
|
55
|
-
}
|
|
56
|
-
return attrs;
|
|
57
|
-
}
|
|
58
|
-
function roleOf(element) {
|
|
59
|
-
const explicit = element.getAttribute('role');
|
|
60
|
-
if (explicit) return explicit;
|
|
61
|
-
const tag = element.tagName.toLowerCase();
|
|
62
|
-
if (tag === 'button') return 'button';
|
|
63
|
-
if (tag === 'a') return 'link';
|
|
64
|
-
if (tag === 'textarea') return 'textbox';
|
|
65
|
-
if (tag === 'input') return 'input';
|
|
66
|
-
if (tag === 'select') return 'combobox';
|
|
67
|
-
if (tag === 'main') return 'main';
|
|
68
|
-
if (tag === 'aside') return 'complementary';
|
|
69
|
-
if (tag === 'nav') return 'navigation';
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
function nameOf(element, options) {
|
|
73
|
-
const aria = element.getAttribute('aria-label');
|
|
74
|
-
if (aria) return short(aria, options.textLimit);
|
|
75
|
-
const title = element.getAttribute('title');
|
|
76
|
-
if (title) return short(title, options.textLimit);
|
|
77
|
-
const tag = element.tagName.toLowerCase();
|
|
78
|
-
if (tag === 'input' || tag === 'textarea') return short(element.getAttribute('placeholder') || '', options.textLimit);
|
|
79
|
-
const debug = element.getAttribute('data-pibo-debug');
|
|
80
|
-
if (debug === 'session-row') return short(element.getAttribute('data-pibo-title') || element.innerText || '', options.textLimit);
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
function elementPath(element) {
|
|
84
|
-
const parts = [];
|
|
85
|
-
let current = element;
|
|
86
|
-
while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.documentElement) {
|
|
87
|
-
const tag = current.tagName.toLowerCase();
|
|
88
|
-
const parent = current.parentElement;
|
|
89
|
-
if (!parent) {
|
|
90
|
-
parts.unshift(tag);
|
|
91
|
-
break;
|
|
92
|
-
}
|
|
93
|
-
const index = Array.from(parent.children).filter((child) => child.tagName === current.tagName).indexOf(current) + 1;
|
|
94
|
-
parts.unshift(tag + ':nth-of-type(' + index + ')');
|
|
95
|
-
current = parent;
|
|
96
|
-
}
|
|
97
|
-
return parts.join('>');
|
|
98
|
-
}
|
|
99
|
-
function identityOf(element) {
|
|
100
|
-
const debug = element.getAttribute('data-pibo-debug');
|
|
101
|
-
const session = element.getAttribute('data-pibo-session-id');
|
|
102
|
-
const room = element.getAttribute('data-pibo-room-id');
|
|
103
|
-
const view = element.getAttribute('data-pibo-view-id');
|
|
104
|
-
const testId = element.getAttribute('data-testid');
|
|
105
|
-
const id = element.id;
|
|
106
|
-
if (debug && session) return { identity: debug + ':' + session, kind: 'pibo-session' };
|
|
107
|
-
if (debug && room) return { identity: debug + ':' + room, kind: 'pibo-room' };
|
|
108
|
-
if (debug && view) return { identity: debug + ':' + view, kind: 'pibo-view' };
|
|
109
|
-
if (debug) return { identity: debug, kind: 'pibo-debug' };
|
|
110
|
-
if (testId) return { identity: 'testid:' + testId, kind: 'testid' };
|
|
111
|
-
if (id) return { identity: 'id:' + id, kind: 'id' };
|
|
112
|
-
const role = roleOf(element);
|
|
113
|
-
const name = nameOf(element, { textLimit: 40 }) || '';
|
|
114
|
-
if (role && name) return { identity: role + ':' + name, kind: 'role-name' };
|
|
115
|
-
return { identity: 'path:' + elementPath(element), kind: 'path' };
|
|
116
|
-
}
|
|
117
|
-
function boxOf(element) {
|
|
118
|
-
const rect = element.getBoundingClientRect();
|
|
119
|
-
return { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) };
|
|
120
|
-
}
|
|
121
|
-
function isImportantElement(element, depth) {
|
|
122
|
-
if (!(element instanceof Element)) return false;
|
|
123
|
-
const tag = element.tagName.toLowerCase();
|
|
124
|
-
if (depth === 0) return true;
|
|
125
|
-
if (['script', 'style', 'svg', 'path', 'rect', 'circle', 'line', 'polyline', 'polygon'].includes(tag)) return false;
|
|
126
|
-
if (element.hasAttribute('data-pibo-debug') || element.hasAttribute('data-pibo-session-id') || element.hasAttribute('data-testid')) return true;
|
|
127
|
-
if (element.hasAttribute('aria-label') || element.hasAttribute('title') || element.hasAttribute('role')) return true;
|
|
128
|
-
if (['button', 'a', 'input', 'textarea', 'select', 'option', 'main', 'aside', 'nav'].includes(tag)) return true;
|
|
129
|
-
if (element === document.activeElement) return true;
|
|
130
|
-
if (element.getAttribute('aria-selected') === 'true' || element.getAttribute('data-pibo-selected') === 'true' || element.hasAttribute('hidden')) return true;
|
|
131
|
-
const text = (element.innerText || element.textContent || '').replace(/\s+/g, ' ').trim();
|
|
132
|
-
if (text && element.children.length === 0 && depth <= 4) return true;
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
function summarizeElement(element, depth, ref, options) {
|
|
136
|
-
const ident = identityOf(element);
|
|
137
|
-
const node = {
|
|
138
|
-
ref,
|
|
139
|
-
identity: ident.identity,
|
|
140
|
-
identityKind: ident.kind,
|
|
141
|
-
depth,
|
|
142
|
-
tag: element.tagName.toLowerCase(),
|
|
143
|
-
attributes: attrMap(element),
|
|
144
|
-
path: elementPath(element),
|
|
145
|
-
};
|
|
146
|
-
const role = roleOf(element); if (role) node.role = role;
|
|
147
|
-
const name = nameOf(element, options); if (name) node.name = name;
|
|
148
|
-
const text = redactText(element, options); if (text) node.text = text;
|
|
149
|
-
const classes = classSummary(element); if (classes) node.classSummary = classes;
|
|
150
|
-
if (document.activeElement === element) node.focused = true;
|
|
151
|
-
if (options.includeLayout) node.box = boxOf(element);
|
|
152
|
-
return node;
|
|
153
|
-
}
|
|
154
|
-
function captureSnapshot(options) {
|
|
155
|
-
const root = document.querySelector(options.scope);
|
|
156
|
-
const nodes = [];
|
|
157
|
-
const omitted = { nodes: 0, depth: 0, budget: false };
|
|
158
|
-
let refSeq = 0;
|
|
159
|
-
function walk(element, depth) {
|
|
160
|
-
if (!(element instanceof Element)) return;
|
|
161
|
-
if (depth > options.maxDepth) { omitted.depth += 1; return; }
|
|
162
|
-
if (isImportantElement(element, depth)) {
|
|
163
|
-
if (nodes.length >= options.maxNodes) { omitted.nodes += 1; omitted.budget = true; return; }
|
|
164
|
-
const node = summarizeElement(element, depth, '@n' + (++refSeq), options);
|
|
165
|
-
nodes.push(node);
|
|
166
|
-
}
|
|
167
|
-
for (const child of Array.from(element.children)) walk(child, depth + 1);
|
|
168
|
-
}
|
|
169
|
-
if (root) walk(root, 0);
|
|
170
|
-
const active = document.activeElement instanceof Element ? summarizeElement(document.activeElement, 0, '@focus', options) : undefined;
|
|
171
|
-
return {
|
|
172
|
-
kind: 'snapshot',
|
|
173
|
-
createdAt: nowIso(),
|
|
174
|
-
url: location.href,
|
|
175
|
-
title: document.title || '',
|
|
176
|
-
scope: options.scope,
|
|
177
|
-
rootFound: Boolean(root),
|
|
178
|
-
root: nodes[0],
|
|
179
|
-
activeElement: active ? { identity: active.identity, tag: active.tag, name: active.name, path: active.path } : undefined,
|
|
180
|
-
nodes,
|
|
181
|
-
omitted,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
function mutationTarget(mutation, options) {
|
|
185
|
-
const target = mutation.target instanceof Element ? mutation.target : mutation.target.parentElement;
|
|
186
|
-
return target ? summarizeElement(target, 0, '@target', options) : undefined;
|
|
187
|
-
}
|
|
188
|
-
function pushEvent(events, omitted, maxEvents, event) {
|
|
189
|
-
if (events.length >= maxEvents) { omitted.events += 1; return; }
|
|
190
|
-
events.push(event);
|
|
191
|
-
}
|
|
192
|
-
function findNewSessionButton() {
|
|
193
|
-
const candidates = Array.from(document.querySelectorAll('button'));
|
|
194
|
-
return candidates.find((button) => {
|
|
195
|
-
const label = [button.getAttribute('aria-label'), button.getAttribute('title'), button.textContent].filter(Boolean).join(' ');
|
|
196
|
-
return /New Session/i.test(label);
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
async function runWatch(options) {
|
|
200
|
-
const root = document.querySelector(options.scope);
|
|
201
|
-
const events = [];
|
|
202
|
-
const omitted = { events: 0, nodes: 0, depth: 0, budget: false };
|
|
203
|
-
const start = performance.now();
|
|
204
|
-
const at = () => Math.max(0, Math.round(performance.now() - start));
|
|
205
|
-
const originalPushState = history.pushState;
|
|
206
|
-
const originalReplaceState = history.replaceState;
|
|
207
|
-
let action = undefined;
|
|
208
|
-
const routeEvent = (kind, beforeUrl, afterUrl) => {
|
|
209
|
-
if (beforeUrl !== afterUrl) pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'route', kind, before: beforeUrl, after: afterUrl });
|
|
210
|
-
};
|
|
211
|
-
history.pushState = function(...args) {
|
|
212
|
-
const beforeUrl = location.href;
|
|
213
|
-
const result = originalPushState.apply(this, args);
|
|
214
|
-
routeEvent('pushState', beforeUrl, location.href);
|
|
215
|
-
return result;
|
|
216
|
-
};
|
|
217
|
-
history.replaceState = function(...args) {
|
|
218
|
-
const beforeUrl = location.href;
|
|
219
|
-
const result = originalReplaceState.apply(this, args);
|
|
220
|
-
routeEvent('replaceState', beforeUrl, location.href);
|
|
221
|
-
return result;
|
|
222
|
-
};
|
|
223
|
-
const onPopState = () => pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'route', kind: 'popstate', after: location.href });
|
|
224
|
-
const onFocusIn = (event) => {
|
|
225
|
-
if (!(event.target instanceof Element)) return;
|
|
226
|
-
if (root && !root.contains(event.target)) return;
|
|
227
|
-
const node = summarizeElement(event.target, 0, '@focus', options);
|
|
228
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'focus', kind: 'focusin', target: node.identity, node });
|
|
229
|
-
};
|
|
230
|
-
const observer = root ? new MutationObserver((mutations) => {
|
|
231
|
-
for (const mutation of mutations) {
|
|
232
|
-
if (mutation.type === 'childList') {
|
|
233
|
-
for (const added of Array.from(mutation.addedNodes)) {
|
|
234
|
-
if (!(added instanceof Element)) continue;
|
|
235
|
-
const node = summarizeElement(added, 0, '@added', options);
|
|
236
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'added', target: node.identity, node });
|
|
237
|
-
}
|
|
238
|
-
for (const removed of Array.from(mutation.removedNodes)) {
|
|
239
|
-
if (!(removed instanceof Element)) continue;
|
|
240
|
-
const node = summarizeElement(removed, 0, '@removed', options);
|
|
241
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'removed', target: node.identity, node });
|
|
242
|
-
}
|
|
243
|
-
} else if (mutation.type === 'attributes') {
|
|
244
|
-
const node = mutationTarget(mutation, options);
|
|
245
|
-
if (!node) continue;
|
|
246
|
-
const name = mutation.attributeName || 'attribute';
|
|
247
|
-
const after = mutation.target instanceof Element ? mutation.target.getAttribute(name) : undefined;
|
|
248
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'attr', target: node.identity, detail: name, before: mutation.oldValue || '', after: after || '', node });
|
|
249
|
-
} else if (mutation.type === 'characterData') {
|
|
250
|
-
const node = mutationTarget(mutation, options);
|
|
251
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'text', target: node ? node.identity : undefined, before: short(mutation.oldValue || '', options.textLimit), after: short(mutation.target.textContent || '', options.textLimit), node });
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}) : undefined;
|
|
255
|
-
if (observer && root) observer.observe(root, { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, attributeFilter: ['class', 'style', 'hidden', 'aria-selected', 'aria-expanded', 'data-pibo-selected', 'data-pibo-session-id', 'data-pibo-selected-session-id', 'data-pibo-state', 'data-pibo-debug'] });
|
|
256
|
-
document.addEventListener('focusin', onFocusIn, true);
|
|
257
|
-
window.addEventListener('popstate', onPopState, true);
|
|
258
|
-
const before = captureSnapshot(options);
|
|
259
|
-
omitted.nodes += before.omitted.nodes;
|
|
260
|
-
omitted.depth += before.omitted.depth;
|
|
261
|
-
omitted.budget = omitted.budget || before.omitted.budget;
|
|
262
|
-
if (options.action === 'new-session') {
|
|
263
|
-
try {
|
|
264
|
-
const button = findNewSessionButton();
|
|
265
|
-
action = { requested: 'new-session', performed: Boolean(button) };
|
|
266
|
-
if (button) {
|
|
267
|
-
const node = summarizeElement(button, 0, '@action', options);
|
|
268
|
-
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'action', kind: 'click', target: node.identity, detail: 'New Session', node });
|
|
269
|
-
button.click();
|
|
270
|
-
} else {
|
|
271
|
-
action.error = 'New Session button not found';
|
|
272
|
-
}
|
|
273
|
-
} catch (error) {
|
|
274
|
-
action = { requested: 'new-session', performed: false, error: String(error && error.message ? error.message : error) };
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
await new Promise((resolve) => setTimeout(resolve, options.durationMs));
|
|
278
|
-
observer?.disconnect();
|
|
279
|
-
document.removeEventListener('focusin', onFocusIn, true);
|
|
280
|
-
window.removeEventListener('popstate', onPopState, true);
|
|
281
|
-
history.pushState = originalPushState;
|
|
282
|
-
history.replaceState = originalReplaceState;
|
|
283
|
-
const after = captureSnapshot(options);
|
|
284
|
-
omitted.nodes += after.omitted.nodes;
|
|
285
|
-
omitted.depth += after.omitted.depth;
|
|
286
|
-
omitted.budget = omitted.budget || after.omitted.budget || omitted.events > 0;
|
|
287
|
-
return {
|
|
288
|
-
kind: 'watch',
|
|
289
|
-
createdAt: nowIso(),
|
|
290
|
-
url: location.href,
|
|
291
|
-
title: document.title || '',
|
|
292
|
-
scope: options.scope,
|
|
293
|
-
durationMs: options.durationMs,
|
|
294
|
-
rootFound: Boolean(before.rootFound || after.rootFound),
|
|
295
|
-
events,
|
|
296
|
-
before,
|
|
297
|
-
after,
|
|
298
|
-
omitted,
|
|
299
|
-
action,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
16
|
+
return String.raw `
|
|
17
|
+
function nowIso() { return new Date().toISOString(); }
|
|
18
|
+
function safeString(value) { return typeof value === 'string' ? value : ''; }
|
|
19
|
+
function short(value, limit) {
|
|
20
|
+
const text = safeString(value).replace(/\s+/g, ' ').trim();
|
|
21
|
+
return text.length > limit ? text.slice(0, Math.max(0, limit - 1)) + '…' : text;
|
|
22
|
+
}
|
|
23
|
+
function redactText(element, options) {
|
|
24
|
+
const tag = element.tagName.toLowerCase();
|
|
25
|
+
const debug = element.getAttribute('data-pibo-debug') || '';
|
|
26
|
+
if (tag === 'textarea' || tag === 'input' || debug === 'composer') {
|
|
27
|
+
const value = 'value' in element ? String(element.value || '') : '';
|
|
28
|
+
return value ? '[redacted:' + value.length + ' chars]' : '';
|
|
29
|
+
}
|
|
30
|
+
const text = element.innerText || element.textContent || '';
|
|
31
|
+
if (!options.includeText && /message|trace|terminal|composer/i.test(debug)) return text ? '[redacted]' : '';
|
|
32
|
+
return short(text, options.textLimit);
|
|
33
|
+
}
|
|
34
|
+
function classSummary(element) {
|
|
35
|
+
const value = safeString(element.getAttribute('class'));
|
|
36
|
+
if (!value) return undefined;
|
|
37
|
+
const parts = value.split(/\s+/).filter(Boolean);
|
|
38
|
+
const useful = parts.filter((part) => /selected|active|hidden|opacity|translate|animate|border|bg-|text-|ring|disabled|pointer|sr-only/.test(part));
|
|
39
|
+
return (useful.length ? useful : parts.slice(0, 6)).slice(0, 10).join(' ');
|
|
40
|
+
}
|
|
41
|
+
function attrMap(element) {
|
|
42
|
+
const attrs = {};
|
|
43
|
+
const allow = /^(id|role|aria-|data-pibo-|data-testid$|disabled$|checked$|selected$|hidden$|tabindex$|title$)/;
|
|
44
|
+
for (const attr of Array.from(element.attributes || [])) {
|
|
45
|
+
if (!allow.test(attr.name)) continue;
|
|
46
|
+
if (/token|cookie|authorization|secret|password/i.test(attr.name)) {
|
|
47
|
+
attrs[attr.name] = '[redacted]';
|
|
48
|
+
} else {
|
|
49
|
+
attrs[attr.name] = short(attr.value, 120);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
53
|
+
attrs.value = element.value ? '[redacted:' + element.value.length + ' chars]' : '';
|
|
54
|
+
attrs.disabled = Boolean(element.disabled);
|
|
55
|
+
}
|
|
56
|
+
return attrs;
|
|
57
|
+
}
|
|
58
|
+
function roleOf(element) {
|
|
59
|
+
const explicit = element.getAttribute('role');
|
|
60
|
+
if (explicit) return explicit;
|
|
61
|
+
const tag = element.tagName.toLowerCase();
|
|
62
|
+
if (tag === 'button') return 'button';
|
|
63
|
+
if (tag === 'a') return 'link';
|
|
64
|
+
if (tag === 'textarea') return 'textbox';
|
|
65
|
+
if (tag === 'input') return 'input';
|
|
66
|
+
if (tag === 'select') return 'combobox';
|
|
67
|
+
if (tag === 'main') return 'main';
|
|
68
|
+
if (tag === 'aside') return 'complementary';
|
|
69
|
+
if (tag === 'nav') return 'navigation';
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
function nameOf(element, options) {
|
|
73
|
+
const aria = element.getAttribute('aria-label');
|
|
74
|
+
if (aria) return short(aria, options.textLimit);
|
|
75
|
+
const title = element.getAttribute('title');
|
|
76
|
+
if (title) return short(title, options.textLimit);
|
|
77
|
+
const tag = element.tagName.toLowerCase();
|
|
78
|
+
if (tag === 'input' || tag === 'textarea') return short(element.getAttribute('placeholder') || '', options.textLimit);
|
|
79
|
+
const debug = element.getAttribute('data-pibo-debug');
|
|
80
|
+
if (debug === 'session-row') return short(element.getAttribute('data-pibo-title') || element.innerText || '', options.textLimit);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function elementPath(element) {
|
|
84
|
+
const parts = [];
|
|
85
|
+
let current = element;
|
|
86
|
+
while (current && current.nodeType === Node.ELEMENT_NODE && current !== document.documentElement) {
|
|
87
|
+
const tag = current.tagName.toLowerCase();
|
|
88
|
+
const parent = current.parentElement;
|
|
89
|
+
if (!parent) {
|
|
90
|
+
parts.unshift(tag);
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const index = Array.from(parent.children).filter((child) => child.tagName === current.tagName).indexOf(current) + 1;
|
|
94
|
+
parts.unshift(tag + ':nth-of-type(' + index + ')');
|
|
95
|
+
current = parent;
|
|
96
|
+
}
|
|
97
|
+
return parts.join('>');
|
|
98
|
+
}
|
|
99
|
+
function identityOf(element) {
|
|
100
|
+
const debug = element.getAttribute('data-pibo-debug');
|
|
101
|
+
const session = element.getAttribute('data-pibo-session-id');
|
|
102
|
+
const room = element.getAttribute('data-pibo-room-id');
|
|
103
|
+
const view = element.getAttribute('data-pibo-view-id');
|
|
104
|
+
const testId = element.getAttribute('data-testid');
|
|
105
|
+
const id = element.id;
|
|
106
|
+
if (debug && session) return { identity: debug + ':' + session, kind: 'pibo-session' };
|
|
107
|
+
if (debug && room) return { identity: debug + ':' + room, kind: 'pibo-room' };
|
|
108
|
+
if (debug && view) return { identity: debug + ':' + view, kind: 'pibo-view' };
|
|
109
|
+
if (debug) return { identity: debug, kind: 'pibo-debug' };
|
|
110
|
+
if (testId) return { identity: 'testid:' + testId, kind: 'testid' };
|
|
111
|
+
if (id) return { identity: 'id:' + id, kind: 'id' };
|
|
112
|
+
const role = roleOf(element);
|
|
113
|
+
const name = nameOf(element, { textLimit: 40 }) || '';
|
|
114
|
+
if (role && name) return { identity: role + ':' + name, kind: 'role-name' };
|
|
115
|
+
return { identity: 'path:' + elementPath(element), kind: 'path' };
|
|
116
|
+
}
|
|
117
|
+
function boxOf(element) {
|
|
118
|
+
const rect = element.getBoundingClientRect();
|
|
119
|
+
return { x: Math.round(rect.x), y: Math.round(rect.y), w: Math.round(rect.width), h: Math.round(rect.height) };
|
|
120
|
+
}
|
|
121
|
+
function isImportantElement(element, depth) {
|
|
122
|
+
if (!(element instanceof Element)) return false;
|
|
123
|
+
const tag = element.tagName.toLowerCase();
|
|
124
|
+
if (depth === 0) return true;
|
|
125
|
+
if (['script', 'style', 'svg', 'path', 'rect', 'circle', 'line', 'polyline', 'polygon'].includes(tag)) return false;
|
|
126
|
+
if (element.hasAttribute('data-pibo-debug') || element.hasAttribute('data-pibo-session-id') || element.hasAttribute('data-testid')) return true;
|
|
127
|
+
if (element.hasAttribute('aria-label') || element.hasAttribute('title') || element.hasAttribute('role')) return true;
|
|
128
|
+
if (['button', 'a', 'input', 'textarea', 'select', 'option', 'main', 'aside', 'nav'].includes(tag)) return true;
|
|
129
|
+
if (element === document.activeElement) return true;
|
|
130
|
+
if (element.getAttribute('aria-selected') === 'true' || element.getAttribute('data-pibo-selected') === 'true' || element.hasAttribute('hidden')) return true;
|
|
131
|
+
const text = (element.innerText || element.textContent || '').replace(/\s+/g, ' ').trim();
|
|
132
|
+
if (text && element.children.length === 0 && depth <= 4) return true;
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
function summarizeElement(element, depth, ref, options) {
|
|
136
|
+
const ident = identityOf(element);
|
|
137
|
+
const node = {
|
|
138
|
+
ref,
|
|
139
|
+
identity: ident.identity,
|
|
140
|
+
identityKind: ident.kind,
|
|
141
|
+
depth,
|
|
142
|
+
tag: element.tagName.toLowerCase(),
|
|
143
|
+
attributes: attrMap(element),
|
|
144
|
+
path: elementPath(element),
|
|
145
|
+
};
|
|
146
|
+
const role = roleOf(element); if (role) node.role = role;
|
|
147
|
+
const name = nameOf(element, options); if (name) node.name = name;
|
|
148
|
+
const text = redactText(element, options); if (text) node.text = text;
|
|
149
|
+
const classes = classSummary(element); if (classes) node.classSummary = classes;
|
|
150
|
+
if (document.activeElement === element) node.focused = true;
|
|
151
|
+
if (options.includeLayout) node.box = boxOf(element);
|
|
152
|
+
return node;
|
|
153
|
+
}
|
|
154
|
+
function captureSnapshot(options) {
|
|
155
|
+
const root = document.querySelector(options.scope);
|
|
156
|
+
const nodes = [];
|
|
157
|
+
const omitted = { nodes: 0, depth: 0, budget: false };
|
|
158
|
+
let refSeq = 0;
|
|
159
|
+
function walk(element, depth) {
|
|
160
|
+
if (!(element instanceof Element)) return;
|
|
161
|
+
if (depth > options.maxDepth) { omitted.depth += 1; return; }
|
|
162
|
+
if (isImportantElement(element, depth)) {
|
|
163
|
+
if (nodes.length >= options.maxNodes) { omitted.nodes += 1; omitted.budget = true; return; }
|
|
164
|
+
const node = summarizeElement(element, depth, '@n' + (++refSeq), options);
|
|
165
|
+
nodes.push(node);
|
|
166
|
+
}
|
|
167
|
+
for (const child of Array.from(element.children)) walk(child, depth + 1);
|
|
168
|
+
}
|
|
169
|
+
if (root) walk(root, 0);
|
|
170
|
+
const active = document.activeElement instanceof Element ? summarizeElement(document.activeElement, 0, '@focus', options) : undefined;
|
|
171
|
+
return {
|
|
172
|
+
kind: 'snapshot',
|
|
173
|
+
createdAt: nowIso(),
|
|
174
|
+
url: location.href,
|
|
175
|
+
title: document.title || '',
|
|
176
|
+
scope: options.scope,
|
|
177
|
+
rootFound: Boolean(root),
|
|
178
|
+
root: nodes[0],
|
|
179
|
+
activeElement: active ? { identity: active.identity, tag: active.tag, name: active.name, path: active.path } : undefined,
|
|
180
|
+
nodes,
|
|
181
|
+
omitted,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function mutationTarget(mutation, options) {
|
|
185
|
+
const target = mutation.target instanceof Element ? mutation.target : mutation.target.parentElement;
|
|
186
|
+
return target ? summarizeElement(target, 0, '@target', options) : undefined;
|
|
187
|
+
}
|
|
188
|
+
function pushEvent(events, omitted, maxEvents, event) {
|
|
189
|
+
if (events.length >= maxEvents) { omitted.events += 1; return; }
|
|
190
|
+
events.push(event);
|
|
191
|
+
}
|
|
192
|
+
function findNewSessionButton() {
|
|
193
|
+
const candidates = Array.from(document.querySelectorAll('button'));
|
|
194
|
+
return candidates.find((button) => {
|
|
195
|
+
const label = [button.getAttribute('aria-label'), button.getAttribute('title'), button.textContent].filter(Boolean).join(' ');
|
|
196
|
+
return /New Session/i.test(label);
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async function runWatch(options) {
|
|
200
|
+
const root = document.querySelector(options.scope);
|
|
201
|
+
const events = [];
|
|
202
|
+
const omitted = { events: 0, nodes: 0, depth: 0, budget: false };
|
|
203
|
+
const start = performance.now();
|
|
204
|
+
const at = () => Math.max(0, Math.round(performance.now() - start));
|
|
205
|
+
const originalPushState = history.pushState;
|
|
206
|
+
const originalReplaceState = history.replaceState;
|
|
207
|
+
let action = undefined;
|
|
208
|
+
const routeEvent = (kind, beforeUrl, afterUrl) => {
|
|
209
|
+
if (beforeUrl !== afterUrl) pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'route', kind, before: beforeUrl, after: afterUrl });
|
|
210
|
+
};
|
|
211
|
+
history.pushState = function(...args) {
|
|
212
|
+
const beforeUrl = location.href;
|
|
213
|
+
const result = originalPushState.apply(this, args);
|
|
214
|
+
routeEvent('pushState', beforeUrl, location.href);
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
history.replaceState = function(...args) {
|
|
218
|
+
const beforeUrl = location.href;
|
|
219
|
+
const result = originalReplaceState.apply(this, args);
|
|
220
|
+
routeEvent('replaceState', beforeUrl, location.href);
|
|
221
|
+
return result;
|
|
222
|
+
};
|
|
223
|
+
const onPopState = () => pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'route', kind: 'popstate', after: location.href });
|
|
224
|
+
const onFocusIn = (event) => {
|
|
225
|
+
if (!(event.target instanceof Element)) return;
|
|
226
|
+
if (root && !root.contains(event.target)) return;
|
|
227
|
+
const node = summarizeElement(event.target, 0, '@focus', options);
|
|
228
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'focus', kind: 'focusin', target: node.identity, node });
|
|
229
|
+
};
|
|
230
|
+
const observer = root ? new MutationObserver((mutations) => {
|
|
231
|
+
for (const mutation of mutations) {
|
|
232
|
+
if (mutation.type === 'childList') {
|
|
233
|
+
for (const added of Array.from(mutation.addedNodes)) {
|
|
234
|
+
if (!(added instanceof Element)) continue;
|
|
235
|
+
const node = summarizeElement(added, 0, '@added', options);
|
|
236
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'added', target: node.identity, node });
|
|
237
|
+
}
|
|
238
|
+
for (const removed of Array.from(mutation.removedNodes)) {
|
|
239
|
+
if (!(removed instanceof Element)) continue;
|
|
240
|
+
const node = summarizeElement(removed, 0, '@removed', options);
|
|
241
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'removed', target: node.identity, node });
|
|
242
|
+
}
|
|
243
|
+
} else if (mutation.type === 'attributes') {
|
|
244
|
+
const node = mutationTarget(mutation, options);
|
|
245
|
+
if (!node) continue;
|
|
246
|
+
const name = mutation.attributeName || 'attribute';
|
|
247
|
+
const after = mutation.target instanceof Element ? mutation.target.getAttribute(name) : undefined;
|
|
248
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'attr', target: node.identity, detail: name, before: mutation.oldValue || '', after: after || '', node });
|
|
249
|
+
} else if (mutation.type === 'characterData') {
|
|
250
|
+
const node = mutationTarget(mutation, options);
|
|
251
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'dom', kind: 'text', target: node ? node.identity : undefined, before: short(mutation.oldValue || '', options.textLimit), after: short(mutation.target.textContent || '', options.textLimit), node });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}) : undefined;
|
|
255
|
+
if (observer && root) observer.observe(root, { childList: true, subtree: true, attributes: true, attributeOldValue: true, characterData: true, characterDataOldValue: true, attributeFilter: ['class', 'style', 'hidden', 'aria-selected', 'aria-expanded', 'data-pibo-selected', 'data-pibo-session-id', 'data-pibo-selected-session-id', 'data-pibo-state', 'data-pibo-debug'] });
|
|
256
|
+
document.addEventListener('focusin', onFocusIn, true);
|
|
257
|
+
window.addEventListener('popstate', onPopState, true);
|
|
258
|
+
const before = captureSnapshot(options);
|
|
259
|
+
omitted.nodes += before.omitted.nodes;
|
|
260
|
+
omitted.depth += before.omitted.depth;
|
|
261
|
+
omitted.budget = omitted.budget || before.omitted.budget;
|
|
262
|
+
if (options.action === 'new-session') {
|
|
263
|
+
try {
|
|
264
|
+
const button = findNewSessionButton();
|
|
265
|
+
action = { requested: 'new-session', performed: Boolean(button) };
|
|
266
|
+
if (button) {
|
|
267
|
+
const node = summarizeElement(button, 0, '@action', options);
|
|
268
|
+
pushEvent(events, omitted, options.maxEvents, { t: at(), source: 'action', kind: 'click', target: node.identity, detail: 'New Session', node });
|
|
269
|
+
button.click();
|
|
270
|
+
} else {
|
|
271
|
+
action.error = 'New Session button not found';
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
action = { requested: 'new-session', performed: false, error: String(error && error.message ? error.message : error) };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await new Promise((resolve) => setTimeout(resolve, options.durationMs));
|
|
278
|
+
observer?.disconnect();
|
|
279
|
+
document.removeEventListener('focusin', onFocusIn, true);
|
|
280
|
+
window.removeEventListener('popstate', onPopState, true);
|
|
281
|
+
history.pushState = originalPushState;
|
|
282
|
+
history.replaceState = originalReplaceState;
|
|
283
|
+
const after = captureSnapshot(options);
|
|
284
|
+
omitted.nodes += after.omitted.nodes;
|
|
285
|
+
omitted.depth += after.omitted.depth;
|
|
286
|
+
omitted.budget = omitted.budget || after.omitted.budget || omitted.events > 0;
|
|
287
|
+
return {
|
|
288
|
+
kind: 'watch',
|
|
289
|
+
createdAt: nowIso(),
|
|
290
|
+
url: location.href,
|
|
291
|
+
title: document.title || '',
|
|
292
|
+
scope: options.scope,
|
|
293
|
+
durationMs: options.durationMs,
|
|
294
|
+
rootFound: Boolean(before.rootFound || after.rootFound),
|
|
295
|
+
events,
|
|
296
|
+
before,
|
|
297
|
+
after,
|
|
298
|
+
omitted,
|
|
299
|
+
action,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
302
|
`;
|
|
303
303
|
}
|