@smithers-orchestrator/pi-plugin 0.16.9
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/LICENSE +21 -0
- package/package.json +64 -0
- package/src/SmithersPiRunContext.ts +7 -0
- package/src/api/SmithersPiHttpClient.ts +86 -0
- package/src/api/approve.ts +23 -0
- package/src/api/cancel.ts +14 -0
- package/src/api/deny.ts +23 -0
- package/src/api/getFrames.ts +14 -0
- package/src/api/getStatus.ts +11 -0
- package/src/api/listRuns.ts +20 -0
- package/src/api/resume.ts +19 -0
- package/src/api/runWorkflow.ts +20 -0
- package/src/api/streamEvents.ts +11 -0
- package/src/buildSmithersPiSystemPrompt.ts +120 -0
- package/src/extension.ts +571 -0
- package/src/index.d.ts +443 -0
- package/src/index.ts +18 -0
- package/src/runtime/DevToolsClient.ts +528 -0
- package/src/runtime/DevToolsStore.ts +927 -0
- package/src/views/FrameScrubber.ts +72 -0
- package/src/views/Header.ts +144 -0
- package/src/views/NodeInspector.ts +221 -0
- package/src/views/RunInspector.ts +232 -0
- package/src/views/RunTree.ts +404 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import type { DevToolsNode } from "@smithers-orchestrator/protocol";
|
|
3
|
+
import type { DevToolsStore } from "../runtime/DevToolsStore.js";
|
|
4
|
+
|
|
5
|
+
type Theme = {
|
|
6
|
+
fg?: (color: string, value: string) => string;
|
|
7
|
+
bold?: (value: string) => string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type TreeRow = {
|
|
11
|
+
node: DevToolsNode;
|
|
12
|
+
depth: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function paint(theme: Theme, color: string, value: string) {
|
|
16
|
+
return theme.fg ? theme.fg(color, value) : value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function bold(theme: Theme, value: string) {
|
|
20
|
+
return theme.bold ? theme.bold(value) : value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stateOf(node: DevToolsNode) {
|
|
24
|
+
const raw = node.props.state;
|
|
25
|
+
return typeof raw === "string" ? raw : "unknown";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizedState(node: DevToolsNode) {
|
|
29
|
+
return stateOf(node).trim().toLowerCase().replace(/[_\s]/g, "-");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stateIcon(node: DevToolsNode) {
|
|
33
|
+
switch (normalizedState(node)) {
|
|
34
|
+
case "running":
|
|
35
|
+
case "in-progress":
|
|
36
|
+
return ">";
|
|
37
|
+
case "finished":
|
|
38
|
+
case "complete":
|
|
39
|
+
case "completed":
|
|
40
|
+
case "success":
|
|
41
|
+
case "succeeded":
|
|
42
|
+
case "done":
|
|
43
|
+
return "v";
|
|
44
|
+
case "failed":
|
|
45
|
+
case "error":
|
|
46
|
+
return "x";
|
|
47
|
+
case "blocked":
|
|
48
|
+
case "waitingapproval":
|
|
49
|
+
case "waiting-approval":
|
|
50
|
+
case "waiting-timer":
|
|
51
|
+
return "!";
|
|
52
|
+
case "cancelled":
|
|
53
|
+
case "canceled":
|
|
54
|
+
return "-";
|
|
55
|
+
default:
|
|
56
|
+
return "o";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function stateColor(node: DevToolsNode) {
|
|
61
|
+
switch (stateIcon(node)) {
|
|
62
|
+
case ">":
|
|
63
|
+
return "accent";
|
|
64
|
+
case "v":
|
|
65
|
+
return "success";
|
|
66
|
+
case "x":
|
|
67
|
+
return "error";
|
|
68
|
+
case "!":
|
|
69
|
+
return "warning";
|
|
70
|
+
case "-":
|
|
71
|
+
return "dim";
|
|
72
|
+
default:
|
|
73
|
+
return "muted";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function nodeLabel(node: DevToolsNode) {
|
|
78
|
+
return node.task?.label ?? node.task?.nodeId ?? node.name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function propsSummary(node: DevToolsNode, maxLength = 80) {
|
|
82
|
+
const parts: string[] = [];
|
|
83
|
+
const id = node.props.id;
|
|
84
|
+
const name = node.props.name;
|
|
85
|
+
if (typeof id === "string" && id.length > 0) {
|
|
86
|
+
parts.push(`id=${id}`);
|
|
87
|
+
}
|
|
88
|
+
if (typeof name === "string" && name.length > 0) {
|
|
89
|
+
parts.push(`name=${name}`);
|
|
90
|
+
}
|
|
91
|
+
if (node.task?.agent) {
|
|
92
|
+
parts.push(`agent=${node.task.agent}`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof node.task?.iteration === "number" && node.task.iteration > 0) {
|
|
95
|
+
parts.push(`iter=${node.task.iteration}`);
|
|
96
|
+
}
|
|
97
|
+
const summary = parts.join(" ");
|
|
98
|
+
return summary.length > maxLength ? `${summary.slice(0, maxLength - 1)}...` : summary;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findParent(root: DevToolsNode | undefined, childId: number): DevToolsNode | undefined {
|
|
102
|
+
if (!root) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
for (const child of root.children) {
|
|
106
|
+
if (child.id === childId) {
|
|
107
|
+
return root;
|
|
108
|
+
}
|
|
109
|
+
const found = findParent(child, childId);
|
|
110
|
+
if (found) {
|
|
111
|
+
return found;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function findNode(root: DevToolsNode | undefined, id: number): DevToolsNode | undefined {
|
|
118
|
+
if (!root) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
if (root.id === id) {
|
|
122
|
+
return root;
|
|
123
|
+
}
|
|
124
|
+
for (const child of root.children) {
|
|
125
|
+
const found = findNode(child, id);
|
|
126
|
+
if (found) {
|
|
127
|
+
return found;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function failedDescendantCount(node: DevToolsNode) {
|
|
134
|
+
let count = 0;
|
|
135
|
+
for (const child of node.children) {
|
|
136
|
+
if (stateIcon(child) === "x") {
|
|
137
|
+
count += 1;
|
|
138
|
+
}
|
|
139
|
+
count += failedDescendantCount(child);
|
|
140
|
+
}
|
|
141
|
+
return count;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectRows(node: DevToolsNode, expandedIds: Set<number>, rows: TreeRow[]) {
|
|
145
|
+
rows.push({ node, depth: node.depth });
|
|
146
|
+
if (expandedIds.has(node.id)) {
|
|
147
|
+
for (const child of node.children) {
|
|
148
|
+
collectRows(child, expandedIds, rows);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function collectPathIds(root: DevToolsNode, target: (node: DevToolsNode) => boolean) {
|
|
154
|
+
const ids = new Set<number>();
|
|
155
|
+
const walk = (node: DevToolsNode, path: number[]) => {
|
|
156
|
+
if (target(node)) {
|
|
157
|
+
for (const id of path) {
|
|
158
|
+
ids.add(id);
|
|
159
|
+
}
|
|
160
|
+
ids.add(node.id);
|
|
161
|
+
}
|
|
162
|
+
for (const child of node.children) {
|
|
163
|
+
walk(child, [...path, node.id]);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
walk(root, []);
|
|
167
|
+
return ids;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function searchText(node: DevToolsNode) {
|
|
171
|
+
return [
|
|
172
|
+
node.name,
|
|
173
|
+
node.type,
|
|
174
|
+
node.task?.nodeId,
|
|
175
|
+
node.task?.label,
|
|
176
|
+
node.task?.agent,
|
|
177
|
+
propsSummary(node, 200),
|
|
178
|
+
].filter(Boolean).join(" ").toLowerCase().normalize("NFC");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class RunTree {
|
|
182
|
+
private readonly expandedIds = new Set<number>();
|
|
183
|
+
private readonly userCollapsedIds = new Set<number>();
|
|
184
|
+
private scrollOffset = 0;
|
|
185
|
+
private searchQuery = "";
|
|
186
|
+
private searchMode = false;
|
|
187
|
+
private lastAutoSeq = -1;
|
|
188
|
+
|
|
189
|
+
constructor(private readonly store: DevToolsStore) {}
|
|
190
|
+
|
|
191
|
+
handleInput(data: string) {
|
|
192
|
+
if (this.searchMode) {
|
|
193
|
+
if (matchesKey(data, "escape")) {
|
|
194
|
+
this.searchMode = false;
|
|
195
|
+
this.searchQuery = "";
|
|
196
|
+
return "handled";
|
|
197
|
+
}
|
|
198
|
+
if (matchesKey(data, "enter")) {
|
|
199
|
+
this.searchMode = false;
|
|
200
|
+
return "handled";
|
|
201
|
+
}
|
|
202
|
+
if (data === "\x7f" || matchesKey(data, "backspace")) {
|
|
203
|
+
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
204
|
+
return "handled";
|
|
205
|
+
}
|
|
206
|
+
if (data.length === 1 && data >= " ") {
|
|
207
|
+
this.searchQuery += data;
|
|
208
|
+
return "handled";
|
|
209
|
+
}
|
|
210
|
+
return "handled";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const rows = this.visibleRows();
|
|
214
|
+
if (matchesKey(data, "/") || matchesKey(data, "f")) {
|
|
215
|
+
this.searchMode = true;
|
|
216
|
+
return "handled";
|
|
217
|
+
}
|
|
218
|
+
if (matchesKey(data, "j") || data === "\x1b[B") {
|
|
219
|
+
this.moveSelection(1, rows);
|
|
220
|
+
return "handled";
|
|
221
|
+
}
|
|
222
|
+
if (matchesKey(data, "k") || data === "\x1b[A") {
|
|
223
|
+
this.moveSelection(-1, rows);
|
|
224
|
+
return "handled";
|
|
225
|
+
}
|
|
226
|
+
if (data === "\x1b[D") {
|
|
227
|
+
this.collapseSelected();
|
|
228
|
+
return "handled";
|
|
229
|
+
}
|
|
230
|
+
if (data === "\x1b[C") {
|
|
231
|
+
this.expandSelected(rows);
|
|
232
|
+
return "handled";
|
|
233
|
+
}
|
|
234
|
+
if (matchesKey(data, "home") || matchesKey(data, "g")) {
|
|
235
|
+
this.selectRow(rows[0]);
|
|
236
|
+
return "handled";
|
|
237
|
+
}
|
|
238
|
+
if (matchesKey(data, "end") || matchesKey(data, "shift+g")) {
|
|
239
|
+
this.selectRow(rows[rows.length - 1]);
|
|
240
|
+
return "handled";
|
|
241
|
+
}
|
|
242
|
+
if (matchesKey(data, "enter")) {
|
|
243
|
+
return "focusInspector";
|
|
244
|
+
}
|
|
245
|
+
return "unhandled";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
render(width: number, height: number, theme: Theme) {
|
|
249
|
+
const W = Math.max(28, width);
|
|
250
|
+
const H = Math.max(3, height);
|
|
251
|
+
this.rebuildAutoExpansion();
|
|
252
|
+
const rows = this.visibleRows();
|
|
253
|
+
this.ensureSelection(rows);
|
|
254
|
+
this.ensureScroll(rows, H - 2);
|
|
255
|
+
const query = this.searchQuery.trim().toLowerCase().normalize("NFC");
|
|
256
|
+
const header = this.searchMode
|
|
257
|
+
? paint(theme, "accent", ` /${this.searchQuery}`)
|
|
258
|
+
: paint(theme, "muted", ` tree ${rows.length} rows / search`);
|
|
259
|
+
const lines = [truncateToWidth(header, W)];
|
|
260
|
+
const visible = rows.slice(this.scrollOffset, this.scrollOffset + H - 2);
|
|
261
|
+
for (const row of visible) {
|
|
262
|
+
lines.push(this.renderRow(row, W, theme, query));
|
|
263
|
+
}
|
|
264
|
+
while (lines.length < H) {
|
|
265
|
+
lines.push("");
|
|
266
|
+
}
|
|
267
|
+
return lines.slice(0, H);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
visibleRows() {
|
|
271
|
+
const root = this.store.tree;
|
|
272
|
+
if (!root) {
|
|
273
|
+
return [];
|
|
274
|
+
}
|
|
275
|
+
const rows: TreeRow[] = [];
|
|
276
|
+
collectRows(root, this.expandedIds, rows);
|
|
277
|
+
if (!this.searchQuery.trim()) {
|
|
278
|
+
return rows;
|
|
279
|
+
}
|
|
280
|
+
const query = this.searchQuery.trim().toLowerCase().normalize("NFC");
|
|
281
|
+
return rows.filter((row) => searchText(row.node).includes(query));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private renderRow(row: TreeRow, width: number, theme: Theme, query: string) {
|
|
285
|
+
const node = row.node;
|
|
286
|
+
const selected = this.store.selectedNodeId === node.id;
|
|
287
|
+
const expanded = this.expandedIds.has(node.id);
|
|
288
|
+
const chevron = node.children.length === 0 ? " " : expanded ? "v" : ">";
|
|
289
|
+
const failedCount = failedDescendantCount(node);
|
|
290
|
+
const failedBubble = failedCount > 0 && !expanded ? paint(theme, "error", ` !${failedCount}`) : "";
|
|
291
|
+
const ghost = this.store.isGhostNode(node) ? paint(theme, "dim", " ghost") : "";
|
|
292
|
+
const searchMatch = query && searchText(node).includes(query);
|
|
293
|
+
const marker = selected ? paint(theme, "accent", ">") : " ";
|
|
294
|
+
const label = searchMatch ? bold(theme, nodeLabel(node)) : nodeLabel(node);
|
|
295
|
+
const summary = propsSummary(node);
|
|
296
|
+
const dim = query && !searchMatch ? "dim" : "muted";
|
|
297
|
+
const indent = " ".repeat(Math.min(20, row.depth * 2));
|
|
298
|
+
const line =
|
|
299
|
+
`${marker}${indent}${chevron} ${paint(theme, stateColor(node), stateIcon(node))} ` +
|
|
300
|
+
`${paint(theme, selected ? "accent" : "muted", `<${node.type}>`)} ` +
|
|
301
|
+
`${paint(theme, selected ? "accent" : "default", label)} ` +
|
|
302
|
+
`${paint(theme, dim, summary)}${failedBubble}${ghost}`;
|
|
303
|
+
return truncateToWidth(line, width);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private rebuildAutoExpansion() {
|
|
307
|
+
const root = this.store.tree;
|
|
308
|
+
if (!root || this.lastAutoSeq === this.store.seq) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.lastAutoSeq = this.store.seq;
|
|
312
|
+
this.expandedIds.add(root.id);
|
|
313
|
+
const running = collectPathIds(root, (node) => stateIcon(node) === ">");
|
|
314
|
+
const failed = collectPathIds(root, (node) => stateIcon(node) === "x");
|
|
315
|
+
for (const id of [...running, ...failed]) {
|
|
316
|
+
if (!this.userCollapsedIds.has(id)) {
|
|
317
|
+
this.expandedIds.add(id);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private ensureSelection(rows: TreeRow[]) {
|
|
323
|
+
if (rows.length === 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (this.store.selectedNodeId === undefined || !rows.some((row) => row.node.id === this.store.selectedNodeId)) {
|
|
327
|
+
this.store.selectNode(rows[0].node.id);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private ensureScroll(rows: TreeRow[], visibleCount: number) {
|
|
332
|
+
const selectedIndex = rows.findIndex((row) => row.node.id === this.store.selectedNodeId);
|
|
333
|
+
if (selectedIndex < 0) {
|
|
334
|
+
this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, rows.length - visibleCount));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (selectedIndex < this.scrollOffset) {
|
|
338
|
+
this.scrollOffset = selectedIndex;
|
|
339
|
+
} else if (selectedIndex >= this.scrollOffset + visibleCount) {
|
|
340
|
+
this.scrollOffset = selectedIndex - visibleCount + 1;
|
|
341
|
+
}
|
|
342
|
+
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, Math.max(0, rows.length - visibleCount)));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private moveSelection(delta: number, rows: TreeRow[]) {
|
|
346
|
+
if (rows.length === 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const selectedIndex = rows.findIndex((row) => row.node.id === this.store.selectedNodeId);
|
|
350
|
+
const nextIndex =
|
|
351
|
+
selectedIndex < 0
|
|
352
|
+
? delta > 0
|
|
353
|
+
? 0
|
|
354
|
+
: rows.length - 1
|
|
355
|
+
: Math.max(0, Math.min(rows.length - 1, selectedIndex + delta));
|
|
356
|
+
this.selectRow(rows[nextIndex]);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private collapseSelected() {
|
|
360
|
+
const selectedId = this.store.selectedNodeId;
|
|
361
|
+
if (selectedId === undefined) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const node = findNode(this.store.tree, selectedId);
|
|
365
|
+
if (!node) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (node.children.length > 0 && this.expandedIds.has(selectedId)) {
|
|
369
|
+
this.expandedIds.delete(selectedId);
|
|
370
|
+
this.userCollapsedIds.add(selectedId);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const parent = findParent(this.store.tree, selectedId);
|
|
374
|
+
if (parent) {
|
|
375
|
+
this.store.selectNode(parent.id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private expandSelected(rows: TreeRow[]) {
|
|
380
|
+
const selectedId = this.store.selectedNodeId;
|
|
381
|
+
if (selectedId === undefined) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const index = rows.findIndex((row) => row.node.id === selectedId);
|
|
385
|
+
if (index < 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const node = rows[index].node;
|
|
389
|
+
if (node.children.length > 0 && !this.expandedIds.has(selectedId)) {
|
|
390
|
+
this.expandedIds.add(selectedId);
|
|
391
|
+
this.userCollapsedIds.delete(selectedId);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
if (node.children.length > 0 && index < rows.length - 1) {
|
|
395
|
+
this.store.selectNode(rows[index + 1].node.id);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private selectRow(row: TreeRow | undefined) {
|
|
400
|
+
if (row) {
|
|
401
|
+
this.store.selectNode(row.node.id);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|