pi-inspect 0.1.0
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 +42 -0
- package/extensions/inspect.ts +309 -0
- package/extensions/package.json +1 -0
- package/extensions/tsconfig.json +16 -0
- package/lib/snapshot.js +56 -0
- package/package.json +66 -0
- package/public/app.js +616 -0
- package/public/icon-maskable.svg +1 -0
- package/public/icon.svg +1 -0
- package/public/index.html +77 -0
- package/public/manifest.webmanifest +24 -0
- package/public/style.css +1443 -0
- package/public/sw.js +41 -0
- package/server.js +152 -0
- package/themes/pi-dark.json +21 -0
- package/themes/pi-light.json +21 -0
package/public/app.js
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
//#region STATE
|
|
2
|
+
const state = {
|
|
3
|
+
sessions: [],
|
|
4
|
+
currentSessionId: null,
|
|
5
|
+
snapshot: null,
|
|
6
|
+
search: '',
|
|
7
|
+
kind: 'all',
|
|
8
|
+
expanded: { context: true, tool: true, command: true, skill: true },
|
|
9
|
+
selected: null,
|
|
10
|
+
expandAll: true,
|
|
11
|
+
};
|
|
12
|
+
const els = {};
|
|
13
|
+
//#endregion
|
|
14
|
+
|
|
15
|
+
//#region UTIL
|
|
16
|
+
const $ = (id) => document.getElementById(id);
|
|
17
|
+
const esc = (s) =>
|
|
18
|
+
String(s ?? '')
|
|
19
|
+
.replace(/&/g, '&')
|
|
20
|
+
.replace(/</g, '<')
|
|
21
|
+
.replace(/>/g, '>')
|
|
22
|
+
.replace(/"/g, '"');
|
|
23
|
+
|
|
24
|
+
function highlightJson(value) {
|
|
25
|
+
const json = JSON.stringify(value, null, 2);
|
|
26
|
+
if (json == null) return '';
|
|
27
|
+
return esc(json).replace(
|
|
28
|
+
/("(?:\\.|(?!").)*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,
|
|
29
|
+
(m, str, colon, kw) => {
|
|
30
|
+
if (str) return `<span class="jk-${colon ? 'key' : 'str'}">${str}</span>${colon ?? ''}`;
|
|
31
|
+
if (kw) return `<span class="jk-${kw === 'null' ? 'null' : 'bool'}">${kw}</span>`;
|
|
32
|
+
return `<span class="jk-num">${m}</span>`;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function basename(p) {
|
|
38
|
+
if (!p) return '';
|
|
39
|
+
const parts = String(p).split(/[\\/]/).filter(Boolean);
|
|
40
|
+
return parts[parts.length - 1] ?? '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getUrlSession() {
|
|
44
|
+
return new URLSearchParams(location.search).get('session');
|
|
45
|
+
}
|
|
46
|
+
function setUrlSession(id, replace = false) {
|
|
47
|
+
const url = new URL(location.href);
|
|
48
|
+
if (id) url.searchParams.set('session', id);
|
|
49
|
+
else url.searchParams.delete('session');
|
|
50
|
+
(replace ? history.replaceState : history.pushState).call(history, null, '', url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toast(msg, kind = 'info') {
|
|
54
|
+
const c = $('toast');
|
|
55
|
+
if (!c) return;
|
|
56
|
+
const el = document.createElement('div');
|
|
57
|
+
el.className = `toast toast-${kind}`;
|
|
58
|
+
el.textContent = msg;
|
|
59
|
+
c.appendChild(el);
|
|
60
|
+
setTimeout(() => el.remove(), 2500);
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
|
|
64
|
+
//#region FETCH
|
|
65
|
+
async function fetchJson(path) {
|
|
66
|
+
const r = await fetch(path);
|
|
67
|
+
if (!r.ok) throw new Error(`${path} → ${r.status}`);
|
|
68
|
+
return r.json();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function loadSessions() {
|
|
72
|
+
const data = await fetchJson('/api/sessions');
|
|
73
|
+
state.sessions = data.sessions ?? [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadSnapshot(sessionId) {
|
|
77
|
+
try {
|
|
78
|
+
const qs = sessionId ? `?session=${encodeURIComponent(sessionId)}` : '';
|
|
79
|
+
state.snapshot = await fetchJson(`/api/introspect${qs}`);
|
|
80
|
+
state.currentSessionId = state.snapshot?.sessionId ?? sessionId ?? null;
|
|
81
|
+
} catch {
|
|
82
|
+
state.snapshot = null;
|
|
83
|
+
state.currentSessionId = sessionId ?? null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
|
|
88
|
+
function inferPath(x) {
|
|
89
|
+
const p = x?.sourceInfo?.path;
|
|
90
|
+
if (!p || typeof p !== 'string') return null;
|
|
91
|
+
if (/^<.*>$/.test(p)) return null;
|
|
92
|
+
return p;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function inferSource(x) {
|
|
96
|
+
const si = x?.sourceInfo;
|
|
97
|
+
if (si) {
|
|
98
|
+
if (si.label) return si.label;
|
|
99
|
+
if (si.source) return si.source.startsWith('npm:') ? si.source.slice(4) : si.source;
|
|
100
|
+
if (si.origin) return si.origin;
|
|
101
|
+
if (si.kind) return si.kind;
|
|
102
|
+
}
|
|
103
|
+
return x?.source ?? 'builtin';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
//#region MODEL
|
|
107
|
+
function buildItems() {
|
|
108
|
+
const s = state.snapshot;
|
|
109
|
+
if (!s) return [];
|
|
110
|
+
const items = [];
|
|
111
|
+
for (const t of s.tools ?? []) {
|
|
112
|
+
items.push({
|
|
113
|
+
kind: 'tool',
|
|
114
|
+
id: `tool:${t.name}`,
|
|
115
|
+
name: t.name ?? '(tool)',
|
|
116
|
+
source: inferSource(t),
|
|
117
|
+
description: t.description ?? '',
|
|
118
|
+
active: (s.activeTools ?? []).includes(t.name),
|
|
119
|
+
path: inferPath(t),
|
|
120
|
+
raw: t,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const c of s.commands ?? []) {
|
|
124
|
+
const name = c.name ?? c.command ?? '';
|
|
125
|
+
const isSkill = name.startsWith('skill:');
|
|
126
|
+
items.push({
|
|
127
|
+
kind: isSkill ? 'skill' : 'command',
|
|
128
|
+
id: `${isSkill ? 'skill' : 'command'}:${name}`,
|
|
129
|
+
name: `/${name}`,
|
|
130
|
+
source: inferSource(c),
|
|
131
|
+
description: c.description ?? '',
|
|
132
|
+
path: inferPath(c),
|
|
133
|
+
raw: c,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (s.systemPrompt) {
|
|
137
|
+
for (const part of splitSystemPrompt(s.systemPrompt, s.cwd)) {
|
|
138
|
+
items.push({
|
|
139
|
+
kind: 'context',
|
|
140
|
+
id: `context:${part.id}`,
|
|
141
|
+
name: part.name,
|
|
142
|
+
source: `${part.text.length} chars`,
|
|
143
|
+
description: part.text.slice(0, 240).replace(/\s+/g, ' '),
|
|
144
|
+
path: part.path ?? null,
|
|
145
|
+
raw: { systemPrompt: part.text, path: part.path ?? null },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return items;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function filterItems(items) {
|
|
153
|
+
const q = state.search.trim().toLowerCase();
|
|
154
|
+
return items.filter((it) => {
|
|
155
|
+
if (state.kind !== 'all' && it.kind !== state.kind) return false;
|
|
156
|
+
if (!q) return true;
|
|
157
|
+
return (
|
|
158
|
+
it.name.toLowerCase().includes(q) ||
|
|
159
|
+
(it.description ?? '').toLowerCase().includes(q)
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const KIND_ORDER = ['context', 'tool', 'command', 'skill'];
|
|
165
|
+
const KIND_LABEL = { context: 'Context', tool: 'Tools', command: 'Commands', skill: 'Skills' };
|
|
166
|
+
//#endregion
|
|
167
|
+
|
|
168
|
+
//#region ICONS
|
|
169
|
+
function iconFor(kind) {
|
|
170
|
+
if (kind === 'tool') {
|
|
171
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 000 1.4l1.6 1.6a1 1 0 001.4 0l3.77-3.77a6 6 0 01-7.94 7.94l-6.91 6.91a2.12 2.12 0 01-3-3l6.91-6.91a6 6 0 017.94-7.94l-3.76 3.76z"/></svg>`;
|
|
172
|
+
}
|
|
173
|
+
if (kind === 'command') {
|
|
174
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/></svg>`;
|
|
175
|
+
}
|
|
176
|
+
if (kind === 'skill') {
|
|
177
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`;
|
|
178
|
+
}
|
|
179
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>`;
|
|
180
|
+
}
|
|
181
|
+
function chevronSvg() {
|
|
182
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 6 15 12 9 18"/></svg>`;
|
|
183
|
+
}
|
|
184
|
+
function highlightMarkdown(md) {
|
|
185
|
+
let s = esc(md);
|
|
186
|
+
s = s.replace(/^(#{1,6}\s.*)$/gm, '<span class="md-h">$1</span>');
|
|
187
|
+
s = s.replace(/(```[\s\S]*?```)/g, '<span class="md-code">$1</span>');
|
|
188
|
+
s = s.replace(/(`[^`\n]+`)/g, '<span class="md-icode">$1</span>');
|
|
189
|
+
s = s.replace(/(\*\*[^*\n]+\*\*)/g, '<span class="md-bold">$1</span>');
|
|
190
|
+
s = s.replace(/^(\s*[-*+]\s+)/gm, '<span class="md-bullet">$1</span>');
|
|
191
|
+
return s;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function splitSystemPrompt(prompt, cwd) {
|
|
195
|
+
const re = /\n(##\s+[^\n]*?(?:AGENTS|CLAUDE)\.md)\n/g;
|
|
196
|
+
const hits = [];
|
|
197
|
+
let m;
|
|
198
|
+
while ((m = re.exec(prompt)) !== null) hits.push({ index: m.index, header: m[1], end: m.index + m[0].length });
|
|
199
|
+
if (!hits.length) return [{ id: 'system-prompt', name: 'system prompt', text: prompt }];
|
|
200
|
+
|
|
201
|
+
// Strip trailing skill-injection block from the final memory section.
|
|
202
|
+
const skillMarker = prompt.search(/\nThe following skills provide specialized instructions/);
|
|
203
|
+
const memoryEnd = skillMarker > 0 ? skillMarker : prompt.length;
|
|
204
|
+
|
|
205
|
+
const parts = [];
|
|
206
|
+
let sysEnd = hits[0].index;
|
|
207
|
+
const ctxHeader = prompt.lastIndexOf('\n# Project Context', sysEnd);
|
|
208
|
+
if (ctxHeader > 0) sysEnd = ctxHeader;
|
|
209
|
+
parts.push({ id: 'system-prompt', name: 'system prompt', text: prompt.slice(0, sysEnd).trimEnd(), order: 0 });
|
|
210
|
+
for (let i = 0; i < hits.length; i++) {
|
|
211
|
+
const start = hits[i].end;
|
|
212
|
+
const end = i + 1 < hits.length ? hits[i + 1].index : memoryEnd;
|
|
213
|
+
const pathMatch = hits[i].header.match(/##\s+(.+?\.md)\s*$/);
|
|
214
|
+
const fullPath = pathMatch ? pathMatch[1].trim() : '(memory)';
|
|
215
|
+
const norm = fullPath.replace(/\\/g, '/').toLowerCase();
|
|
216
|
+
const isUserScope = /\/\.pi\/agent\//.test(norm) || /\/\.agents\//.test(norm);
|
|
217
|
+
const cwdNorm = cwd ? String(cwd).replace(/\\/g, '/').toLowerCase() : null;
|
|
218
|
+
const isProject = !isUserScope && (cwdNorm ? norm.startsWith(cwdNorm) : true);
|
|
219
|
+
const fileName = fullPath.split(/[\\/]/).pop() || 'memory';
|
|
220
|
+
parts.push({
|
|
221
|
+
id: `memory:${i}`,
|
|
222
|
+
name: `${isProject ? 'project' : 'user'} · ${fileName}`,
|
|
223
|
+
text: prompt.slice(start, end).trim(),
|
|
224
|
+
path: fullPath,
|
|
225
|
+
order: isProject ? 2 : 1,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
parts.sort((a, b) => a.order - b.order);
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function packageSvg() {
|
|
233
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>`;
|
|
234
|
+
}
|
|
235
|
+
//#endregion
|
|
236
|
+
|
|
237
|
+
//#region RENDER TOPBAR
|
|
238
|
+
function renderTopbar() {
|
|
239
|
+
// Session picker
|
|
240
|
+
const sel = $('sessionSelect');
|
|
241
|
+
sel.innerHTML = '';
|
|
242
|
+
if (!state.sessions.length) {
|
|
243
|
+
const opt = document.createElement('option');
|
|
244
|
+
opt.value = '';
|
|
245
|
+
opt.textContent = '(no sessions)';
|
|
246
|
+
sel.appendChild(opt);
|
|
247
|
+
sel.disabled = true;
|
|
248
|
+
} else {
|
|
249
|
+
sel.disabled = false;
|
|
250
|
+
for (const s of state.sessions) {
|
|
251
|
+
const opt = document.createElement('option');
|
|
252
|
+
opt.value = s.id;
|
|
253
|
+
const label = s.name ? s.name : `${s.id.slice(0, 8)}…`;
|
|
254
|
+
const cwd = s.cwd ? ` · ${basename(s.cwd)}` : '';
|
|
255
|
+
opt.textContent = `${label}${cwd}`;
|
|
256
|
+
if (s.id === state.currentSessionId) opt.selected = true;
|
|
257
|
+
sel.appendChild(opt);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Project path
|
|
261
|
+
$('projectPath').textContent = state.snapshot?.cwd ?? '—';
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
|
|
265
|
+
//#region RENDER TREE
|
|
266
|
+
function renderTree() {
|
|
267
|
+
const root = $('treeContainer');
|
|
268
|
+
const items = filterItems(buildItems());
|
|
269
|
+
if (!state.snapshot) {
|
|
270
|
+
root.innerHTML = `<div class="loading">No snapshot for this session. Run <code>/inspect snapshot</code> in a pi session.</div>`;
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (!items.length) {
|
|
274
|
+
root.innerHTML = `<div class="loading">No items match the current filter.</div>`;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const groups = new Map();
|
|
279
|
+
for (const k of KIND_ORDER) groups.set(k, []);
|
|
280
|
+
for (const it of items) groups.get(it.kind).push(it);
|
|
281
|
+
|
|
282
|
+
const html = [];
|
|
283
|
+
for (const kind of KIND_ORDER) {
|
|
284
|
+
const list = groups.get(kind);
|
|
285
|
+
if (!list.length) continue;
|
|
286
|
+
const expanded = state.expanded[kind];
|
|
287
|
+
html.push(`
|
|
288
|
+
<div class="tree-row marketplace-row" data-group="${kind}">
|
|
289
|
+
<div class="tree-chevron ${expanded ? 'expanded' : ''}">${chevronSvg()}</div>
|
|
290
|
+
<div class="tree-icon">${iconFor(kind)}</div>
|
|
291
|
+
<div class="tree-label"><span class="mkt-name">${esc(KIND_LABEL[kind])}</span></div>
|
|
292
|
+
<div class="spacer"></div>
|
|
293
|
+
<div class="tree-meta">${list.length}</div>
|
|
294
|
+
</div>
|
|
295
|
+
`);
|
|
296
|
+
if (expanded) {
|
|
297
|
+
const bySource = new Map();
|
|
298
|
+
for (const it of list) {
|
|
299
|
+
const k = it.source || '(unknown)';
|
|
300
|
+
if (!bySource.has(k)) bySource.set(k, []);
|
|
301
|
+
bySource.get(k).push(it);
|
|
302
|
+
}
|
|
303
|
+
const useSubgroups = bySource.size > 1 && kind !== 'context';
|
|
304
|
+
const sources = useSubgroups
|
|
305
|
+
? [...bySource.keys()].sort((a, b) => {
|
|
306
|
+
if (a === 'builtin') return -1;
|
|
307
|
+
if (b === 'builtin') return 1;
|
|
308
|
+
return a.localeCompare(b);
|
|
309
|
+
})
|
|
310
|
+
: ['__all__'];
|
|
311
|
+
if (!useSubgroups) bySource.set('__all__', list);
|
|
312
|
+
for (const src of sources) {
|
|
313
|
+
const sublist = bySource.get(src);
|
|
314
|
+
const subKey = `${kind}::${src}`;
|
|
315
|
+
const subExpanded = state.expanded[subKey] !== false;
|
|
316
|
+
if (useSubgroups) {
|
|
317
|
+
html.push(`
|
|
318
|
+
<div class="tree-row marketplace-row tree-subgroup" data-subgroup="${esc(subKey)}" style="padding-left:24px">
|
|
319
|
+
<div class="tree-chevron ${subExpanded ? 'expanded' : ''}">${chevronSvg()}</div>
|
|
320
|
+
<div class="tree-icon">${packageSvg()}</div>
|
|
321
|
+
<div class="tree-label"><span class="mkt-name">${esc(src)}</span></div>
|
|
322
|
+
<div class="spacer"></div>
|
|
323
|
+
<div class="tree-meta">${sublist.length}</div>
|
|
324
|
+
</div>
|
|
325
|
+
`);
|
|
326
|
+
}
|
|
327
|
+
if (!useSubgroups || subExpanded) {
|
|
328
|
+
for (const it of sublist) {
|
|
329
|
+
const selected = state.selected === it.id ? 'selected' : '';
|
|
330
|
+
const pad = useSubgroups ? 48 : 32;
|
|
331
|
+
html.push(`
|
|
332
|
+
<div class="tree-row ${selected}" data-item="${esc(it.id)}" style="padding-left:${pad}px">
|
|
333
|
+
<div class="tree-icon">${iconFor(it.kind)}</div>
|
|
334
|
+
<div class="tree-label">${esc(it.name)}</div>
|
|
335
|
+
<div class="spacer"></div>
|
|
336
|
+
<div class="tree-meta">${esc(it.source)}</div>
|
|
337
|
+
</div>
|
|
338
|
+
`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
root.innerHTML = html.join('');
|
|
345
|
+
|
|
346
|
+
root.querySelectorAll('.tree-row[data-group]').forEach((el) => {
|
|
347
|
+
el.addEventListener('click', () => {
|
|
348
|
+
const k = el.dataset.group;
|
|
349
|
+
state.expanded[k] = !state.expanded[k];
|
|
350
|
+
renderTree();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
root.querySelectorAll('.tree-row[data-subgroup]').forEach((el) => {
|
|
354
|
+
el.addEventListener('click', () => {
|
|
355
|
+
const k = el.dataset.subgroup;
|
|
356
|
+
state.expanded[k] = state.expanded[k] === false;
|
|
357
|
+
renderTree();
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
root.querySelectorAll('.tree-row[data-item]').forEach((el) => {
|
|
361
|
+
el.addEventListener('click', () => {
|
|
362
|
+
state.selected = el.dataset.item;
|
|
363
|
+
renderTree();
|
|
364
|
+
renderDetail();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
369
|
+
|
|
370
|
+
//#region RENDER DETAIL
|
|
371
|
+
function renderDetail() {
|
|
372
|
+
const panel = $('detailPanel');
|
|
373
|
+
if (!state.selected || !state.snapshot) {
|
|
374
|
+
panel.innerHTML = `
|
|
375
|
+
<div class="detail-empty">
|
|
376
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" opacity="0.3"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
377
|
+
<span>Select an item to view details</span>
|
|
378
|
+
</div>`;
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const items = buildItems();
|
|
382
|
+
const it = items.find((x) => x.id === state.selected);
|
|
383
|
+
if (!it) {
|
|
384
|
+
panel.innerHTML = `<div class="detail-empty"><span>Item not found in current snapshot.</span></div>`;
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const isPrompt = it.kind === 'context' && it.raw?.systemPrompt;
|
|
389
|
+
const bodySections = [];
|
|
390
|
+
|
|
391
|
+
if (it.description && !isPrompt) {
|
|
392
|
+
bodySections.push(`
|
|
393
|
+
<div class="detail-section">
|
|
394
|
+
<h4>Description</h4>
|
|
395
|
+
<div class="detail-desc">${esc(it.description)}</div>
|
|
396
|
+
</div>
|
|
397
|
+
`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
bodySections.push(`
|
|
401
|
+
<div class="detail-section">
|
|
402
|
+
<h4>Metadata</h4>
|
|
403
|
+
<div class="detail-meta-row">
|
|
404
|
+
<span class="detail-meta-item">Kind: ${esc(it.kind)}</span>
|
|
405
|
+
<span class="detail-meta-item">Source: ${esc(it.source)}</span>
|
|
406
|
+
${it.active != null ? `<span class="detail-meta-item">Active: ${it.active ? 'yes' : 'no'}</span>` : ''}
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
`);
|
|
410
|
+
|
|
411
|
+
if (isPrompt) {
|
|
412
|
+
bodySections.push(`
|
|
413
|
+
<div class="detail-section">
|
|
414
|
+
<h4>System prompt (${it.raw.systemPrompt.length} chars)</h4>
|
|
415
|
+
<pre class="content-viewer-code"><code class="md">${highlightMarkdown(it.raw.systemPrompt)}</code></pre>
|
|
416
|
+
</div>
|
|
417
|
+
`);
|
|
418
|
+
} else {
|
|
419
|
+
bodySections.push(`
|
|
420
|
+
<div class="detail-section">
|
|
421
|
+
<h4>Raw</h4>
|
|
422
|
+
<pre class="content-viewer-code"><code class="json">${highlightJson(it.raw)}</code></pre>
|
|
423
|
+
</div>
|
|
424
|
+
`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
panel.innerHTML = `
|
|
428
|
+
<div class="detail-header">
|
|
429
|
+
<h3>${iconFor(it.kind)} ${esc(it.name)} <span class="version">${esc(it.kind)}</span></h3>
|
|
430
|
+
<div class="detail-header-actions">
|
|
431
|
+
${it.path ? `<button class="detail-action" id="openEditorBtn" title="Open in $EDITOR"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg></button>` : ''}
|
|
432
|
+
${it.path ? `<button class="detail-action" id="copyPathBtn" title="Copy path"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg></button>` : ''}
|
|
433
|
+
<button class="detail-close" id="detailCloseBtn" title="Close">✕</button>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
<div class="detail-body">${bodySections.join('')}</div>
|
|
437
|
+
`;
|
|
438
|
+
$('detailCloseBtn').addEventListener('click', () => {
|
|
439
|
+
state.selected = null;
|
|
440
|
+
renderTree();
|
|
441
|
+
renderDetail();
|
|
442
|
+
});
|
|
443
|
+
const openBtn = $('openEditorBtn');
|
|
444
|
+
if (openBtn) openBtn.addEventListener('click', async () => {
|
|
445
|
+
try {
|
|
446
|
+
const r = await fetch('/api/open', {
|
|
447
|
+
method: 'POST',
|
|
448
|
+
headers: { 'content-type': 'application/json' },
|
|
449
|
+
body: JSON.stringify({ path: it.path }),
|
|
450
|
+
});
|
|
451
|
+
const ct = r.headers.get('content-type') || '';
|
|
452
|
+
if (!ct.includes('application/json')) {
|
|
453
|
+
toast(`Failed: server returned ${r.status} — restart pi-inspect server`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const j = await r.json();
|
|
457
|
+
toast(j.ok ? `Opened in ${j.editor}` : `Failed: ${j.error || 'unknown'}`);
|
|
458
|
+
} catch (e) { toast(`Failed: ${e.message}`); }
|
|
459
|
+
});
|
|
460
|
+
const copyBtn = $('copyPathBtn');
|
|
461
|
+
if (copyBtn) copyBtn.addEventListener('click', async () => {
|
|
462
|
+
try { await navigator.clipboard.writeText(it.path); toast('Path copied'); }
|
|
463
|
+
catch { toast('Copy failed'); }
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
|
|
468
|
+
//#region EVENTS
|
|
469
|
+
function bindResize() {
|
|
470
|
+
const handle = $('resizeHandle');
|
|
471
|
+
const panel = $('treePanel');
|
|
472
|
+
let dragging = false;
|
|
473
|
+
handle.addEventListener('mousedown', () => {
|
|
474
|
+
dragging = true;
|
|
475
|
+
handle.classList.add('is-dragging');
|
|
476
|
+
document.body.style.cursor = 'col-resize';
|
|
477
|
+
document.body.style.userSelect = 'none';
|
|
478
|
+
});
|
|
479
|
+
window.addEventListener('mousemove', (e) => {
|
|
480
|
+
if (!dragging) return;
|
|
481
|
+
const layout = panel.parentElement;
|
|
482
|
+
const rect = layout.getBoundingClientRect();
|
|
483
|
+
const w = Math.max(220, Math.min(rect.width - 280, e.clientX - rect.left));
|
|
484
|
+
panel.style.flex = `0 0 ${w}px`;
|
|
485
|
+
try { localStorage.setItem('inspect.sidebarW', String(w)); } catch {}
|
|
486
|
+
});
|
|
487
|
+
window.addEventListener('mouseup', () => {
|
|
488
|
+
if (!dragging) return;
|
|
489
|
+
dragging = false;
|
|
490
|
+
handle.classList.remove('is-dragging');
|
|
491
|
+
document.body.style.cursor = '';
|
|
492
|
+
document.body.style.userSelect = '';
|
|
493
|
+
});
|
|
494
|
+
try {
|
|
495
|
+
const w = Number(localStorage.getItem('inspect.sidebarW'));
|
|
496
|
+
if (w >= 220) panel.style.flex = `0 0 ${w}px`;
|
|
497
|
+
} catch {}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function bindEvents() {
|
|
501
|
+
$('sessionSelect').addEventListener('change', async (e) => {
|
|
502
|
+
const id = e.target.value || null;
|
|
503
|
+
setUrlSession(id);
|
|
504
|
+
await loadSnapshot(id);
|
|
505
|
+
state.selected = null;
|
|
506
|
+
renderTopbar();
|
|
507
|
+
renderTree();
|
|
508
|
+
renderDetail();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
$('kindFilter').addEventListener('change', (e) => {
|
|
512
|
+
state.kind = e.target.value;
|
|
513
|
+
renderTree();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
$('searchInput').addEventListener('input', (e) => {
|
|
517
|
+
state.search = e.target.value;
|
|
518
|
+
renderTree();
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
$('refreshBtn').addEventListener('click', async () => {
|
|
522
|
+
const btn = $('refreshBtn');
|
|
523
|
+
btn.classList.add('loading');
|
|
524
|
+
await loadSessions();
|
|
525
|
+
await loadSnapshot(state.currentSessionId);
|
|
526
|
+
renderTopbar();
|
|
527
|
+
renderTree();
|
|
528
|
+
renderDetail();
|
|
529
|
+
setTimeout(() => btn.classList.remove('loading'), 200);
|
|
530
|
+
toast('refreshed');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
$('themeBtn').addEventListener('click', () => {
|
|
534
|
+
document.body.classList.toggle('light');
|
|
535
|
+
try {
|
|
536
|
+
localStorage.setItem('inspect.theme', document.body.classList.contains('light') ? 'light' : 'dark');
|
|
537
|
+
} catch {}
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
$('expandToggle').addEventListener('click', () => {
|
|
541
|
+
state.expandAll = !state.expandAll;
|
|
542
|
+
for (const k of KIND_ORDER) state.expanded[k] = state.expandAll;
|
|
543
|
+
$('expandToggle').textContent = state.expandAll ? 'Collapse all' : 'Expand all';
|
|
544
|
+
renderTree();
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
window.addEventListener('popstate', async () => {
|
|
548
|
+
await loadSnapshot(getUrlSession());
|
|
549
|
+
renderTopbar();
|
|
550
|
+
renderTree();
|
|
551
|
+
renderDetail();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
window.addEventListener('keydown', (e) => {
|
|
555
|
+
if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'SELECT') return;
|
|
556
|
+
if (e.key === '/') { e.preventDefault(); $('searchInput').focus(); }
|
|
557
|
+
else if (e.key === 'r' || e.key === 'R') $('refreshBtn').click();
|
|
558
|
+
else if (e.key === 't' || e.key === 'T') $('themeBtn').click();
|
|
559
|
+
else if (e.key === 'Escape') { state.selected = null; renderTree(); renderDetail(); }
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function bindSse() {
|
|
564
|
+
const es = new EventSource('/api/events');
|
|
565
|
+
es.addEventListener('snapshot', async () => {
|
|
566
|
+
await loadSessions();
|
|
567
|
+
await loadSnapshot(state.currentSessionId);
|
|
568
|
+
renderTopbar();
|
|
569
|
+
renderTree();
|
|
570
|
+
renderDetail();
|
|
571
|
+
});
|
|
572
|
+
es.addEventListener('navigate', async (ev) => {
|
|
573
|
+
try {
|
|
574
|
+
const { session } = JSON.parse(ev.data);
|
|
575
|
+
const sid = session || (state.sessions[0] && state.sessions[0].id);
|
|
576
|
+
if (!sid) return;
|
|
577
|
+
const u = new URL(window.location.href);
|
|
578
|
+
u.searchParams.set('session', sid);
|
|
579
|
+
window.history.replaceState({}, '', u);
|
|
580
|
+
state.currentSessionId = sid;
|
|
581
|
+
await loadSnapshot(sid);
|
|
582
|
+
renderTopbar();
|
|
583
|
+
renderTree();
|
|
584
|
+
renderDetail();
|
|
585
|
+
try { window.focus(); } catch {}
|
|
586
|
+
} catch {}
|
|
587
|
+
});
|
|
588
|
+
es.onerror = () => {};
|
|
589
|
+
}
|
|
590
|
+
//#endregion
|
|
591
|
+
|
|
592
|
+
//#region INIT
|
|
593
|
+
(async function init() {
|
|
594
|
+
try {
|
|
595
|
+
if (localStorage.getItem('inspect.theme') === 'light') document.body.classList.add('light');
|
|
596
|
+
} catch {}
|
|
597
|
+
|
|
598
|
+
bindResize();
|
|
599
|
+
bindEvents();
|
|
600
|
+
|
|
601
|
+
await loadSessions();
|
|
602
|
+
const requested = getUrlSession();
|
|
603
|
+
await loadSnapshot(requested);
|
|
604
|
+
if (state.currentSessionId && !requested) setUrlSession(state.currentSessionId, true);
|
|
605
|
+
|
|
606
|
+
if (state.snapshot?.systemPrompt) {
|
|
607
|
+
const firstCtx = buildItems().find((x) => x.kind === 'context');
|
|
608
|
+
if (firstCtx) state.selected = firstCtx.id;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
renderTopbar();
|
|
612
|
+
renderTree();
|
|
613
|
+
renderDetail();
|
|
614
|
+
bindSse();
|
|
615
|
+
})();
|
|
616
|
+
//#endregion
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#1a1a1a"/><circle cx="16" cy="16" r="5" fill="none" stroke="#e8927c" stroke-width="2"/><line x1="19.5" y1="19.5" x2="23.5" y2="23.5" stroke="#e8927c" stroke-width="2" stroke-linecap="round"/></svg>
|
package/public/icon.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#1a1a1a"/><circle cx="16" cy="16" r="6" fill="none" stroke="#e8927c" stroke-width="2"/><line x1="21" y1="21" x2="27" y2="27" stroke="#e8927c" stroke-width="2" stroke-linecap="round"/></svg>
|