@tt-a1i/openpi 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -24
- package/SETUP.md +3 -3
- package/extensions/ask-user/index.ts +30 -14
- package/extensions/background-terminals/src/prompt.ts +1 -1
- package/extensions/background-terminals/src/ui/ps.ts +132 -129
- package/extensions/capabilities/index.ts +35 -44
- package/extensions/capabilities/src/ui.ts +93 -0
- package/extensions/file-mutation-display/index.ts +34 -76
- package/extensions/file-mutation-display/render.ts +387 -88
- package/extensions/file-search/index.ts +8 -7
- package/extensions/file-search/src/binaries.ts +18 -18
- package/extensions/git-info/src/changed-files-view.ts +47 -14
- package/extensions/git-read/index.ts +330 -0
- package/extensions/git-read/src/args.ts +171 -0
- package/extensions/git-read/src/process.ts +81 -0
- package/extensions/git-read/src/prompt.ts +56 -0
- package/extensions/sessions/index.ts +70 -55
- package/extensions/setup/index.ts +6 -6
- package/extensions/shared/activity-status.ts +6 -5
- package/extensions/shared/below-editor-navigation.ts +26 -0
- package/extensions/shared/capability-intent.ts +53 -0
- package/extensions/shared/child-session.ts +7 -1
- package/extensions/shared/result-budget.ts +134 -0
- package/extensions/shared/screen-chrome.ts +133 -0
- package/extensions/shared/setup-config.ts +24 -5
- package/extensions/shared/spinner.ts +28 -0
- package/extensions/shared/text-projection.ts +56 -0
- package/extensions/shared/tool-surface.ts +13 -6
- package/extensions/subagents/index.ts +216 -170
- package/extensions/subagents/navigation.ts +52 -23
- package/extensions/subagents/src/agent-types.ts +37 -15
- package/extensions/subagents/src/backends/stub.ts +7 -0
- package/extensions/subagents/src/id-sequence.ts +84 -0
- package/extensions/subagents/src/manager.ts +620 -537
- package/extensions/subagents/src/prompt.ts +153 -38
- package/extensions/subagents/src/result-artifact.ts +142 -0
- package/extensions/subagents/src/result-delivery.ts +50 -5
- package/extensions/subagents/src/runtime.ts +8 -5
- package/extensions/subagents/src/ui/takeover.ts +84 -109
- package/extensions/subagents/src/ui/transcript.ts +76 -42
- package/extensions/subagents/src/ui/wait-result.ts +1 -1
- package/extensions/tasks/ui.ts +79 -62
- package/extensions/ui-customization/footer.ts +7 -4
- package/extensions/user-input-fold/index.ts +185 -0
- package/extensions/workflows/artifacts.ts +35 -0
- package/extensions/workflows/controller.ts +14 -2
- package/extensions/workflows/coordinator.ts +64 -0
- package/extensions/workflows/dashboard.ts +353 -173
- package/extensions/workflows/handoff.ts +62 -20
- package/extensions/workflows/index.ts +647 -387
- package/extensions/workflows/model.ts +57 -15
- package/extensions/workflows/navigation.ts +33 -14
- package/extensions/workflows/prompt.ts +104 -8
- package/extensions/workflows/replay-safety.ts +16 -6
- package/extensions/workflows/result-delivery.ts +189 -0
- package/extensions/workflows/sandbox-child.cjs +11 -0
- package/package.json +1 -1
- package/skills/subagents/SKILL.md +2 -2
- package/skills/workflows/REFERENCE.md +7 -4
- package/skills/workflows/SKILL.md +53 -10
- package/extensions/subagents/src/format.ts +0 -48
|
@@ -1,88 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createBashToolDefinition,
|
|
3
3
|
createEditToolDefinition,
|
|
4
|
+
createFindToolDefinition,
|
|
5
|
+
createGrepToolDefinition,
|
|
6
|
+
createLsToolDefinition,
|
|
7
|
+
createReadToolDefinition,
|
|
4
8
|
createWriteToolDefinition,
|
|
5
9
|
type ExtensionAPI,
|
|
6
10
|
type ToolDefinition,
|
|
7
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { TSchema } from "typebox";
|
|
8
13
|
import { loadSetupConfig } from "../shared/setup-config.ts";
|
|
9
|
-
import {
|
|
10
|
-
compactBashRenderedComponent,
|
|
11
|
-
compactRenderedComponent,
|
|
12
|
-
singleLineRenderedComponent,
|
|
13
|
-
} from "./render.ts";
|
|
14
|
-
|
|
15
|
-
function withCompactCallRenderer(
|
|
16
|
-
definition: ToolDefinition<any, any, any>,
|
|
17
|
-
): ToolDefinition<any, any, any> {
|
|
18
|
-
const renderCall = definition.renderCall;
|
|
19
|
-
if (!renderCall) return definition;
|
|
20
|
-
return {
|
|
21
|
-
...definition,
|
|
22
|
-
renderCall(args, theme, context) {
|
|
23
|
-
const component = renderCall(args, theme, context);
|
|
24
|
-
if (
|
|
25
|
-
context.expanded ||
|
|
26
|
-
loadSetupConfig().ui.fileMutationDisplay === "full"
|
|
27
|
-
) {
|
|
28
|
-
return component;
|
|
29
|
-
}
|
|
30
|
-
// While the model is still streaming a large Write/Edit payload, a
|
|
31
|
-
// syntax-highlighted preview changes on nearly every token and makes the
|
|
32
|
-
// entire tool block flash. Keep that phase to a stable one-line header;
|
|
33
|
-
// reveal the bounded preview once arguments are complete.
|
|
34
|
-
if (!context.argsComplete) {
|
|
35
|
-
return singleLineRenderedComponent(component, theme);
|
|
36
|
-
}
|
|
37
|
-
const background = context.isPartial
|
|
38
|
-
? (text: string) => theme.bg("toolPendingBg", text)
|
|
39
|
-
: context.isError
|
|
40
|
-
? (text: string) => theme.bg("toolErrorBg", text)
|
|
41
|
-
: (text: string) => theme.bg("toolSuccessBg", text);
|
|
42
|
-
return compactRenderedComponent(component, theme, undefined, background);
|
|
43
|
-
},
|
|
44
|
-
};
|
|
45
|
-
}
|
|
14
|
+
import { withActivityRenderer } from "./render.ts";
|
|
46
15
|
|
|
47
|
-
function
|
|
48
|
-
definition: ToolDefinition<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!renderCall || !renderResult) return definition;
|
|
53
|
-
return {
|
|
54
|
-
...definition,
|
|
55
|
-
renderCall(args, theme, context) {
|
|
56
|
-
// A compact wrapper is not the native Text component expected through
|
|
57
|
-
// lastComponent, so rebuild the cheap call renderer on each update.
|
|
58
|
-
const component = renderCall(args, theme, {
|
|
59
|
-
...context,
|
|
60
|
-
lastComponent: undefined,
|
|
61
|
-
});
|
|
62
|
-
if (context.expanded || loadSetupConfig().ui.bashToolDisplay === "full") {
|
|
63
|
-
return component;
|
|
64
|
-
}
|
|
65
|
-
return singleLineRenderedComponent(component, theme);
|
|
66
|
-
},
|
|
67
|
-
renderResult(result, options, theme, context) {
|
|
68
|
-
// Bash streams partial results. Never hand our wrapper back to Pi's
|
|
69
|
-
// native BashResultRenderComponent updater.
|
|
70
|
-
const component = renderResult(result, options, theme, {
|
|
71
|
-
...context,
|
|
72
|
-
lastComponent: undefined,
|
|
73
|
-
});
|
|
74
|
-
if (options.expanded || loadSetupConfig().ui.bashToolDisplay === "full") {
|
|
75
|
-
return component;
|
|
76
|
-
}
|
|
77
|
-
return compactBashRenderedComponent(component, theme);
|
|
78
|
-
},
|
|
79
|
-
};
|
|
16
|
+
function compact<TParams extends TSchema, TDetails, TState>(
|
|
17
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
18
|
+
enabled: boolean,
|
|
19
|
+
) {
|
|
20
|
+
return enabled ? withActivityRenderer(definition) : definition;
|
|
80
21
|
}
|
|
81
22
|
|
|
82
23
|
/**
|
|
83
|
-
* Override only
|
|
84
|
-
*
|
|
85
|
-
* errors stay on the upstream implementation.
|
|
24
|
+
* Override only Pi's TUI projection. Every wrapped definition retains its
|
|
25
|
+
* native schema, prompt metadata, execute function, result, and details.
|
|
86
26
|
*/
|
|
87
27
|
export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
88
28
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -96,10 +36,28 @@ export default function fileMutationDisplay(pi: ExtensionAPI) {
|
|
|
96
36
|
display.fileMutationDisplay === "full",
|
|
97
37
|
);
|
|
98
38
|
}
|
|
99
|
-
|
|
39
|
+
|
|
40
|
+
pi.registerTool(
|
|
41
|
+
compact(
|
|
42
|
+
createBashToolDefinition(ctx.cwd),
|
|
43
|
+
display.bashToolDisplay !== "full",
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
pi.registerTool(
|
|
47
|
+
compact(
|
|
48
|
+
createWriteToolDefinition(ctx.cwd),
|
|
49
|
+
display.fileMutationDisplay !== "full",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
100
52
|
pi.registerTool(
|
|
101
|
-
|
|
53
|
+
compact(
|
|
54
|
+
createEditToolDefinition(ctx.cwd),
|
|
55
|
+
display.fileMutationDisplay !== "full",
|
|
56
|
+
),
|
|
102
57
|
);
|
|
103
|
-
pi.registerTool(
|
|
58
|
+
pi.registerTool(withActivityRenderer(createReadToolDefinition(ctx.cwd)));
|
|
59
|
+
pi.registerTool(withActivityRenderer(createGrepToolDefinition(ctx.cwd)));
|
|
60
|
+
pi.registerTool(withActivityRenderer(createFindToolDefinition(ctx.cwd)));
|
|
61
|
+
pi.registerTool(withActivityRenderer(createLsToolDefinition(ctx.cwd)));
|
|
104
62
|
});
|
|
105
63
|
}
|
|
@@ -1,107 +1,406 @@
|
|
|
1
|
+
import { isAbsolute, relative } from "node:path";
|
|
1
2
|
import { stripVTControlCharacters } from "node:util";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from "@earendil-works/pi-tui";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
theme: Theme,
|
|
23
|
-
): Component {
|
|
24
|
-
return {
|
|
25
|
-
render(width) {
|
|
26
|
-
const lines = component.render(width);
|
|
27
|
-
if (lines.length <= 1) return lines;
|
|
28
|
-
if (width <= 2) return [truncateToWidth(lines[0] ?? "", width, "")];
|
|
29
|
-
return [
|
|
30
|
-
truncateToWidth(lines[0] ?? "", width - 2, "") + theme.fg("dim", " …"),
|
|
31
|
-
];
|
|
32
|
-
},
|
|
33
|
-
invalidate() {
|
|
34
|
-
component.invalidate();
|
|
35
|
-
},
|
|
3
|
+
import type {
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
Theme,
|
|
6
|
+
ToolDefinition,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
9
|
+
import type { TSchema } from "typebox";
|
|
10
|
+
import { spinnerFrame } from "../shared/spinner.ts";
|
|
11
|
+
|
|
12
|
+
type ActivityStatus = "pending" | "success" | "error";
|
|
13
|
+
|
|
14
|
+
type ActivityRenderState<TDetails> = {
|
|
15
|
+
openpiActivity?: {
|
|
16
|
+
result?: AgentToolResult<TDetails>;
|
|
17
|
+
status: ActivityStatus;
|
|
18
|
+
startedAt?: number;
|
|
19
|
+
endedAt?: number;
|
|
20
|
+
interval?: NodeJS.Timeout;
|
|
21
|
+
nativeCallComponent?: Component;
|
|
22
|
+
nativeResultComponent?: Component;
|
|
36
23
|
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ActivityRow = {
|
|
27
|
+
verb: string;
|
|
28
|
+
target: string;
|
|
29
|
+
detail?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const HORIZONTAL_PADDING = " ";
|
|
33
|
+
|
|
34
|
+
const emptyComponent: Component = {
|
|
35
|
+
render: () => [],
|
|
36
|
+
invalidate() {},
|
|
37
|
+
};
|
|
38
|
+
|
|
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
|
+
function textOutput(result: AgentToolResult<unknown> | undefined) {
|
|
54
|
+
return (
|
|
55
|
+
result?.content
|
|
56
|
+
.filter((item) => item.type === "text")
|
|
57
|
+
.map((item) => item.text)
|
|
58
|
+
.join("\n") ?? ""
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
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;
|
|
37
118
|
}
|
|
38
119
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
+
function activityComponent(
|
|
285
|
+
name: string,
|
|
286
|
+
args: unknown,
|
|
287
|
+
state: NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
42
288
|
theme: Theme,
|
|
43
|
-
|
|
289
|
+
cwd: string,
|
|
44
290
|
): Component {
|
|
45
291
|
return {
|
|
46
292
|
render(width) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
(line) => stripVTControlCharacters(line).trim().length > 0,
|
|
50
|
-
);
|
|
51
|
-
let nativeHidden = 0;
|
|
52
|
-
const content: string[] = [];
|
|
53
|
-
const metadata: string[] = [];
|
|
54
|
-
let status: string | undefined;
|
|
55
|
-
for (const line of visible) {
|
|
56
|
-
const plain = stripVTControlCharacters(line).trim();
|
|
57
|
-
const hiddenMatch = plain.match(/\(?([0-9]+) earlier lines,/);
|
|
58
|
-
if (hiddenMatch) {
|
|
59
|
-
nativeHidden += Number(hiddenMatch[1]);
|
|
60
|
-
} else if (/^(Took|Elapsed)\s/.test(plain)) {
|
|
61
|
-
status = line;
|
|
62
|
-
} else if (/^\[(Full output|Truncated):/.test(plain)) {
|
|
63
|
-
metadata.push(line);
|
|
64
|
-
} else {
|
|
65
|
-
content.push(line);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const preview = content.slice(0, maximum);
|
|
69
|
-
const hidden =
|
|
70
|
-
nativeHidden + Math.max(0, content.length - preview.length);
|
|
293
|
+
const contentWidth = width - HORIZONTAL_PADDING.length * 2;
|
|
294
|
+
if (contentWidth <= 0) return [];
|
|
71
295
|
return [
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
296
|
+
`${HORIZONTAL_PADDING}${truncateToWidth(
|
|
297
|
+
activityText(name, args, state, theme, cwd),
|
|
298
|
+
contentWidth,
|
|
299
|
+
"…",
|
|
300
|
+
)}${HORIZONTAL_PADDING}`,
|
|
76
301
|
];
|
|
77
302
|
},
|
|
78
|
-
invalidate() {
|
|
79
|
-
component.invalidate();
|
|
80
|
-
},
|
|
303
|
+
invalidate() {},
|
|
81
304
|
};
|
|
82
305
|
}
|
|
83
306
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Preserve Pi's complete tool definition and execution semantics while
|
|
309
|
+
* replacing only the collapsed operator-facing projection.
|
|
310
|
+
*/
|
|
311
|
+
export function withActivityRenderer<TParams extends TSchema, TDetails, TState>(
|
|
312
|
+
definition: ToolDefinition<TParams, TDetails, TState>,
|
|
313
|
+
): ToolDefinition<TParams, TDetails, TState & ActivityRenderState<TDetails>> {
|
|
314
|
+
const nativeRenderCall = definition.renderCall;
|
|
315
|
+
const nativeRenderResult = definition.renderResult;
|
|
90
316
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
317
|
+
...definition,
|
|
318
|
+
renderShell: "self",
|
|
319
|
+
renderCall(args, theme, context) {
|
|
320
|
+
const state = context.state as TState & ActivityRenderState<TDetails>;
|
|
321
|
+
state.openpiActivity ??= { status: "pending" };
|
|
322
|
+
const activity = state.openpiActivity;
|
|
323
|
+
if (context.executionStarted && activity.startedAt === undefined) {
|
|
324
|
+
activity.startedAt = Date.now();
|
|
325
|
+
}
|
|
326
|
+
if (
|
|
327
|
+
context.executionStarted &&
|
|
328
|
+
definition.name === "bash" &&
|
|
329
|
+
activity.status === "pending" &&
|
|
330
|
+
!context.expanded &&
|
|
331
|
+
activity.interval === undefined
|
|
332
|
+
) {
|
|
333
|
+
activity.interval = setInterval(() => context.invalidate(), 1000);
|
|
334
|
+
activity.interval.unref();
|
|
335
|
+
}
|
|
336
|
+
if (context.expanded && activity.interval) {
|
|
337
|
+
clearInterval(activity.interval);
|
|
338
|
+
activity.interval = undefined;
|
|
339
|
+
}
|
|
340
|
+
if (context.expanded && nativeRenderCall) {
|
|
341
|
+
const nativeContext: Parameters<typeof nativeRenderCall>[2] = {
|
|
342
|
+
...context,
|
|
343
|
+
state,
|
|
344
|
+
lastComponent: activity.nativeCallComponent,
|
|
345
|
+
};
|
|
346
|
+
const component = nativeRenderCall(args, theme, nativeContext);
|
|
347
|
+
activity.nativeCallComponent = component;
|
|
348
|
+
return component;
|
|
349
|
+
}
|
|
350
|
+
return activityComponent(
|
|
351
|
+
definition.name,
|
|
352
|
+
args,
|
|
353
|
+
activity as NonNullable<ActivityRenderState<unknown>["openpiActivity"]>,
|
|
354
|
+
theme,
|
|
355
|
+
context.cwd,
|
|
356
|
+
);
|
|
102
357
|
},
|
|
103
|
-
|
|
104
|
-
|
|
358
|
+
renderResult(result, options, theme, context) {
|
|
359
|
+
const state = context.state as TState & ActivityRenderState<TDetails>;
|
|
360
|
+
state.openpiActivity ??= { status: "pending" };
|
|
361
|
+
const activity = state.openpiActivity;
|
|
362
|
+
activity.result = result;
|
|
363
|
+
activity.status = options.isPartial
|
|
364
|
+
? "pending"
|
|
365
|
+
: context.isError
|
|
366
|
+
? "error"
|
|
367
|
+
: "success";
|
|
368
|
+
if (
|
|
369
|
+
options.isPartial &&
|
|
370
|
+
definition.name === "bash" &&
|
|
371
|
+
!options.expanded &&
|
|
372
|
+
activity.interval === undefined
|
|
373
|
+
) {
|
|
374
|
+
activity.interval = setInterval(() => context.invalidate(), 1000);
|
|
375
|
+
activity.interval.unref();
|
|
376
|
+
}
|
|
377
|
+
if (!options.isPartial || context.isError || options.expanded) {
|
|
378
|
+
activity.endedAt ??= Date.now();
|
|
379
|
+
if (activity.interval) {
|
|
380
|
+
clearInterval(activity.interval);
|
|
381
|
+
activity.interval = undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (options.isPartial && options.expanded) {
|
|
385
|
+
activity.endedAt = undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (options.expanded && nativeRenderResult) {
|
|
389
|
+
const nativeContext: Parameters<typeof nativeRenderResult>[3] = {
|
|
390
|
+
...context,
|
|
391
|
+
state,
|
|
392
|
+
lastComponent: activity.nativeResultComponent,
|
|
393
|
+
};
|
|
394
|
+
const component = nativeRenderResult(
|
|
395
|
+
result,
|
|
396
|
+
options,
|
|
397
|
+
theme,
|
|
398
|
+
nativeContext,
|
|
399
|
+
);
|
|
400
|
+
activity.nativeResultComponent = component;
|
|
401
|
+
return component;
|
|
402
|
+
}
|
|
403
|
+
return emptyComponent;
|
|
105
404
|
},
|
|
106
405
|
};
|
|
107
406
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* On session start the extension resolves a usable binary for each tool:
|
|
5
5
|
* a normally installed system binary is preferred (silently), then an
|
|
6
|
-
* existing
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* existing binary in the agent's managed bin directory (`~/.pi/agent/bin`,
|
|
7
|
+
* silently — e.g. cached by an earlier download), and only when neither
|
|
8
|
+
* exists is an official release downloaded into that directory — the single
|
|
9
|
+
* case that shows a UI notification. Tools await that initialization before
|
|
10
|
+
* executing, and report a clear error if it failed.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import * as NodeServices from "@effect/platform-node/NodeServices";
|
|
@@ -33,7 +34,7 @@ import {
|
|
|
33
34
|
import {
|
|
34
35
|
currentTarget,
|
|
35
36
|
liveBinaryEnv,
|
|
36
|
-
|
|
37
|
+
managedBinDir,
|
|
37
38
|
resolveBinary,
|
|
38
39
|
TOOL_SPECS,
|
|
39
40
|
type BinaryEnv,
|
|
@@ -80,7 +81,7 @@ export function installNotifications(binaries: readonly ResolvedBinary[]) {
|
|
|
80
81
|
.map(
|
|
81
82
|
(binary) =>
|
|
82
83
|
`file-search: no system ${binary.tool} found — downloaded ${binary.tool} ${binary.version ?? ""}`.trimEnd() +
|
|
83
|
-
` to ${
|
|
84
|
+
` to ${managedBinDir()}`,
|
|
84
85
|
);
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -136,7 +137,7 @@ export default function fileSearchTools(pi: ExtensionAPI) {
|
|
|
136
137
|
}
|
|
137
138
|
};
|
|
138
139
|
|
|
139
|
-
const binDir =
|
|
140
|
+
const binDir = managedBinDir();
|
|
140
141
|
const target = currentTarget();
|
|
141
142
|
const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
|
|
142
143
|
|