@tt-a1i/openpi 0.3.1 → 0.4.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 +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +30 -42
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +204 -140
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* /ps UI — two-stage
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - TerminalDetailView: read-only inspector for one terminal —
|
|
6
|
-
* stdout/stderr toggle, scrolling, live tail. No input surface:
|
|
7
|
-
* terminals have no stdin by design.
|
|
2
|
+
* /ps UI — two-stage inspector over the synchronous TerminalReadModel:
|
|
3
|
+
* - TerminalDashboard: compact picker docked above the input, listing all
|
|
4
|
+
* tracked terminals (select, kill, open).
|
|
5
|
+
* - TerminalDetailView: full-screen read-only inspector for one terminal —
|
|
6
|
+
* metadata, stdout/stderr toggle, scrolling, live tail. No input surface:
|
|
7
|
+
* background terminals have no stdin by design.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type {
|
|
@@ -15,6 +15,13 @@ import type {
|
|
|
15
15
|
import { formatSize } from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
17
17
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
18
|
+
import {
|
|
19
|
+
hintLine,
|
|
20
|
+
overflowNote,
|
|
21
|
+
panelFrame,
|
|
22
|
+
screenTitleLine,
|
|
23
|
+
} from "../../../shared/screen-chrome.ts";
|
|
24
|
+
import { spinnerFrame } from "../../../shared/spinner.ts";
|
|
18
25
|
import {
|
|
19
26
|
formatDuration,
|
|
20
27
|
formatElapsed,
|
|
@@ -37,18 +44,30 @@ function configuredKeys(
|
|
|
37
44
|
return keybindings.getKeys(binding).join("/") || "unbound";
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
/**
|
|
48
|
+
* One status indicator per state, shared by the picker rows and the detail
|
|
49
|
+
* header. Running spins, in step with every other OpenPI surface. A selected
|
|
50
|
+
* row keeps its state glyph and borrows the accent tone.
|
|
51
|
+
*/
|
|
52
|
+
function statusGlyph(
|
|
53
|
+
snap: TerminalSnapshot,
|
|
54
|
+
theme: Theme,
|
|
55
|
+
now = Date.now(),
|
|
56
|
+
selected = false,
|
|
57
|
+
) {
|
|
58
|
+
const tone = (color: "warning" | "success" | "error" | "muted") =>
|
|
59
|
+
selected ? ("accent" as const) : color;
|
|
41
60
|
switch (snap.status) {
|
|
42
61
|
case "running":
|
|
43
|
-
return theme.fg("warning",
|
|
62
|
+
return theme.fg(tone("warning"), spinnerFrame(now));
|
|
44
63
|
case "done":
|
|
45
|
-
return theme.fg("success", "
|
|
64
|
+
return theme.fg(tone("success"), "✓");
|
|
46
65
|
case "failed":
|
|
47
|
-
return theme.fg("error", "
|
|
66
|
+
return theme.fg(tone("error"), "✗");
|
|
48
67
|
case "killed":
|
|
49
|
-
return theme.fg("muted", "
|
|
68
|
+
return theme.fg(tone("muted"), "✓");
|
|
50
69
|
case "timed_out":
|
|
51
|
-
return theme.fg("error", "
|
|
70
|
+
return theme.fg(tone("error"), "✗");
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
|
|
@@ -86,7 +105,14 @@ export async function openTerminalPicker(
|
|
|
86
105
|
new TerminalDashboard(tui, theme, keybindings, view, selection, done),
|
|
87
106
|
{
|
|
88
107
|
overlay: true,
|
|
89
|
-
|
|
108
|
+
// Dock the picker just above the editor (editor + strip + footer ≈ 5
|
|
109
|
+
// rows) like a command palette, instead of blanking the conversation.
|
|
110
|
+
overlayOptions: {
|
|
111
|
+
anchor: "bottom-center",
|
|
112
|
+
width: "100%",
|
|
113
|
+
maxHeight: "60%",
|
|
114
|
+
margin: { bottom: 5 },
|
|
115
|
+
},
|
|
90
116
|
},
|
|
91
117
|
);
|
|
92
118
|
|
|
@@ -105,7 +131,10 @@ export async function openTerminalPicker(
|
|
|
105
131
|
}
|
|
106
132
|
}
|
|
107
133
|
|
|
108
|
-
// --- Dashboard (
|
|
134
|
+
// --- Dashboard (picker docked above the input) ---------------------------------
|
|
135
|
+
|
|
136
|
+
/** A picker is a glance, not a workspace: cap the list window and scroll. */
|
|
137
|
+
const MAX_PICKER_ROWS = 10;
|
|
109
138
|
|
|
110
139
|
export interface DashboardSelection {
|
|
111
140
|
id?: string;
|
|
@@ -217,92 +246,44 @@ class TerminalDashboard implements Component {
|
|
|
217
246
|
}
|
|
218
247
|
}
|
|
219
248
|
|
|
220
|
-
private pad(text: string, width: number): string {
|
|
221
|
-
const truncated = truncateToWidth(text, width);
|
|
222
|
-
return truncated + " ".repeat(Math.max(0, width - visibleWidth(truncated)));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private borderSegment(width: number, title: string): string {
|
|
226
|
-
const theme = this.theme;
|
|
227
|
-
const label = title
|
|
228
|
-
? ` ${truncateToWidth(title, Math.max(0, width - 3))} `
|
|
229
|
-
: "";
|
|
230
|
-
const labelWidth = visibleWidth(label);
|
|
231
|
-
return (
|
|
232
|
-
theme.fg("border", "─") +
|
|
233
|
-
(label ? theme.fg("text", label) : "") +
|
|
234
|
-
theme.fg("border", "─".repeat(Math.max(0, width - 1 - labelWidth)))
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
249
|
render(width: number): string[] {
|
|
239
250
|
const theme = this.theme;
|
|
240
251
|
const terminals = this.terminals();
|
|
241
252
|
reconcileDashboardSelection(this.selection, terminals);
|
|
242
253
|
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
);
|
|
258
|
-
const headerPad = Math.max(
|
|
259
|
-
1,
|
|
260
|
-
width - visibleWidth(headerLeft) - visibleWidth(headerRight) - 4,
|
|
261
|
-
);
|
|
262
|
-
lines.push(
|
|
263
|
-
truncateToWidth(
|
|
264
|
-
` ${headerLeft}${" ".repeat(headerPad)}${headerRight} `,
|
|
254
|
+
// Size the panel to its content (bounded, scrolling past the cap) so the
|
|
255
|
+
// docked picker never covers more conversation than the list needs.
|
|
256
|
+
const bodyHeight = Math.min(Math.max(terminals.length, 1), MAX_PICKER_ROWS);
|
|
257
|
+
const running = terminals.filter((s) => s.status === "running").length;
|
|
258
|
+
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
259
|
+
configuredKeys(this.keybindings, binding);
|
|
260
|
+
|
|
261
|
+
return [
|
|
262
|
+
// One empty row of air between the conversation and the docked panel.
|
|
263
|
+
"",
|
|
264
|
+
screenTitleLine(
|
|
265
|
+
theme,
|
|
266
|
+
"Background terminals",
|
|
267
|
+
`${terminals.length} terminal${terminals.length === 1 ? "" : "s"}`,
|
|
265
268
|
width,
|
|
266
269
|
),
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const divider = theme.fg("border", "│");
|
|
282
|
-
const rowLines = this.renderRows(terminals, innerWidth, bodyHeight);
|
|
283
|
-
for (let i = 0; i < bodyHeight; i++) {
|
|
284
|
-
lines.push(divider + this.pad(rowLines[i] ?? "", innerWidth) + divider);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Bottom border
|
|
288
|
-
lines.push(
|
|
289
|
-
theme.fg("border", "╰") +
|
|
290
|
-
theme.fg("border", "─".repeat(Math.max(0, innerWidth))) +
|
|
291
|
-
theme.fg("border", "╯"),
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Hints
|
|
295
|
-
lines.push(
|
|
296
|
-
truncateToWidth(
|
|
297
|
-
theme.fg(
|
|
298
|
-
"dim",
|
|
299
|
-
` ${configuredKeys(this.keybindings, "tui.select.up")}/${configuredKeys(this.keybindings, "tui.select.down")}/jk select · ${configuredKeys(this.keybindings, "tui.select.confirm")} inspect · x kill · ${configuredKeys(this.keybindings, "tui.select.cancel")} close`,
|
|
300
|
-
),
|
|
270
|
+
...panelFrame(theme, {
|
|
271
|
+
label: `terminals · ${running}/${terminals.length} running`,
|
|
272
|
+
rows: this.renderRows(terminals, width - 2, bodyHeight),
|
|
273
|
+
width,
|
|
274
|
+
height: bodyHeight + 2,
|
|
275
|
+
}),
|
|
276
|
+
hintLine(
|
|
277
|
+
theme,
|
|
278
|
+
[
|
|
279
|
+
[`${keys("tui.select.up")}/${keys("tui.select.down")}/jk`, "select"],
|
|
280
|
+
[keys("tui.select.confirm"), "inspect"],
|
|
281
|
+
["x", "kill"],
|
|
282
|
+
[keys("tui.select.cancel"), "close"],
|
|
283
|
+
],
|
|
301
284
|
width,
|
|
302
285
|
),
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return lines;
|
|
286
|
+
];
|
|
306
287
|
}
|
|
307
288
|
|
|
308
289
|
private renderRows(
|
|
@@ -328,12 +309,13 @@ class TerminalDashboard implements Component {
|
|
|
328
309
|
const index = start + i;
|
|
329
310
|
const isSelected = index === this.selection.index;
|
|
330
311
|
|
|
331
|
-
// Left:
|
|
332
|
-
|
|
312
|
+
// Left: one glyph — a selected row tints its status glyph instead of
|
|
313
|
+
// stacking a marker — then title and dim id.
|
|
314
|
+
const glyph = statusGlyph(snap, theme, Date.now(), isSelected);
|
|
333
315
|
const title = isSelected
|
|
334
316
|
? theme.fg("accent", oneLine(snap.title))
|
|
335
317
|
: theme.fg("text", oneLine(snap.title));
|
|
336
|
-
const left = ` ${
|
|
318
|
+
const left = ` ${glyph} ${title} ${theme.fg("dim", snap.id)}`;
|
|
337
319
|
|
|
338
320
|
// Right: pid · elapsed · exit/status
|
|
339
321
|
const dot = theme.fg("dim", " · ");
|
|
@@ -359,13 +341,13 @@ class TerminalDashboard implements Component {
|
|
|
359
341
|
out.push(truncateToWidth(leftTruncated + " ".repeat(gap) + right, width));
|
|
360
342
|
}
|
|
361
343
|
|
|
362
|
-
if (start > 0)
|
|
363
|
-
out[0] = truncateToWidth(theme.fg("dim", ` ... ${start} more`), width);
|
|
364
|
-
}
|
|
344
|
+
if (start > 0) out[0] = overflowNote(theme, start, width, "above");
|
|
365
345
|
if (start + height < terminals.length) {
|
|
366
|
-
out[out.length - 1] =
|
|
367
|
-
theme
|
|
346
|
+
out[out.length - 1] = overflowNote(
|
|
347
|
+
theme,
|
|
348
|
+
terminals.length - start - height,
|
|
368
349
|
width,
|
|
350
|
+
"below",
|
|
369
351
|
);
|
|
370
352
|
}
|
|
371
353
|
return out;
|
|
@@ -514,35 +496,43 @@ class TerminalDetailView implements Component {
|
|
|
514
496
|
|
|
515
497
|
render(width: number): string[] {
|
|
516
498
|
const theme = this.theme;
|
|
517
|
-
|
|
499
|
+
// One accent rule opens and closes the overlay; interior seams stay quiet
|
|
500
|
+
// so the output, not the frame, is what the eye lands on.
|
|
501
|
+
const edge = theme.fg("borderAccent", "─".repeat(Math.max(1, width)));
|
|
502
|
+
const seam = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
|
|
518
503
|
const lines: string[] = [];
|
|
519
504
|
const snap = this.snap();
|
|
520
505
|
|
|
521
506
|
if (!snap) {
|
|
522
|
-
lines.push(
|
|
507
|
+
lines.push(edge);
|
|
523
508
|
lines.push(theme.fg("dim", `${this.id} is no longer tracked`));
|
|
524
|
-
lines.push(
|
|
509
|
+
lines.push(edge);
|
|
525
510
|
return lines;
|
|
526
511
|
}
|
|
527
512
|
|
|
528
|
-
lines.push(
|
|
529
|
-
const
|
|
513
|
+
lines.push(edge);
|
|
514
|
+
const dot = theme.fg("dim", " · ");
|
|
515
|
+
const header = [
|
|
530
516
|
`${statusGlyph(snap, theme)} ` +
|
|
531
|
-
|
|
532
|
-
theme
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
517
|
+
theme.fg("accent", theme.bold(`${snap.id} · ${oneLine(snap.title)}`)),
|
|
518
|
+
statusWord(snap, theme),
|
|
519
|
+
theme.fg("muted", formatElapsed(snap)),
|
|
520
|
+
theme.fg("muted", `pid ${snap.pid ?? "?"}`),
|
|
521
|
+
...(snap.status !== "running"
|
|
522
|
+
? [theme.fg("muted", formatExit(snap))]
|
|
523
|
+
: []),
|
|
524
|
+
...(snap.status === "running" && snap.timeoutAt !== undefined
|
|
525
|
+
? [
|
|
526
|
+
theme.fg(
|
|
527
|
+
"warning",
|
|
528
|
+
`${formatDuration((snap.timeoutAt - Date.now()) / 1_000)} left`,
|
|
529
|
+
),
|
|
530
|
+
]
|
|
531
|
+
: []),
|
|
532
|
+
theme.fg("dim", snap.cwd),
|
|
533
|
+
]
|
|
534
|
+
.filter(Boolean)
|
|
535
|
+
.join(dot);
|
|
546
536
|
lines.push(truncateToWidth(header, width));
|
|
547
537
|
lines.push(
|
|
548
538
|
truncateToWidth(
|
|
@@ -550,7 +540,7 @@ class TerminalDetailView implements Component {
|
|
|
550
540
|
width,
|
|
551
541
|
),
|
|
552
542
|
);
|
|
553
|
-
lines.push(
|
|
543
|
+
lines.push(seam);
|
|
554
544
|
|
|
555
545
|
// Stream tab line: which stream is active, both sizes.
|
|
556
546
|
const active = this.stream;
|
|
@@ -561,7 +551,7 @@ class TerminalDetailView implements Component {
|
|
|
561
551
|
: theme.fg("dim", `${name} (${formatSize(size)})`);
|
|
562
552
|
lines.push(
|
|
563
553
|
truncateToWidth(
|
|
564
|
-
`
|
|
554
|
+
` ${tab("stdout", snap.stdout.totalBytes)}${theme.fg("dim", " · ")}${tab("stderr", snap.stderr.totalBytes)}${theme.fg("dim", " t")} ${theme.fg("dim", "switch")}`,
|
|
565
555
|
width,
|
|
566
556
|
),
|
|
567
557
|
);
|
|
@@ -616,7 +606,7 @@ class TerminalDetailView implements Component {
|
|
|
616
606
|
if (this.scrollOffset > 0) {
|
|
617
607
|
body.push(
|
|
618
608
|
truncateToWidth(
|
|
619
|
-
theme.fg("dim",
|
|
609
|
+
theme.fg("dim", `… ${this.scrollOffset} lines below · ↓/pgdn`),
|
|
620
610
|
width,
|
|
621
611
|
),
|
|
622
612
|
);
|
|
@@ -624,17 +614,30 @@ class TerminalDetailView implements Component {
|
|
|
624
614
|
while (body.length < viewport) body.push("");
|
|
625
615
|
lines.push(...body.slice(0, viewport));
|
|
626
616
|
|
|
627
|
-
lines.push(
|
|
617
|
+
lines.push(seam);
|
|
618
|
+
const keys = (binding: Parameters<KeybindingsManager["getKeys"]>[0]) =>
|
|
619
|
+
configuredKeys(this.keybindings, binding);
|
|
628
620
|
lines.push(
|
|
629
|
-
|
|
630
|
-
theme
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
621
|
+
hintLine(
|
|
622
|
+
theme,
|
|
623
|
+
[
|
|
624
|
+
[keys("tui.select.cancel"), "back"],
|
|
625
|
+
["t", "stdout/stderr"],
|
|
626
|
+
["x", "kill"],
|
|
627
|
+
[
|
|
628
|
+
`${keys("tui.editor.cursorUp")}/${keys("tui.editor.cursorDown")}/jk`,
|
|
629
|
+
"scroll",
|
|
630
|
+
],
|
|
631
|
+
[
|
|
632
|
+
`${keys("tui.editor.pageUp")}/${keys("tui.editor.pageDown")}`,
|
|
633
|
+
"page",
|
|
634
|
+
],
|
|
635
|
+
["g/G", "top/bottom"],
|
|
636
|
+
],
|
|
634
637
|
width,
|
|
635
638
|
),
|
|
636
639
|
);
|
|
637
|
-
lines.push(
|
|
640
|
+
lines.push(edge);
|
|
638
641
|
return lines;
|
|
639
642
|
}
|
|
640
643
|
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Static, Type } from "typebox";
|
|
4
|
+
import {
|
|
5
|
+
capabilitiesRequestedByPrompt,
|
|
6
|
+
requestsCapabilityGateway,
|
|
7
|
+
} from "../shared/capability-intent.ts";
|
|
8
|
+
import {
|
|
9
|
+
registerEditorLayer,
|
|
10
|
+
removeEditorLayer,
|
|
11
|
+
} from "../shared/editor-layers.ts";
|
|
4
12
|
import {
|
|
5
13
|
loadSetupConfig,
|
|
6
|
-
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
7
14
|
type MyPiSetupConfig,
|
|
15
|
+
SETUP_CONFIG_CHANGED_CHANNEL,
|
|
8
16
|
} from "../shared/setup-config.ts";
|
|
9
17
|
import {
|
|
10
18
|
getLoadedOpenPiCapabilities,
|
|
@@ -16,6 +24,11 @@ import {
|
|
|
16
24
|
patchOwnedTools,
|
|
17
25
|
resetOpenPiToolSurface,
|
|
18
26
|
} from "../shared/tool-surface.ts";
|
|
27
|
+
import {
|
|
28
|
+
CapabilityIntentHighlightEditor,
|
|
29
|
+
colorCapabilityKeyword,
|
|
30
|
+
isLightNamedTheme,
|
|
31
|
+
} from "./src/ui.ts";
|
|
19
32
|
|
|
20
33
|
const CapabilitySchema = Type.Unsafe<OpenPiCapability>({
|
|
21
34
|
type: "string",
|
|
@@ -35,25 +48,6 @@ const OpenPiLoadToolsParameters = Type.Object({
|
|
|
35
48
|
|
|
36
49
|
type OpenPiLoadToolsInput = Static<typeof OpenPiLoadToolsParameters>;
|
|
37
50
|
|
|
38
|
-
const CAPABILITY_INTENT = {
|
|
39
|
-
search:
|
|
40
|
-
/\b(?:use|run)\s+(?:fd|rg)\b|\buse\s+(?:structured\s+)?(?:(?:file|code|content)\s+)?search\b|\b(?:structured|fast)\s+(?:file|code|content)\s+search\b|(?:使用|运行).{0,8}(?:fd|rg)|结构化(?:文件|代码|内容)搜索/iu,
|
|
41
|
-
delegate:
|
|
42
|
-
/\b(?:use|spawn|run)\s+(?:an?\s+|multiple\s+|several\s+|two\s+)?(?:pi\s+)?subagents?\b|(?:^|[.!?]\s+)(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\b(?:can|could|would)\s+you\s+(?:please\s+)?(?:delegate|parallelize)\s+(?:this|the)\s+(?:task|work)\b|\bparallel\s+agents?\b|(?:使用|启动|调用|来|开).{0,8}子代理|(?:多个?|多路)子代理|并行.{0,8}(?:代理|agent)|委派.{0,6}任务/iu,
|
|
43
|
-
workflow:
|
|
44
|
-
/\b(?:use|run|create|build)\s+(?:(?:an?|the)\s+)?(?:openpi\s+)?workflow\b|(?:使用|运行|创建|构建).{0,8}工作流/iu,
|
|
45
|
-
background:
|
|
46
|
-
/\b(?:run|start|keep)\b.{0,40}\b(?:in the background|background\s+(?:process|terminal|job))\b|后台.{0,8}(?:运行|进程|终端|任务)/iu,
|
|
47
|
-
session:
|
|
48
|
-
/\b(?:create|set|update|track)\s+(?:an?\s+)?(?:session\s+)?(?:goal|task list|tasks)\b|(?:设置|创建|更新|跟踪|追踪).{0,8}(?:目标|任务)/iu,
|
|
49
|
-
} as const satisfies Record<OpenPiCapability, RegExp>;
|
|
50
|
-
|
|
51
|
-
const CAPABILITY_GATEWAY_INTENT =
|
|
52
|
-
/\bopenpi\s+(?:capabilit(?:y|ies)|tools?|features?)\b|openpi.{0,8}(?:能力|工具|功能)/iu;
|
|
53
|
-
|
|
54
|
-
const CONDITIONAL_OR_NEGATED_INTENT =
|
|
55
|
-
/^(?:\s*(?:only\s+)?(?:if|when|unless|before|in case)\b)|\b(?:do not|don't|cannot|can't|not|no|never|avoid)\b|\b(?:if|unless)\b|\bwhen\s+(?:needed|required|necessary)\b|(?:如果|若|假如|除非|仅当|需要时|不要|不能|不用|不必|无需|避免|请勿|禁止)/iu;
|
|
56
|
-
|
|
57
51
|
const CAPABILITY_SKILLS: Partial<Record<OpenPiCapability, string>> = {
|
|
58
52
|
delegate: fileURLToPath(
|
|
59
53
|
new URL("../../skills/subagents/SKILL.md", import.meta.url),
|
|
@@ -66,27 +60,6 @@ const CAPABILITY_SKILLS: Partial<Record<OpenPiCapability, string>> = {
|
|
|
66
60
|
),
|
|
67
61
|
};
|
|
68
62
|
|
|
69
|
-
function capabilitiesRequestedByPrompt(prompt: string) {
|
|
70
|
-
const clauses = prompt.split(/[\n.!?。!?;;]+/u);
|
|
71
|
-
return OPENPI_CAPABILITY_NAMES.filter((capability) =>
|
|
72
|
-
clauses.some(
|
|
73
|
-
(clause) =>
|
|
74
|
-
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
|
|
75
|
-
CAPABILITY_INTENT[capability].test(clause),
|
|
76
|
-
),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function requestsCapabilityGateway(prompt: string) {
|
|
81
|
-
return prompt
|
|
82
|
-
.split(/[\n.!?。!?;;]+/u)
|
|
83
|
-
.some(
|
|
84
|
-
(clause) =>
|
|
85
|
-
!CONDITIONAL_OR_NEGATED_INTENT.test(clause) &&
|
|
86
|
-
CAPABILITY_GATEWAY_INTENT.test(clause),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
63
|
function capabilitySkillPaths(capabilities: readonly OpenPiCapability[]) {
|
|
91
64
|
return capabilities.flatMap((capability) => {
|
|
92
65
|
const skill = CAPABILITY_SKILLS[capability];
|
|
@@ -124,7 +97,7 @@ export function createCapabilitiesExtension(
|
|
|
124
97
|
|
|
125
98
|
pi.events.on(SETUP_CONFIG_CHANGED_CHANNEL, reconcileDiscoveryGateway);
|
|
126
99
|
|
|
127
|
-
pi.on("session_start", () => {
|
|
100
|
+
pi.on("session_start", (_event, ctx) => {
|
|
128
101
|
resetOpenPiToolSurface(
|
|
129
102
|
pi,
|
|
130
103
|
dependencies.sourcePath
|
|
@@ -132,6 +105,21 @@ export function createCapabilitiesExtension(
|
|
|
132
105
|
: undefined,
|
|
133
106
|
);
|
|
134
107
|
reconcileDiscoveryGateway();
|
|
108
|
+
registerEditorLayer(pi, ctx, {
|
|
109
|
+
id: "capability-intent-highlight",
|
|
110
|
+
order: 150,
|
|
111
|
+
wrap: (base, _tui, _theme, keybindings) =>
|
|
112
|
+
new CapabilityIntentHighlightEditor(base, keybindings, (text) =>
|
|
113
|
+
colorCapabilityKeyword(text, {
|
|
114
|
+
colorMode: ctx.ui.theme.getColorMode(),
|
|
115
|
+
light: isLightNamedTheme(ctx.ui.theme.name),
|
|
116
|
+
}),
|
|
117
|
+
),
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
pi.on("session_shutdown", () => {
|
|
122
|
+
removeEditorLayer(pi, "capability-intent-highlight");
|
|
135
123
|
});
|
|
136
124
|
|
|
137
125
|
pi.on("before_agent_start", (event) => {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { EditorComponent } from "@earendil-works/pi-tui";
|
|
3
|
+
import {
|
|
4
|
+
BelowEditorNavigationEditor,
|
|
5
|
+
BelowEditorStripState,
|
|
6
|
+
} from "../../shared/below-editor-navigation.ts";
|
|
7
|
+
import { capabilitiesRequestedByPrompt } from "../../shared/capability-intent.ts";
|
|
8
|
+
|
|
9
|
+
const DELEGATE_NAMES = /\bsubagents?\b|子代理/giu;
|
|
10
|
+
const WORKFLOW_NAMES = /\bworkflows?\b|工作流/giu;
|
|
11
|
+
const FOREGROUND_RESET = "\u001b[39m";
|
|
12
|
+
|
|
13
|
+
interface CapabilityKeywordColorOptions {
|
|
14
|
+
readonly colorMode: "truecolor" | "256color";
|
|
15
|
+
readonly light: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isLightNamedTheme(name: string | undefined) {
|
|
19
|
+
return name !== undefined && /(?:^|[-_])light(?:$|[-_])/iu.test(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Claude-style lavender keyword color. The light variant preserves readable
|
|
24
|
+
* contrast instead of mechanically reusing the bright dark-terminal swatch.
|
|
25
|
+
*/
|
|
26
|
+
export function colorCapabilityKeyword(
|
|
27
|
+
text: string,
|
|
28
|
+
options: CapabilityKeywordColorOptions,
|
|
29
|
+
) {
|
|
30
|
+
const start = options.light
|
|
31
|
+
? options.colorMode === "truecolor"
|
|
32
|
+
? "\u001b[38;2;130;80;223m"
|
|
33
|
+
: "\u001b[38;5;98m"
|
|
34
|
+
: options.colorMode === "truecolor"
|
|
35
|
+
? "\u001b[38;2;210;168;255m"
|
|
36
|
+
: "\u001b[38;5;183m";
|
|
37
|
+
return `${start}${text}${FOREGROUND_RESET}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function highlightCapabilityNames(
|
|
41
|
+
line: string,
|
|
42
|
+
capabilities: readonly string[],
|
|
43
|
+
highlight: (text: string) => string,
|
|
44
|
+
) {
|
|
45
|
+
let result = line;
|
|
46
|
+
if (capabilities.includes("delegate")) {
|
|
47
|
+
result = result.replace(DELEGATE_NAMES, (match) => highlight(match));
|
|
48
|
+
}
|
|
49
|
+
if (capabilities.includes("workflow")) {
|
|
50
|
+
result = result.replace(WORKFLOW_NAMES, (match) => highlight(match));
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Transparent, pre-submit feedback for capability intent. It colours only
|
|
57
|
+
* names whose capability the shared classifier would load after submission;
|
|
58
|
+
* it never changes editor text, Session history, or model context.
|
|
59
|
+
*/
|
|
60
|
+
export class CapabilityIntentHighlightEditor extends BelowEditorNavigationEditor {
|
|
61
|
+
private readonly highlight: (text: string) => string;
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
base: EditorComponent,
|
|
65
|
+
keybindings: KeybindingsManager,
|
|
66
|
+
highlight: (text: string) => string,
|
|
67
|
+
) {
|
|
68
|
+
super(
|
|
69
|
+
base,
|
|
70
|
+
keybindings,
|
|
71
|
+
new BelowEditorStripState(),
|
|
72
|
+
() => false,
|
|
73
|
+
() => undefined,
|
|
74
|
+
() => undefined,
|
|
75
|
+
);
|
|
76
|
+
this.highlight = highlight;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
override render(width: number) {
|
|
80
|
+
const capabilities = capabilitiesRequestedByPrompt(this.getText());
|
|
81
|
+
if (
|
|
82
|
+
!capabilities.includes("delegate") &&
|
|
83
|
+
!capabilities.includes("workflow")
|
|
84
|
+
) {
|
|
85
|
+
return super.render(width);
|
|
86
|
+
}
|
|
87
|
+
return super
|
|
88
|
+
.render(width)
|
|
89
|
+
.map((line) =>
|
|
90
|
+
highlightCapabilityNames(line, capabilities, this.highlight),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|