@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
formatSize,
|
|
3
|
+
truncateHead,
|
|
4
|
+
truncateTail,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
|
|
7
|
+
const HEAD_SHARE = 0.75;
|
|
8
|
+
const OMISSION_MARKER = "[... middle omitted ...]";
|
|
9
|
+
// Three framing blanks, the omission marker, and the footer.
|
|
10
|
+
const STATIC_PROJECTION_OVERHEAD_LINES = 5;
|
|
11
|
+
|
|
12
|
+
function utf8Prefix(content: string, maxBytes: number) {
|
|
13
|
+
const bytes = Buffer.from(content, "utf8");
|
|
14
|
+
if (bytes.length <= maxBytes) return content;
|
|
15
|
+
let end = Math.max(0, maxBytes);
|
|
16
|
+
while (end > 0) {
|
|
17
|
+
const value = bytes.subarray(0, end).toString("utf8");
|
|
18
|
+
if (!value.endsWith("�")) return value;
|
|
19
|
+
end -= 1;
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function boundedHead(content: string, maxBytes: number, maxLines: number) {
|
|
25
|
+
const result = truncateHead(content, { maxBytes, maxLines });
|
|
26
|
+
if (result.content || !result.firstLineExceedsLimit) return result.content;
|
|
27
|
+
return utf8Prefix(content, maxBytes);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function compactProjection(
|
|
31
|
+
content: string,
|
|
32
|
+
maxBytes: number,
|
|
33
|
+
maxLines: number,
|
|
34
|
+
) {
|
|
35
|
+
const marker =
|
|
36
|
+
maxBytes >= OMISSION_MARKER.length
|
|
37
|
+
? OMISSION_MARKER
|
|
38
|
+
: ".".repeat(Math.min(3, maxBytes));
|
|
39
|
+
if (!marker || maxLines <= 0) return "";
|
|
40
|
+
if (maxLines === 1) return marker;
|
|
41
|
+
|
|
42
|
+
const bodyLineBudget = maxLines - 1;
|
|
43
|
+
const keepTail = bodyLineBudget >= 2;
|
|
44
|
+
const separatorBytes = keepTail ? 2 : 1;
|
|
45
|
+
const bodyByteBudget = maxBytes - marker.length - separatorBytes;
|
|
46
|
+
if (bodyByteBudget <= 0 || (keepTail && bodyByteBudget < 2)) return marker;
|
|
47
|
+
|
|
48
|
+
const headLines = keepTail
|
|
49
|
+
? Math.max(1, Math.floor(bodyLineBudget * HEAD_SHARE))
|
|
50
|
+
: bodyLineBudget;
|
|
51
|
+
const tailLines = keepTail ? Math.max(1, bodyLineBudget - headLines) : 0;
|
|
52
|
+
const headBytes = keepTail
|
|
53
|
+
? Math.max(1, Math.floor(bodyByteBudget * HEAD_SHARE))
|
|
54
|
+
: bodyByteBudget;
|
|
55
|
+
const tailBytes = keepTail ? Math.max(1, bodyByteBudget - headBytes) : 0;
|
|
56
|
+
const head = boundedHead(content, headBytes, headLines);
|
|
57
|
+
const tail = keepTail
|
|
58
|
+
? truncateTail(content, {
|
|
59
|
+
maxBytes: tailBytes,
|
|
60
|
+
maxLines: tailLines,
|
|
61
|
+
}).content
|
|
62
|
+
: "";
|
|
63
|
+
return [head, marker, tail].filter(Boolean).join("\n");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Keep decision context at the start and verdict/evidence at the end. */
|
|
67
|
+
export function projectText(
|
|
68
|
+
content: string,
|
|
69
|
+
options: { maxBytes: number; maxLines: number; recovery: string },
|
|
70
|
+
) {
|
|
71
|
+
if (options.maxBytes <= 0 || options.maxLines <= 0) return "";
|
|
72
|
+
|
|
73
|
+
const probe = truncateHead(content, {
|
|
74
|
+
maxBytes: options.maxBytes,
|
|
75
|
+
maxLines: options.maxLines,
|
|
76
|
+
});
|
|
77
|
+
if (!probe.truncated) return content;
|
|
78
|
+
|
|
79
|
+
const recoveryNewlines = options.recovery.split("\n").length - 1;
|
|
80
|
+
const bodyLineBudget =
|
|
81
|
+
options.maxLines - STATIC_PROJECTION_OVERHEAD_LINES - recoveryNewlines;
|
|
82
|
+
if (bodyLineBudget < 2) {
|
|
83
|
+
return compactProjection(content, options.maxBytes, options.maxLines);
|
|
84
|
+
}
|
|
85
|
+
const headLines = Math.max(1, Math.floor(bodyLineBudget * HEAD_SHARE));
|
|
86
|
+
const tailLines = Math.max(1, bodyLineBudget - headLines);
|
|
87
|
+
|
|
88
|
+
let bodyBudget = options.maxBytes;
|
|
89
|
+
let projected = "";
|
|
90
|
+
for (let attempt = 0; attempt < 8; attempt++) {
|
|
91
|
+
const headBytes = Math.max(1, Math.floor(bodyBudget * HEAD_SHARE));
|
|
92
|
+
const tailBytes = Math.max(1, bodyBudget - headBytes);
|
|
93
|
+
const head = boundedHead(content, headBytes, headLines);
|
|
94
|
+
const tail = truncateTail(content, {
|
|
95
|
+
maxBytes: tailBytes,
|
|
96
|
+
maxLines: tailLines,
|
|
97
|
+
}).content;
|
|
98
|
+
const shown =
|
|
99
|
+
Buffer.byteLength(head, "utf8") + Buffer.byteLength(tail, "utf8");
|
|
100
|
+
const footer = `[Projection bounded: showing ${formatSize(shown)} of ${formatSize(probe.totalBytes)} across the head and tail. ${options.recovery}]`;
|
|
101
|
+
projected = `${head}\n\n${OMISSION_MARKER}\n\n${tail}\n\n${footer}`;
|
|
102
|
+
const overflow = Buffer.byteLength(projected, "utf8") - options.maxBytes;
|
|
103
|
+
if (overflow <= 0 || bodyBudget <= overflow + 2) break;
|
|
104
|
+
bodyBudget -= overflow;
|
|
105
|
+
}
|
|
106
|
+
const projectedProbe = truncateHead(projected, {
|
|
107
|
+
maxBytes: options.maxBytes,
|
|
108
|
+
maxLines: options.maxLines,
|
|
109
|
+
});
|
|
110
|
+
return projectedProbe.truncated
|
|
111
|
+
? compactProjection(content, options.maxBytes, options.maxLines)
|
|
112
|
+
: projected;
|
|
113
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
2
|
+
import { stripVTControlCharacters } from "node:util";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import { spinnerFrame } from "./spinner.ts";
|
|
6
|
+
import { sanitizeTerminalText } from "./terminal-text.ts";
|
|
7
|
+
|
|
8
|
+
export type ToolActivityStatus = "pending" | "success" | "error";
|
|
9
|
+
|
|
10
|
+
export interface ToolActivity {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly args?: unknown;
|
|
13
|
+
readonly argsFallback?: string;
|
|
14
|
+
readonly output?: string;
|
|
15
|
+
readonly details?: unknown;
|
|
16
|
+
readonly status: ToolActivityStatus;
|
|
17
|
+
readonly cwd?: string;
|
|
18
|
+
readonly startedAt?: number;
|
|
19
|
+
readonly endedAt?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ActivityRow {
|
|
23
|
+
readonly verb: string;
|
|
24
|
+
readonly target: string;
|
|
25
|
+
readonly detail?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function record(value: unknown): Record<string, unknown> {
|
|
29
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
30
|
+
? (value as Record<string, unknown>)
|
|
31
|
+
: {};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function string(value: unknown) {
|
|
35
|
+
return typeof value === "string" ? sanitizeTerminalText(value) : "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function number(value: unknown) {
|
|
39
|
+
return typeof value === "number" ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function compact(value: string) {
|
|
43
|
+
return sanitizeTerminalText(value).replace(/\r?\n/g, " ↵ ").trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseToolArgsPreview(preview?: string) {
|
|
47
|
+
const clean = sanitizeTerminalText(preview ?? "").trim();
|
|
48
|
+
const fallback = compact(clean);
|
|
49
|
+
if (!fallback) return { args: undefined, fallback: undefined };
|
|
50
|
+
try {
|
|
51
|
+
const args: unknown = JSON.parse(clean);
|
|
52
|
+
return { args, fallback };
|
|
53
|
+
} catch {
|
|
54
|
+
return { args: undefined, fallback };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function displayPath(value: string, cwd?: string) {
|
|
59
|
+
if (!isAbsolute(value) || !cwd) return value;
|
|
60
|
+
const local = relative(cwd, value);
|
|
61
|
+
if (local === "") return ".";
|
|
62
|
+
return local.startsWith("..") || isAbsolute(local) ? value : local;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resultCount(output: string) {
|
|
66
|
+
return output
|
|
67
|
+
.split(/\r?\n/)
|
|
68
|
+
.filter((line) => line.trim().length > 0 && !line.trim().startsWith("["))
|
|
69
|
+
.length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function grepMatchCount(output: string) {
|
|
73
|
+
const text = output.trim();
|
|
74
|
+
if (!text || text === "No matches found") return 0;
|
|
75
|
+
return text.split(/\r?\n/).filter((line) => /^.+:\d+:/.test(line)).length;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function itemCount(output: string, emptyMessage: string) {
|
|
79
|
+
const text = output.trim();
|
|
80
|
+
return !text || text === emptyMessage ? 0 : resultCount(output);
|
|
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 canonicalName(name: string) {
|
|
114
|
+
const lower = sanitizeTerminalText(name).toLowerCase();
|
|
115
|
+
if (lower === "rg") return "grep";
|
|
116
|
+
if (lower === "fd") return "find";
|
|
117
|
+
return lower;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function activityRow(activity: ToolActivity): ActivityRow {
|
|
121
|
+
const name = canonicalName(activity.name);
|
|
122
|
+
const args = record(activity.args);
|
|
123
|
+
const fallback = compact(activity.argsFallback ?? "");
|
|
124
|
+
const output = sanitizeTerminalText(activity.output ?? "");
|
|
125
|
+
const path = displayPath(string(args.path) || ".", activity.cwd);
|
|
126
|
+
switch (name) {
|
|
127
|
+
case "read":
|
|
128
|
+
return { verb: "Read", target: `${path}${range(args)}` };
|
|
129
|
+
case "bash":
|
|
130
|
+
return {
|
|
131
|
+
verb: "Ran",
|
|
132
|
+
target:
|
|
133
|
+
string(args.command).replace(/\s+/g, " ").trim() || fallback || name,
|
|
134
|
+
};
|
|
135
|
+
case "write": {
|
|
136
|
+
const content = string(args.content);
|
|
137
|
+
const lines =
|
|
138
|
+
content.length === 0
|
|
139
|
+
? 0
|
|
140
|
+
: content.replace(/\r?\n$/, "").split(/\r?\n/).length;
|
|
141
|
+
return {
|
|
142
|
+
verb: "Wrote",
|
|
143
|
+
target: string(args.path) ? path : fallback,
|
|
144
|
+
...(content ? { detail: plural(lines, "line") } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case "edit":
|
|
148
|
+
return { verb: "Edited", target: string(args.path) ? path : fallback };
|
|
149
|
+
case "grep": {
|
|
150
|
+
const pattern = string(args.pattern) || fallback;
|
|
151
|
+
return {
|
|
152
|
+
verb: "Searched",
|
|
153
|
+
target: pattern,
|
|
154
|
+
...(output
|
|
155
|
+
? { detail: `in ${path} ${plural(grepMatchCount(output), "match")}` }
|
|
156
|
+
: string(args.path)
|
|
157
|
+
? { detail: `in ${path}` }
|
|
158
|
+
: {}),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
case "find": {
|
|
162
|
+
const pattern = string(args.pattern) || fallback;
|
|
163
|
+
return {
|
|
164
|
+
verb: "Searched",
|
|
165
|
+
target: pattern,
|
|
166
|
+
...(output
|
|
167
|
+
? {
|
|
168
|
+
detail: `in ${path} ${plural(itemCount(output, "No files found matching pattern"), "result")}`,
|
|
169
|
+
}
|
|
170
|
+
: string(args.path)
|
|
171
|
+
? { detail: `in ${path}` }
|
|
172
|
+
: {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
case "ls":
|
|
176
|
+
return {
|
|
177
|
+
verb: "Listed",
|
|
178
|
+
target: string(args.path) ? path : fallback || ".",
|
|
179
|
+
...(output
|
|
180
|
+
? { detail: plural(itemCount(output, "(empty directory)"), "entry") }
|
|
181
|
+
: {}),
|
|
182
|
+
};
|
|
183
|
+
default:
|
|
184
|
+
return {
|
|
185
|
+
verb: sanitizeTerminalText(activity.name),
|
|
186
|
+
target: fallback || name,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pendingVerb(name: string) {
|
|
192
|
+
switch (canonicalName(name)) {
|
|
193
|
+
case "read":
|
|
194
|
+
return "Reading";
|
|
195
|
+
case "bash":
|
|
196
|
+
return "Running";
|
|
197
|
+
case "write":
|
|
198
|
+
return "Writing";
|
|
199
|
+
case "edit":
|
|
200
|
+
return "Editing";
|
|
201
|
+
case "grep":
|
|
202
|
+
case "find":
|
|
203
|
+
return "Searching";
|
|
204
|
+
case "ls":
|
|
205
|
+
return "Listing";
|
|
206
|
+
default:
|
|
207
|
+
return "Running";
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function activityIcon(name: string) {
|
|
212
|
+
switch (canonicalName(name)) {
|
|
213
|
+
case "read":
|
|
214
|
+
return "\ueaa4";
|
|
215
|
+
case "bash":
|
|
216
|
+
return "\uea85";
|
|
217
|
+
case "write":
|
|
218
|
+
case "edit":
|
|
219
|
+
return "\uea73";
|
|
220
|
+
case "grep":
|
|
221
|
+
case "find":
|
|
222
|
+
return "\uea6d";
|
|
223
|
+
case "ls":
|
|
224
|
+
return "\uea83";
|
|
225
|
+
default:
|
|
226
|
+
return "✓";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function errorSummary(output?: string) {
|
|
231
|
+
const lines = sanitizeTerminalText(output ?? "")
|
|
232
|
+
.split(/\r?\n/)
|
|
233
|
+
.map((line) => stripVTControlCharacters(line).trim())
|
|
234
|
+
.filter(Boolean);
|
|
235
|
+
return (
|
|
236
|
+
[...lines]
|
|
237
|
+
.reverse()
|
|
238
|
+
.find((line) =>
|
|
239
|
+
/(?:command (?:exited|timed out|aborted)|error|denied|failed)/i.test(
|
|
240
|
+
line,
|
|
241
|
+
),
|
|
242
|
+
) ?? lines[0]
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function elapsed(activity: ToolActivity, now: number) {
|
|
247
|
+
if (activity.startedAt === undefined) return undefined;
|
|
248
|
+
const seconds = Math.floor(
|
|
249
|
+
((activity.endedAt ?? now) - activity.startedAt) / 1000,
|
|
250
|
+
);
|
|
251
|
+
return seconds > 0 ? `${seconds}s` : undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function toolActivityText(
|
|
255
|
+
activity: ToolActivity,
|
|
256
|
+
theme: Theme,
|
|
257
|
+
now = Date.now(),
|
|
258
|
+
) {
|
|
259
|
+
const row = activityRow(activity);
|
|
260
|
+
const duration = elapsed(activity, now);
|
|
261
|
+
const verbText = (
|
|
262
|
+
activity.status === "pending"
|
|
263
|
+
? pendingVerb(activity.name)
|
|
264
|
+
: activity.status === "error"
|
|
265
|
+
? "Failed"
|
|
266
|
+
: row.verb
|
|
267
|
+
).padEnd(8);
|
|
268
|
+
const verb = theme.fg(
|
|
269
|
+
activity.status === "error"
|
|
270
|
+
? "error"
|
|
271
|
+
: activity.status === "success"
|
|
272
|
+
? "muted"
|
|
273
|
+
: "toolTitle",
|
|
274
|
+
verbText,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (activity.status === "pending") {
|
|
278
|
+
const detail = duration ? ` · ${duration}` : "";
|
|
279
|
+
return `${theme.fg("warning", spinnerFrame(now))} ${verb} ${row.target}${theme.fg("dim", detail)}`;
|
|
280
|
+
}
|
|
281
|
+
if (activity.status === "error") {
|
|
282
|
+
const summary = errorSummary(activity.output);
|
|
283
|
+
const detail = [duration, summary].filter(Boolean).join(" · ");
|
|
284
|
+
return `${theme.fg("error", "✕")} ${verb} ${row.target}${detail ? theme.fg("dim", ` · ${detail}`) : ""}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const parts: string[] = [];
|
|
288
|
+
if (canonicalName(activity.name) === "edit") {
|
|
289
|
+
const stats = editStats(activity.details);
|
|
290
|
+
if (stats) {
|
|
291
|
+
parts.push(
|
|
292
|
+
`${theme.fg("success", `+${stats.additions}`)} ${theme.fg("error", `-${stats.removals}`)}`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} else if (row.detail) {
|
|
296
|
+
parts.push(theme.fg("dim", row.detail));
|
|
297
|
+
}
|
|
298
|
+
if (duration) parts.push(theme.fg("dim", duration));
|
|
299
|
+
const detail = parts.join(theme.fg("dim", " · "));
|
|
300
|
+
return `${theme.fg("dim", activityIcon(activity.name))} ${verb} ${theme.fg("muted", row.target)}${detail ? ` ${detail}` : ""}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function renderToolActivityLine(
|
|
304
|
+
activity: ToolActivity,
|
|
305
|
+
theme: Theme,
|
|
306
|
+
width: number,
|
|
307
|
+
now = Date.now(),
|
|
308
|
+
) {
|
|
309
|
+
const ellipsis = activity.status === "success" ? theme.fg("muted", "…") : "…";
|
|
310
|
+
return truncateToWidth(
|
|
311
|
+
toolActivityText(activity, theme, now),
|
|
312
|
+
width,
|
|
313
|
+
ellipsis,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Match the horizontal shell used by Pi's collapsed tool component. Keeping
|
|
319
|
+
* this here lets persisted child transcripts and live parent tools share the
|
|
320
|
+
* exact same operator-facing row without pretending bounded previews contain
|
|
321
|
+
* the full native tool result.
|
|
322
|
+
*/
|
|
323
|
+
export function renderPaddedToolActivityLine(
|
|
324
|
+
activity: ToolActivity,
|
|
325
|
+
theme: Theme,
|
|
326
|
+
width: number,
|
|
327
|
+
now = Date.now(),
|
|
328
|
+
) {
|
|
329
|
+
const padding = " ";
|
|
330
|
+
const contentWidth = width - padding.length * 2;
|
|
331
|
+
if (contentWidth <= 0) return "";
|
|
332
|
+
return `${padding}${renderToolActivityLine(activity, theme, contentWidth, now)}${padding}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Historical Direct helper retained without owning a second formatter. */
|
|
336
|
+
export function summarizeToolArgs(
|
|
337
|
+
name: string,
|
|
338
|
+
argsPreview?: string,
|
|
339
|
+
cwd?: string,
|
|
340
|
+
) {
|
|
341
|
+
if (!argsPreview) return undefined;
|
|
342
|
+
const fallback = compact(argsPreview);
|
|
343
|
+
if (!fallback || fallback === "{}") return undefined;
|
|
344
|
+
|
|
345
|
+
let args: Record<string, unknown> | undefined;
|
|
346
|
+
try {
|
|
347
|
+
const parsed: unknown = JSON.parse(argsPreview);
|
|
348
|
+
if (
|
|
349
|
+
parsed !== null &&
|
|
350
|
+
typeof parsed === "object" &&
|
|
351
|
+
!Array.isArray(parsed)
|
|
352
|
+
) {
|
|
353
|
+
args = parsed as Record<string, unknown>;
|
|
354
|
+
}
|
|
355
|
+
} catch {
|
|
356
|
+
return fallback;
|
|
357
|
+
}
|
|
358
|
+
if (!args) return fallback;
|
|
359
|
+
|
|
360
|
+
const field = (key: string) => {
|
|
361
|
+
const value = args[key];
|
|
362
|
+
return typeof value === "string" && value.length > 0
|
|
363
|
+
? sanitizeTerminalText(value).replace(/\r?\n/g, " ↵ ")
|
|
364
|
+
: undefined;
|
|
365
|
+
};
|
|
366
|
+
const path = () => {
|
|
367
|
+
const value = field("path");
|
|
368
|
+
return value ? displayPath(value, cwd) : undefined;
|
|
369
|
+
};
|
|
370
|
+
const tool = canonicalName(name);
|
|
371
|
+
if (tool === "bash") return field("command") ?? fallback;
|
|
372
|
+
if (tool === "read" || tool === "write" || tool === "edit") {
|
|
373
|
+
return path() ?? fallback;
|
|
374
|
+
}
|
|
375
|
+
if (tool === "grep" || tool === "find") {
|
|
376
|
+
const pattern = field("pattern");
|
|
377
|
+
const searchPath = path();
|
|
378
|
+
if (pattern && searchPath) return `${pattern} · ${searchPath}`;
|
|
379
|
+
return pattern ?? searchPath ?? fallback;
|
|
380
|
+
}
|
|
381
|
+
return fallback;
|
|
382
|
+
}
|
|
@@ -17,19 +17,24 @@ export const OPENPI_TOOL_SURFACE = {
|
|
|
17
17
|
entry: ["fd", "rg"],
|
|
18
18
|
deferred: [],
|
|
19
19
|
},
|
|
20
|
+
gitRead: {
|
|
21
|
+
entry: ["git_show", "git_diff", "git_log"],
|
|
22
|
+
deferred: [],
|
|
23
|
+
},
|
|
20
24
|
subagents: {
|
|
21
|
-
entry: [
|
|
22
|
-
|
|
25
|
+
entry: [
|
|
26
|
+
"subagent_spawn",
|
|
23
27
|
"subagent_wait",
|
|
24
28
|
"subagent_cancel",
|
|
25
29
|
"subagent_send",
|
|
26
30
|
"subagent_check",
|
|
27
31
|
"subagent_list",
|
|
28
32
|
],
|
|
33
|
+
deferred: [],
|
|
29
34
|
},
|
|
30
35
|
workflows: {
|
|
31
|
-
entry: ["workflow"],
|
|
32
|
-
deferred: [
|
|
36
|
+
entry: ["workflow", "workflow_stop", "workflow_status"],
|
|
37
|
+
deferred: [],
|
|
33
38
|
},
|
|
34
39
|
background: {
|
|
35
40
|
entry: ["bg_start"],
|
|
@@ -65,8 +70,9 @@ export type OpenPiToolOwner = keyof typeof OPENPI_TOOL_SURFACE;
|
|
|
65
70
|
|
|
66
71
|
export const OPENPI_CAPABILITY_GROUPS = {
|
|
67
72
|
search: {
|
|
68
|
-
owners: ["fileSearch"],
|
|
69
|
-
summary:
|
|
73
|
+
owners: ["fileSearch", "gitRead"],
|
|
74
|
+
summary:
|
|
75
|
+
"Fast structured file and content search (fd, rg) plus read-only git inspection (git_show, git_diff, git_log).",
|
|
70
76
|
},
|
|
71
77
|
delegate: {
|
|
72
78
|
owners: ["subagents"],
|
|
@@ -129,13 +135,14 @@ interface ToolSurfaceState {
|
|
|
129
135
|
subscribed: boolean;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
|
-
const
|
|
138
|
+
export const OPENPI_OWNER_SOURCE_PATHS = {
|
|
133
139
|
capabilities: fileURLToPath(
|
|
134
140
|
new URL("../capabilities/index.ts", import.meta.url),
|
|
135
141
|
),
|
|
136
142
|
fileSearch: fileURLToPath(
|
|
137
143
|
new URL("../file-search/index.ts", import.meta.url),
|
|
138
144
|
),
|
|
145
|
+
gitRead: fileURLToPath(new URL("../git-read/index.ts", import.meta.url)),
|
|
139
146
|
subagents: fileURLToPath(new URL("../subagents/index.ts", import.meta.url)),
|
|
140
147
|
workflows: fileURLToPath(new URL("../workflows/index.ts", import.meta.url)),
|
|
141
148
|
background: fileURLToPath(
|
|
@@ -261,7 +268,7 @@ function availableOwnedToolNames(
|
|
|
261
268
|
}
|
|
262
269
|
|
|
263
270
|
const expectedSource =
|
|
264
|
-
state.sourceByOwner.get(owner) ??
|
|
271
|
+
state.sourceByOwner.get(owner) ?? OPENPI_OWNER_SOURCE_PATHS[owner];
|
|
265
272
|
return new Set(
|
|
266
273
|
reportedTools
|
|
267
274
|
.filter(
|
|
@@ -272,6 +279,33 @@ function availableOwnedToolNames(
|
|
|
272
279
|
);
|
|
273
280
|
}
|
|
274
281
|
|
|
282
|
+
/** Verify that one active tool is the definition owned by this OpenPI source. */
|
|
283
|
+
export function isOwnedToolActive(
|
|
284
|
+
pi: ActiveToolSurface,
|
|
285
|
+
owner: OpenPiToolOwner,
|
|
286
|
+
name: string,
|
|
287
|
+
) {
|
|
288
|
+
if (!ownedToolNames(owner).includes(name)) {
|
|
289
|
+
throw new Error(`${owner} does not own tool ${JSON.stringify(name)}.`);
|
|
290
|
+
}
|
|
291
|
+
return (
|
|
292
|
+
pi.getActiveTools().includes(name) &&
|
|
293
|
+
availableOwnedToolNames(pi, stateFor(pi), owner).has(name)
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Verify that one registered tool is available from this OpenPI source. */
|
|
298
|
+
export function isOwnedToolAvailable(
|
|
299
|
+
pi: ActiveToolSurface,
|
|
300
|
+
owner: OpenPiToolOwner,
|
|
301
|
+
name: string,
|
|
302
|
+
) {
|
|
303
|
+
if (!ownedToolNames(owner).includes(name)) {
|
|
304
|
+
throw new Error(`${owner} does not own tool ${JSON.stringify(name)}.`);
|
|
305
|
+
}
|
|
306
|
+
return availableOwnedToolNames(pi, stateFor(pi), owner).has(name);
|
|
307
|
+
}
|
|
308
|
+
|
|
275
309
|
function projectedOwnerTools(
|
|
276
310
|
pi: ActiveToolSurface,
|
|
277
311
|
state: ToolSurfaceState,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function maxScrollTop(rowCount: number, viewportSize: number) {
|
|
2
|
+
return Math.max(0, rowCount - Math.max(1, viewportSize));
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function clamp(value: number, maximum: number) {
|
|
6
|
+
return Math.max(0, Math.min(value, maximum));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure transcript viewport state with the same follow-to-end contract as Pi's
|
|
11
|
+
* ScrollView. Operators may pause following without new rows moving their
|
|
12
|
+
* absolute reading anchor; reaching the end explicitly resumes following.
|
|
13
|
+
*/
|
|
14
|
+
export class TranscriptViewport {
|
|
15
|
+
scrollTop = 0;
|
|
16
|
+
followingEnd = true;
|
|
17
|
+
|
|
18
|
+
reconcile(rowCount: number, viewportSize: number) {
|
|
19
|
+
const maximum = maxScrollTop(rowCount, viewportSize);
|
|
20
|
+
this.scrollTop = this.followingEnd
|
|
21
|
+
? maximum
|
|
22
|
+
: clamp(this.scrollTop, maximum);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
scrollBy(delta: number, rowCount: number, viewportSize: number) {
|
|
26
|
+
this.reconcile(rowCount, viewportSize);
|
|
27
|
+
const maximum = maxScrollTop(rowCount, viewportSize);
|
|
28
|
+
this.scrollTop = clamp(this.scrollTop + delta, maximum);
|
|
29
|
+
this.followingEnd = this.scrollTop === maximum && delta > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
scrollToTop(rowCount: number, viewportSize: number) {
|
|
33
|
+
this.scrollTop = 0;
|
|
34
|
+
this.followingEnd = false;
|
|
35
|
+
this.reconcile(rowCount, viewportSize);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
scrollToEnd(rowCount: number, viewportSize: number) {
|
|
39
|
+
this.followingEnd = true;
|
|
40
|
+
this.reconcile(rowCount, viewportSize);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
linesBelow(rowCount: number, viewportSize: number) {
|
|
44
|
+
return maxScrollTop(rowCount, viewportSize) - this.scrollTop;
|
|
45
|
+
}
|
|
46
|
+
}
|