@tt-a1i/openpi 0.4.0 → 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 +102 -40
- package/SETUP.md +22 -6
- package/assets/openpi-launch-card-v1.webp +0 -0
- package/bin/openpi.js +145 -0
- 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 +13 -5
- package/extensions/background-terminals/src/result-delivery.ts +4 -1
- 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/render.ts +17 -257
- package/extensions/file-search/src/binaries.ts +57 -41
- package/extensions/git-read/index.ts +1 -3
- 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 +153 -86
- 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 +123 -127
- package/extensions/shared/activity-status.ts +30 -0
- 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/capability-intent.ts +1 -1
- package/extensions/shared/child-session.ts +437 -21
- package/extensions/shared/result-delivery.ts +34 -0
- package/extensions/shared/setup-config.ts +73 -33
- package/extensions/shared/setup-episode-state.ts +1 -1
- package/extensions/shared/terminal-text.ts +110 -23
- package/extensions/shared/text-projection.ts +72 -15
- package/extensions/shared/tool-activity.ts +382 -0
- package/extensions/shared/tool-surface.ts +29 -2
- 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 +270 -59
- package/extensions/subagents/navigation.ts +34 -5
- 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/manager.ts +34 -2
- package/extensions/subagents/src/prompt.ts +32 -4
- package/extensions/subagents/src/result-artifact.ts +4 -0
- package/extensions/subagents/src/result-delivery.ts +7 -1
- package/extensions/subagents/src/runtime.ts +15 -1
- package/extensions/subagents/src/ui/takeover.ts +73 -257
- package/extensions/subagents/src/ui/transcript.ts +38 -535
- 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/ui-customization/footer.ts +0 -40
- package/extensions/ui-customization/index.ts +0 -4
- package/extensions/user-input-fold/index.ts +1 -1
- package/extensions/web/index.ts +234 -0
- package/extensions/workflows/artifacts.ts +137 -47
- package/extensions/workflows/completion-projection.ts +457 -0
- package/extensions/workflows/coordinator.ts +8 -10
- package/extensions/workflows/dashboard.ts +167 -228
- package/extensions/workflows/handoff.ts +70 -16
- package/extensions/workflows/index.ts +488 -198
- package/extensions/workflows/journal.ts +148 -13
- package/extensions/workflows/model.ts +74 -4
- package/extensions/workflows/navigation.ts +32 -8
- package/extensions/workflows/progress-projection.ts +306 -0
- package/extensions/workflows/prompt.ts +66 -6
- package/extensions/workflows/replay-safety.ts +42 -21
- package/extensions/workflows/result-delivery.ts +128 -64
- package/extensions/workflows/retention.ts +593 -0
- package/extensions/workflows/runner.ts +388 -279
- package/extensions/workflows/sandbox-child.cjs +25 -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 +1 -1
- package/skills/workflows/REFERENCE.md +4 -2
- 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 -303
|
@@ -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
|
});
|
|
@@ -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
|
/**
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ExtensionAPI,
|
|
3
|
+
type ExtensionContext,
|
|
4
|
+
type KeybindingsManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { Key, matchesKey, type EditorComponent } from "@earendil-works/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
BelowEditorNavigationEditor,
|
|
9
|
+
BelowEditorStripState,
|
|
10
|
+
} from "../shared/below-editor-navigation.ts";
|
|
11
|
+
import {
|
|
12
|
+
registerEditorLayer,
|
|
13
|
+
removeEditorLayer,
|
|
14
|
+
} from "../shared/editor-layers.ts";
|
|
15
|
+
|
|
16
|
+
export function shouldClearContext(
|
|
17
|
+
data: string,
|
|
18
|
+
editorText: string,
|
|
19
|
+
isIdle: boolean,
|
|
20
|
+
) {
|
|
21
|
+
return isIdle && editorText.length === 0 && matchesKey(data, Key.ctrl("c"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class ClearContextEditor extends BelowEditorNavigationEditor {
|
|
25
|
+
private readonly isIdle: () => boolean;
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
base: EditorComponent,
|
|
29
|
+
keybindings: KeybindingsManager,
|
|
30
|
+
isIdle: () => boolean,
|
|
31
|
+
) {
|
|
32
|
+
super(
|
|
33
|
+
base,
|
|
34
|
+
keybindings,
|
|
35
|
+
new BelowEditorStripState(),
|
|
36
|
+
() => false,
|
|
37
|
+
() => undefined,
|
|
38
|
+
() => undefined,
|
|
39
|
+
);
|
|
40
|
+
this.isIdle = isIdle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override handleInput(data: string) {
|
|
44
|
+
if (shouldClearContext(data, this.getText(), this.isIdle())) {
|
|
45
|
+
// Route through Pi's built-in /new command so session replacement keeps
|
|
46
|
+
// Pi's persistence, cleanup, and transcript lifecycle as the source of truth.
|
|
47
|
+
this.setText("/new");
|
|
48
|
+
this.onSubmit?.("/new");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
super.handleInput(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function installClearContextShortcut(pi: ExtensionAPI, ctx: ExtensionContext) {
|
|
57
|
+
if (ctx.mode !== "tui") return () => {};
|
|
58
|
+
|
|
59
|
+
registerEditorLayer(pi, ctx, {
|
|
60
|
+
id: "clear-context",
|
|
61
|
+
order: 100,
|
|
62
|
+
wrap: (base, _tui, _theme, keybindings) =>
|
|
63
|
+
new ClearContextEditor(base, keybindings, () => ctx.isIdle()),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return () => removeEditorLayer(pi, "clear-context");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default function clearContext(pi: ExtensionAPI) {
|
|
70
|
+
let removeShortcut = () => {};
|
|
71
|
+
|
|
72
|
+
pi.on("session_start", (_event, ctx) => {
|
|
73
|
+
removeShortcut();
|
|
74
|
+
removeShortcut = installClearContextShortcut(pi, ctx);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
pi.on("session_shutdown", () => {
|
|
78
|
+
removeShortcut();
|
|
79
|
+
removeShortcut = () => {};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export { installClearContextShortcut };
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
|
|
12
12
|
export const MIN_CONTEXT_PIVOT_TOKENS = 30_000;
|
|
13
13
|
const STATUS_KEY = "context-pivot";
|
|
14
|
+
const NOTHING_TO_COMPACT_ERROR = "Nothing to compact (session too small)";
|
|
15
|
+
const NO_DISCARDABLE_HISTORY_MESSAGE =
|
|
16
|
+
"Context pivot could not run: this session has no discardable history to compact. Continue in the current session, or use /sessions to choose another session; to begin cleanly, start a new Session in Pi.";
|
|
14
17
|
|
|
15
18
|
interface PendingPivot {
|
|
16
19
|
brief: string;
|
|
@@ -57,6 +60,13 @@ export function buildPivotSummary(brief: string): string {
|
|
|
57
60
|
].join("\n");
|
|
58
61
|
}
|
|
59
62
|
|
|
63
|
+
function formatContextPivotError(error: unknown): string {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
return message === NOTHING_TO_COMPACT_ERROR
|
|
66
|
+
? NO_DISCARDABLE_HISTORY_MESSAGE
|
|
67
|
+
: `Context pivot failed: ${message}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function impossibleKeptId(entries: readonly SessionEntry[]) {
|
|
61
71
|
return `${entries.at(-1)?.id ?? "context-pivot"}-context-pivot-cut`;
|
|
62
72
|
}
|
|
@@ -73,7 +83,7 @@ function validateBrief(brief: string, ctx: ExtensionContext) {
|
|
|
73
83
|
}
|
|
74
84
|
if (tokens < MIN_CONTEXT_PIVOT_TOKENS) {
|
|
75
85
|
throw new Error(
|
|
76
|
-
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or /
|
|
86
|
+
`Context is only ${Math.round(tokens).toLocaleString()} tokens; use context_pivot once context reaches at least ${MIN_CONTEXT_PIVOT_TOKENS.toLocaleString()} tokens, or use /sessions to browse or switch an existing session; start a new Session in Pi when a clean session is needed.`,
|
|
77
87
|
);
|
|
78
88
|
}
|
|
79
89
|
}
|
|
@@ -127,7 +137,7 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
127
137
|
name: "context_pivot",
|
|
128
138
|
label: "Context Pivot",
|
|
129
139
|
description:
|
|
130
|
-
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /
|
|
140
|
+
"Deliberately replace a long, noisy active context with a concise brief for the next phase while staying in the same Pi session. Use once context is at least 30k tokens and the work is moving between phases such as research → implementation or implementation → review; below 30k it is rejected. Use /sessions to browse or switch an existing session; start a new Session in Pi when a genuinely new session is needed.",
|
|
131
141
|
promptSnippet:
|
|
132
142
|
"Compress a long current session into a clean brief before changing phase",
|
|
133
143
|
promptGuidelines: [
|
|
@@ -176,11 +186,11 @@ export default function contextPivot(pi: ExtensionAPI) {
|
|
|
176
186
|
onError: (error) => {
|
|
177
187
|
if (pivotGeneration === generation) pending = undefined;
|
|
178
188
|
clear();
|
|
189
|
+
const message = formatContextPivotError(error);
|
|
179
190
|
if (ctx.hasUI) {
|
|
180
|
-
ctx.ui.notify(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
191
|
+
ctx.ui.notify(message, "error");
|
|
192
|
+
} else {
|
|
193
|
+
console.error(message);
|
|
184
194
|
}
|
|
185
195
|
},
|
|
186
196
|
});
|
|
@@ -81,8 +81,14 @@ export function parseCronCommand(raw: string): ParsedCronCommand {
|
|
|
81
81
|
error: `Minimum interval is ${MIN_INTERVAL_MS / 1000}s (the scheduler polls about that often).`,
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
|
-
const prompt = schedule[3].trim()
|
|
84
|
+
const prompt = schedule[3].trim();
|
|
85
85
|
if (!prompt) return { action: "help", error: "Provide a prompt to run." };
|
|
86
|
+
if (prompt.length > CRON_PROMPT_MAX_CHARS) {
|
|
87
|
+
return {
|
|
88
|
+
action: "help",
|
|
89
|
+
error: `Prompt is too long. Maximum is ${CRON_PROMPT_MAX_CHARS} characters.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
86
92
|
return {
|
|
87
93
|
action: "add",
|
|
88
94
|
intervalMs,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { isAbsolute, relative } from "node:path";
|
|
2
|
-
import { stripVTControlCharacters } from "node:util";
|
|
3
1
|
import type {
|
|
4
2
|
AgentToolResult,
|
|
5
3
|
Theme,
|
|
6
4
|
ToolDefinition,
|
|
7
5
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
6
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
9
7
|
import type { TSchema } from "typebox";
|
|
10
|
-
import {
|
|
8
|
+
import { renderPaddedToolActivityLine } from "../shared/tool-activity.ts";
|
|
11
9
|
|
|
12
10
|
type ActivityStatus = "pending" | "success" | "error";
|
|
13
11
|
|
|
@@ -23,33 +21,11 @@ type ActivityRenderState<TDetails> = {
|
|
|
23
21
|
};
|
|
24
22
|
};
|
|
25
23
|
|
|
26
|
-
type ActivityRow = {
|
|
27
|
-
verb: string;
|
|
28
|
-
target: string;
|
|
29
|
-
detail?: string;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const HORIZONTAL_PADDING = " ";
|
|
33
|
-
|
|
34
24
|
const emptyComponent: Component = {
|
|
35
25
|
render: () => [],
|
|
36
26
|
invalidate() {},
|
|
37
27
|
};
|
|
38
28
|
|
|
39
|
-
function record(value: unknown): Record<string, unknown> {
|
|
40
|
-
return value !== null && typeof value === "object"
|
|
41
|
-
? (value as Record<string, unknown>)
|
|
42
|
-
: {};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function string(value: unknown) {
|
|
46
|
-
return typeof value === "string" ? value : "";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function number(value: unknown) {
|
|
50
|
-
return typeof value === "number" ? value : undefined;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
29
|
function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
54
30
|
return (
|
|
55
31
|
result?.content
|
|
@@ -59,228 +35,6 @@ function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
|
59
35
|
);
|
|
60
36
|
}
|
|
61
37
|
|
|
62
|
-
function resultCount(result: AgentToolResult<unknown> | undefined) {
|
|
63
|
-
return textOutput(result)
|
|
64
|
-
.split(/\r?\n/)
|
|
65
|
-
.filter((line) => line.trim().length > 0 && !line.trim().startsWith("["))
|
|
66
|
-
.length;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function grepMatchCount(result: AgentToolResult<unknown> | undefined) {
|
|
70
|
-
const output = textOutput(result).trim();
|
|
71
|
-
if (!output || output === "No matches found") return 0;
|
|
72
|
-
return output.split(/\r?\n/).filter((line) => /^.+:\d+:/.test(line)).length;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function itemCount(
|
|
76
|
-
result: AgentToolResult<unknown> | undefined,
|
|
77
|
-
emptyMessage: string,
|
|
78
|
-
) {
|
|
79
|
-
const output = textOutput(result).trim();
|
|
80
|
-
return !output || output === emptyMessage ? 0 : resultCount(result);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function plural(count: number, singular: string) {
|
|
84
|
-
const pluralForm =
|
|
85
|
-
singular === "match"
|
|
86
|
-
? "matches"
|
|
87
|
-
: singular === "entry"
|
|
88
|
-
? "entries"
|
|
89
|
-
: `${singular}s`;
|
|
90
|
-
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function editStats(details: unknown) {
|
|
94
|
-
const diff = string(record(details).diff);
|
|
95
|
-
if (!diff) return undefined;
|
|
96
|
-
let additions = 0;
|
|
97
|
-
let removals = 0;
|
|
98
|
-
for (const line of diff.split(/\r?\n/)) {
|
|
99
|
-
if (line.startsWith("+") && !line.startsWith("+++")) additions += 1;
|
|
100
|
-
if (line.startsWith("-") && !line.startsWith("---")) removals += 1;
|
|
101
|
-
}
|
|
102
|
-
return { additions, removals };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function range(args: Record<string, unknown>) {
|
|
106
|
-
const offset = number(args.offset);
|
|
107
|
-
const limit = number(args.limit);
|
|
108
|
-
if (offset === undefined && limit === undefined) return "";
|
|
109
|
-
const start = offset ?? 1;
|
|
110
|
-
return limit === undefined ? `:${start}-` : `:${start}-${start + limit - 1}`;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function displayPath(path: string, cwd: string) {
|
|
114
|
-
if (!isAbsolute(path)) return path;
|
|
115
|
-
const local = relative(cwd, path);
|
|
116
|
-
if (local === "") return ".";
|
|
117
|
-
return local.startsWith("..") || isAbsolute(local) ? path : local;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function activityRow(
|
|
121
|
-
name: string,
|
|
122
|
-
argsValue: unknown,
|
|
123
|
-
result: AgentToolResult<unknown> | undefined,
|
|
124
|
-
cwd: string,
|
|
125
|
-
): ActivityRow {
|
|
126
|
-
const args = record(argsValue);
|
|
127
|
-
const path = displayPath(string(args.path) || ".", cwd);
|
|
128
|
-
switch (name) {
|
|
129
|
-
case "read":
|
|
130
|
-
return { verb: "Read", target: `${path}${range(args)}` };
|
|
131
|
-
case "bash":
|
|
132
|
-
return {
|
|
133
|
-
verb: "Ran",
|
|
134
|
-
target: string(args.command).replace(/\s+/g, " ").trim(),
|
|
135
|
-
};
|
|
136
|
-
case "write": {
|
|
137
|
-
const content = string(args.content);
|
|
138
|
-
const lines =
|
|
139
|
-
content.length === 0
|
|
140
|
-
? 0
|
|
141
|
-
: content.replace(/\r?\n$/, "").split(/\r?\n/).length;
|
|
142
|
-
return { verb: "Wrote", target: path, detail: plural(lines, "line") };
|
|
143
|
-
}
|
|
144
|
-
case "edit":
|
|
145
|
-
return { verb: "Edited", target: path };
|
|
146
|
-
case "grep":
|
|
147
|
-
return {
|
|
148
|
-
verb: "Searched",
|
|
149
|
-
target: string(args.pattern),
|
|
150
|
-
detail: `in ${path} ${plural(grepMatchCount(result), "match")}`,
|
|
151
|
-
};
|
|
152
|
-
case "find":
|
|
153
|
-
return {
|
|
154
|
-
verb: "Searched",
|
|
155
|
-
target: string(args.pattern),
|
|
156
|
-
detail: `in ${path} ${plural(itemCount(result, "No files found matching pattern"), "result")}`,
|
|
157
|
-
};
|
|
158
|
-
case "ls":
|
|
159
|
-
return {
|
|
160
|
-
verb: "Listed",
|
|
161
|
-
target: path,
|
|
162
|
-
detail: plural(itemCount(result, "(empty directory)"), "entry"),
|
|
163
|
-
};
|
|
164
|
-
default:
|
|
165
|
-
return { verb: name, target: "" };
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function pendingVerb(name: string) {
|
|
170
|
-
switch (name) {
|
|
171
|
-
case "read":
|
|
172
|
-
return "Reading";
|
|
173
|
-
case "bash":
|
|
174
|
-
return "Running";
|
|
175
|
-
case "write":
|
|
176
|
-
return "Writing";
|
|
177
|
-
case "edit":
|
|
178
|
-
return "Editing";
|
|
179
|
-
case "grep":
|
|
180
|
-
case "find":
|
|
181
|
-
return "Searching";
|
|
182
|
-
case "ls":
|
|
183
|
-
return "Listing";
|
|
184
|
-
default:
|
|
185
|
-
return "Running";
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function activityIcon(name: string) {
|
|
190
|
-
switch (name) {
|
|
191
|
-
case "read":
|
|
192
|
-
return "\ueaa4"; // Nerd Fonts Codicon: book
|
|
193
|
-
case "bash":
|
|
194
|
-
return "\uea85"; // Nerd Fonts Codicon: terminal
|
|
195
|
-
case "write":
|
|
196
|
-
case "edit":
|
|
197
|
-
return "\uea73"; // Nerd Fonts Codicon: edit
|
|
198
|
-
case "grep":
|
|
199
|
-
case "find":
|
|
200
|
-
return "\uea6d"; // Nerd Fonts Codicon: search
|
|
201
|
-
case "ls":
|
|
202
|
-
return "\uea83"; // Nerd Fonts Codicon: folder
|
|
203
|
-
default:
|
|
204
|
-
return "✓";
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function errorSummary(result: AgentToolResult<unknown> | undefined) {
|
|
209
|
-
const lines = textOutput(result)
|
|
210
|
-
.split(/\r?\n/)
|
|
211
|
-
.map((line) => stripVTControlCharacters(line).trim())
|
|
212
|
-
.filter(Boolean);
|
|
213
|
-
return (
|
|
214
|
-
[...lines]
|
|
215
|
-
.reverse()
|
|
216
|
-
.find((line) =>
|
|
217
|
-
/(?:command (?:exited|timed out|aborted)|error|denied|failed)/i.test(
|
|
218
|
-
line,
|
|
219
|
-
),
|
|
220
|
-
) ?? lines[0]
|
|
221
|
-
);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function duration(
|
|
225
|
-
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
226
|
-
) {
|
|
227
|
-
if (state.startedAt === undefined) return undefined;
|
|
228
|
-
const seconds = Math.floor(
|
|
229
|
-
((state.endedAt ?? Date.now()) - state.startedAt) / 1000,
|
|
230
|
-
);
|
|
231
|
-
return seconds > 0 ? `${seconds}s` : undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function activityText(
|
|
235
|
-
name: string,
|
|
236
|
-
args: unknown,
|
|
237
|
-
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
238
|
-
theme: Theme,
|
|
239
|
-
cwd: string,
|
|
240
|
-
) {
|
|
241
|
-
const row = activityRow(name, args, state.result, cwd);
|
|
242
|
-
const elapsed = duration(state);
|
|
243
|
-
const verbText = (
|
|
244
|
-
state.status === "pending"
|
|
245
|
-
? pendingVerb(name)
|
|
246
|
-
: state.status === "error"
|
|
247
|
-
? "Failed"
|
|
248
|
-
: row.verb
|
|
249
|
-
).padEnd(8);
|
|
250
|
-
const verb = theme.fg(
|
|
251
|
-
state.status === "error"
|
|
252
|
-
? "error"
|
|
253
|
-
: state.status === "success"
|
|
254
|
-
? "muted"
|
|
255
|
-
: "toolTitle",
|
|
256
|
-
verbText,
|
|
257
|
-
);
|
|
258
|
-
if (state.status === "pending") {
|
|
259
|
-
const detail = elapsed ? ` · ${elapsed}` : "";
|
|
260
|
-
return `${theme.fg("warning", spinnerFrame(Date.now()))} ${verb} ${row.target}${theme.fg("dim", detail)}`;
|
|
261
|
-
}
|
|
262
|
-
if (state.status === "error") {
|
|
263
|
-
const summary = errorSummary(state.result);
|
|
264
|
-
const detail = [elapsed, summary].filter(Boolean).join(" · ");
|
|
265
|
-
return `${theme.fg("error", "✕")} ${verb} ${row.target}${detail ? theme.fg("dim", ` · ${detail}`) : ""}`;
|
|
266
|
-
}
|
|
267
|
-
const parts: string[] = [];
|
|
268
|
-
if (name === "edit") {
|
|
269
|
-
// Kimi-style diff stats: additions green, removals red.
|
|
270
|
-
const stats = editStats(state.result?.details);
|
|
271
|
-
if (stats) {
|
|
272
|
-
parts.push(
|
|
273
|
-
`${theme.fg("success", `+${stats.additions}`)} ${theme.fg("error", `-${stats.removals}`)}`,
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
} else if (row.detail) {
|
|
277
|
-
parts.push(theme.fg("dim", row.detail));
|
|
278
|
-
}
|
|
279
|
-
if (elapsed) parts.push(theme.fg("dim", elapsed));
|
|
280
|
-
const detail = parts.join(theme.fg("dim", " · "));
|
|
281
|
-
return `${theme.fg("dim", activityIcon(name))} ${verb} ${theme.fg("muted", row.target)}${detail ? ` ${detail}` : ""}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
38
|
function activityComponent(
|
|
285
39
|
name: string,
|
|
286
40
|
args: unknown,
|
|
@@ -290,15 +44,21 @@ function activityComponent(
|
|
|
290
44
|
): Component {
|
|
291
45
|
return {
|
|
292
46
|
render(width) {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
47
|
+
const line = renderPaddedToolActivityLine(
|
|
48
|
+
{
|
|
49
|
+
name,
|
|
50
|
+
args,
|
|
51
|
+
output: textOutput(state.result),
|
|
52
|
+
details: state.result?.details,
|
|
53
|
+
status: state.status,
|
|
54
|
+
cwd,
|
|
55
|
+
startedAt: state.startedAt,
|
|
56
|
+
endedAt: state.endedAt,
|
|
57
|
+
},
|
|
58
|
+
theme,
|
|
59
|
+
width,
|
|
60
|
+
);
|
|
61
|
+
return line ? [line] : [];
|
|
302
62
|
},
|
|
303
63
|
invalidate() {},
|
|
304
64
|
};
|