@tt-a1i/openpi 0.3.1 → 0.5.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 +184 -59
- package/SETUP.md +23 -7
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/index.ts +30 -2
- package/extensions/background-terminals/src/domain.ts +2 -0
- package/extensions/background-terminals/src/manager.ts +486 -106
- package/extensions/background-terminals/src/output.ts +33 -0
- package/extensions/background-terminals/src/prompt.ts +14 -6
- package/extensions/background-terminals/src/result-delivery.ts +4 -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/clear-context/index.ts +83 -0
- package/extensions/context-pivot/index.ts +16 -6
- package/extensions/cron/schedule.ts +7 -1
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +146 -87
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +75 -59
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +328 -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/model-info/index.ts +21 -33
- package/extensions/model-info/session-metrics.ts +96 -0
- package/extensions/plan-mode/bash-policy.ts +54 -9
- package/extensions/plan-mode/index.ts +7 -2
- package/extensions/post-edit/index.ts +16 -6
- package/extensions/sessions/git-stats.ts +258 -72
- package/extensions/sessions/index.ts +222 -140
- package/extensions/sessions/preview-cache.ts +104 -0
- package/extensions/sessions/preview-loader.ts +856 -0
- package/extensions/sessions/sessions.ts +43 -4
- package/extensions/setup/index.ts +127 -131
- package/extensions/shared/activity-status.ts +36 -5
- package/extensions/shared/agent-session-page.ts +319 -0
- package/extensions/shared/agent-tool-renderer.ts +218 -0
- package/extensions/shared/agent-transcript.ts +524 -0
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +444 -22
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +97 -38
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +113 -0
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +42 -8
- package/extensions/shared/transcript-viewport.ts +46 -0
- package/extensions/shared/web-observer-registry.ts +390 -0
- package/extensions/shared/worktree.ts +11 -0
- package/extensions/subagents/index.ts +461 -186
- package/extensions/subagents/navigation.ts +86 -28
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backend.ts +12 -1
- package/extensions/subagents/src/backends/pi.ts +375 -66
- package/extensions/subagents/src/domain.ts +5 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +651 -536
- package/extensions/subagents/src/prompt.ts +185 -42
- package/extensions/subagents/src/result-artifact.ts +146 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +23 -6
- package/extensions/subagents/src/ui/takeover.ts +128 -337
- package/extensions/subagents/src/ui/transcript.ts +38 -501
- package/extensions/subagents/src/ui/wait-result.ts +103 -15
- package/extensions/suggestions/src/ui.ts +10 -4
- package/extensions/tasks/index.ts +0 -3
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -44
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +147 -22
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +62 -0
- package/extensions/workflows/dashboard.ts +458 -339
- package/extensions/workflows/handoff.ts +121 -25
- package/extensions/workflows/index.ts +1042 -492
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +131 -19
- package/extensions/workflows/navigation.ts +61 -18
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +166 -10
- package/extensions/workflows/replay-safety.ts +58 -27
- package/extensions/workflows/result-delivery.ts +253 -0
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +36 -3
- package/extensions/workflows/sandbox.ts +62 -8
- package/extensions/workflows/serialization.ts +325 -17
- package/extensions/workflows/tool-renderer.ts +22 -0
- package/extensions/workflows/transcript.ts +149 -0
- package/extensions/workspace-cleanup-guard/index.ts +54 -0
- package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
- package/package.json +28 -8
- package/skills/subagents/REFERENCE.md +189 -0
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +10 -5
- package/skills/workflows/SKILL.md +53 -10
- package/web/adapter/pi-adapter.ts +661 -0
- package/web/host/browser-launcher.ts +20 -0
- package/web/host/static-assets.ts +4 -0
- package/web/host/terminal-status.ts +38 -0
- package/web/host/web-host.ts +789 -0
- package/web/http-dispatcher.ts +125 -0
- package/web/protocol/types.ts +462 -0
- package/web/runtime/pi-runtime.ts +991 -0
- package/web/runtime/types.ts +71 -0
- package/web/runtime/web-host-lease.ts +497 -0
- package/web/trace.ts +18 -0
- package/web/ui/app.js +1398 -0
- package/web/ui/index.html +139 -0
- package/web/ui/styles.css +598 -0
- package/web/vite.config.mjs +34 -0
- package/extensions/execution-convergence/active-evidence.ts +0 -129
- package/extensions/execution-convergence/index.ts +0 -442
- package/extensions/execution-convergence/workspace-provenance.ts +0 -338
- package/extensions/setup/intercom-fs-helper.cjs +0 -130
- package/extensions/setup/intercom.ts +0 -603
- package/extensions/subagents/src/backends/stub.ts +0 -296
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -13,13 +13,17 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { OutputView } from "./domain.ts";
|
|
16
|
+
import { TerminalTextSanitizer } from "../../shared/terminal-text.ts";
|
|
16
17
|
|
|
17
18
|
export class OutputBuffer {
|
|
18
19
|
private chunks: string[] = [];
|
|
20
|
+
private modelSafeChunks: string[] = [];
|
|
19
21
|
/** Bytes currently retained across `chunks`. */
|
|
20
22
|
private retainedBytes = 0;
|
|
23
|
+
private modelSafeRetainedBytes = 0;
|
|
21
24
|
/** Cached join of `chunks`; invalidated on push so 1Hz UI ticks are cheap. */
|
|
22
25
|
private cachedText: string | undefined = "";
|
|
26
|
+
private cachedModelSafeText: string | undefined = "";
|
|
23
27
|
/** Bumped on every push; lets the UI cache derived line layouts. */
|
|
24
28
|
version = 0;
|
|
25
29
|
totalBytes = 0;
|
|
@@ -28,6 +32,7 @@ export class OutputBuffer {
|
|
|
28
32
|
|
|
29
33
|
private readonly maxRetainedBytes: number;
|
|
30
34
|
private readonly spill?: (chunk: string) => unknown;
|
|
35
|
+
private readonly modelSanitizer = new TerminalTextSanitizer();
|
|
31
36
|
|
|
32
37
|
constructor(maxRetainedBytes: number, spill?: (chunk: string) => unknown) {
|
|
33
38
|
this.maxRetainedBytes = maxRetainedBytes;
|
|
@@ -36,6 +41,7 @@ export class OutputBuffer {
|
|
|
36
41
|
|
|
37
42
|
push(chunk: string) {
|
|
38
43
|
if (chunk.length === 0) return true;
|
|
44
|
+
this.retainModelSafeText(this.modelSanitizer.push(chunk));
|
|
39
45
|
let bytes = Buffer.byteLength(chunk, "utf8");
|
|
40
46
|
this.totalBytes += bytes;
|
|
41
47
|
const spillAccepted = this.spill?.(chunk) !== false;
|
|
@@ -74,11 +80,38 @@ export class OutputBuffer {
|
|
|
74
80
|
|
|
75
81
|
view(): OutputView {
|
|
76
82
|
this.cachedText ??= this.chunks.join("");
|
|
83
|
+
this.cachedModelSafeText ??= this.modelSafeChunks.join("");
|
|
77
84
|
return {
|
|
78
85
|
text: this.cachedText,
|
|
86
|
+
modelSafeText: this.cachedModelSafeText,
|
|
79
87
|
totalBytes: this.totalBytes,
|
|
80
88
|
truncatedBytes: this.truncatedBytes,
|
|
81
89
|
spillPath: this.spillPath,
|
|
82
90
|
};
|
|
83
91
|
}
|
|
92
|
+
|
|
93
|
+
private retainModelSafeText(text: string) {
|
|
94
|
+
if (text.length === 0) return;
|
|
95
|
+
let bytes = Buffer.byteLength(text, "utf8");
|
|
96
|
+
if (bytes > this.maxRetainedBytes) {
|
|
97
|
+
this.modelSafeChunks = [];
|
|
98
|
+
this.modelSafeRetainedBytes = 0;
|
|
99
|
+
const raw = Buffer.from(text, "utf8");
|
|
100
|
+
let start = raw.length - this.maxRetainedBytes;
|
|
101
|
+
while (start < raw.length && (raw[start] & 0xc0) === 0x80) start++;
|
|
102
|
+
text = raw.subarray(start).toString("utf8");
|
|
103
|
+
bytes = raw.length - start;
|
|
104
|
+
}
|
|
105
|
+
this.modelSafeChunks.push(text);
|
|
106
|
+
this.modelSafeRetainedBytes += bytes;
|
|
107
|
+
while (
|
|
108
|
+
this.modelSafeRetainedBytes > this.maxRetainedBytes &&
|
|
109
|
+
this.modelSafeChunks.length > 1
|
|
110
|
+
) {
|
|
111
|
+
const evicted = this.modelSafeChunks.shift();
|
|
112
|
+
if (evicted === undefined) break;
|
|
113
|
+
this.modelSafeRetainedBytes -= Buffer.byteLength(evicted, "utf8");
|
|
114
|
+
}
|
|
115
|
+
this.cachedModelSafeText = undefined;
|
|
116
|
+
}
|
|
84
117
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatSize,
|
|
7
7
|
truncateTail,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { sanitizeTerminalText } from "../../shared/terminal-text.ts";
|
|
9
10
|
import {
|
|
10
11
|
formatDuration,
|
|
11
12
|
formatElapsed,
|
|
@@ -139,7 +140,7 @@ function outputSection(
|
|
|
139
140
|
maxLines: number,
|
|
140
141
|
) {
|
|
141
142
|
if (view.totalBytes === 0) return `${label}: (empty)`;
|
|
142
|
-
const truncation = truncateTail(view.
|
|
143
|
+
const truncation = truncateTail(view.modelSafeText, {
|
|
143
144
|
maxBytes: Math.min(maxBytes, DEFAULT_MAX_BYTES),
|
|
144
145
|
maxLines: Math.min(maxLines, DEFAULT_MAX_LINES),
|
|
145
146
|
});
|
|
@@ -178,7 +179,7 @@ export function buildTerminalResultMessage(snap: TerminalSnapshot) {
|
|
|
178
179
|
if (snap.stderr.totalBytes > 0) {
|
|
179
180
|
text += `\n\n${outputSection("stderr", snap.stderr, RESULT_STDERR_MAX, RESULT_STDERR_MAX_LINES)}`;
|
|
180
181
|
}
|
|
181
|
-
return text
|
|
182
|
+
return `${text}\n\n(This result is already shown to the user. Act on it and relay only the decisions or next steps — do not repeat it verbatim.)`;
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
/** Preserve every retained terminal identity while globally bounding batch logs. */
|
|
@@ -186,12 +187,15 @@ export function buildTerminalBatchResultMessage(
|
|
|
186
187
|
messages: readonly string[],
|
|
187
188
|
omitted = 0,
|
|
188
189
|
) {
|
|
189
|
-
|
|
190
|
-
|
|
190
|
+
const sanitizedMessages = messages.map(sanitizeTerminalText);
|
|
191
|
+
if (sanitizedMessages.length === 1 && omitted === 0) {
|
|
192
|
+
return sanitizedMessages[0]!;
|
|
193
|
+
}
|
|
194
|
+
const summaries = sanitizedMessages.map(
|
|
191
195
|
(message) => message.split("\n", 1)[0] || "Background terminal result",
|
|
192
196
|
);
|
|
193
197
|
const header = [
|
|
194
|
-
`${
|
|
198
|
+
`${sanitizedMessages.length} background terminal result${sanitizedMessages.length === 1 ? "" : "s"}:`,
|
|
195
199
|
...summaries.map((summary) => `- ${summary}`),
|
|
196
200
|
omitted > 0
|
|
197
201
|
? `- ${omitted} older result${omitted === 1 ? "" : "s"} omitted from this bounded batch; use bg_list/bg_status for retained details.`
|
|
@@ -206,7 +210,7 @@ export function buildTerminalBatchResultMessage(
|
|
|
206
210
|
`${header}${logsHeader}${truncationMarker}`,
|
|
207
211
|
"utf8",
|
|
208
212
|
);
|
|
209
|
-
const logs = truncateTail(
|
|
213
|
+
const logs = truncateTail(sanitizedMessages.join("\n\n"), {
|
|
210
214
|
maxBytes: Math.max(1, RESULT_BATCH_MAX - fixedBytes),
|
|
211
215
|
maxLines: DEFAULT_MAX_LINES,
|
|
212
216
|
});
|
|
@@ -220,6 +224,10 @@ export function buildKillReport(results: ReadonlyArray<KillResult>) {
|
|
|
220
224
|
if (entry.killed) {
|
|
221
225
|
return `Killed ${entry.id} "${entry.title}" (${entry.exit}).`;
|
|
222
226
|
}
|
|
227
|
+
if (entry.terminationFailed) {
|
|
228
|
+
const detail = entry.errorText ? ` ${entry.errorText}.` : "";
|
|
229
|
+
return `Could not confirm process-tree termination for ${entry.id} "${entry.title}" (${entry.exit}).${detail}`;
|
|
230
|
+
}
|
|
223
231
|
if (entry.wasRunning) {
|
|
224
232
|
// The natural exit won the race with the kill signal.
|
|
225
233
|
return `${entry.id} "${entry.title}" exited on its own before the kill landed (${entry.exit}).`;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Deferred one-shot delivery map (same semantics as subagents'): a settled
|
|
3
5
|
* terminal's result is held here until it is either drained into a follow-up
|
|
@@ -8,7 +10,7 @@
|
|
|
8
10
|
export function createDeferredResultDelivery<T extends { id: string }>() {
|
|
9
11
|
const pending = new Map<string, T>();
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
const queue = {
|
|
12
14
|
defer(result: T) {
|
|
13
15
|
pending.set(result.id, result);
|
|
14
16
|
return pending.size;
|
|
@@ -38,6 +40,7 @@ export function createDeferredResultDelivery<T extends { id: string }>() {
|
|
|
38
40
|
pending.clear();
|
|
39
41
|
},
|
|
40
42
|
};
|
|
43
|
+
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -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) => {
|