@tt-a1i/openpi 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +87 -24
  2. package/SETUP.md +3 -3
  3. package/extensions/ask-user/index.ts +30 -14
  4. package/extensions/background-terminals/src/prompt.ts +1 -1
  5. package/extensions/background-terminals/src/ui/ps.ts +132 -129
  6. package/extensions/capabilities/index.ts +30 -42
  7. package/extensions/capabilities/src/ui.ts +93 -0
  8. package/extensions/file-mutation-display/index.ts +34 -76
  9. package/extensions/file-mutation-display/render.ts +387 -88
  10. package/extensions/file-search/index.ts +8 -7
  11. package/extensions/file-search/src/binaries.ts +18 -18
  12. package/extensions/git-info/src/changed-files-view.ts +47 -14
  13. package/extensions/git-read/index.ts +330 -0
  14. package/extensions/git-read/src/args.ts +171 -0
  15. package/extensions/git-read/src/process.ts +81 -0
  16. package/extensions/git-read/src/prompt.ts +56 -0
  17. package/extensions/sessions/index.ts +70 -55
  18. package/extensions/setup/index.ts +6 -6
  19. package/extensions/shared/activity-status.ts +6 -5
  20. package/extensions/shared/below-editor-navigation.ts +26 -0
  21. package/extensions/shared/capability-intent.ts +53 -0
  22. package/extensions/shared/child-session.ts +7 -1
  23. package/extensions/shared/result-budget.ts +134 -0
  24. package/extensions/shared/screen-chrome.ts +133 -0
  25. package/extensions/shared/setup-config.ts +24 -5
  26. package/extensions/shared/spinner.ts +28 -0
  27. package/extensions/shared/text-projection.ts +56 -0
  28. package/extensions/shared/tool-surface.ts +13 -6
  29. package/extensions/subagents/index.ts +204 -140
  30. package/extensions/subagents/navigation.ts +52 -23
  31. package/extensions/subagents/src/agent-types.ts +37 -15
  32. package/extensions/subagents/src/backends/stub.ts +7 -0
  33. package/extensions/subagents/src/id-sequence.ts +84 -0
  34. package/extensions/subagents/src/manager.ts +620 -537
  35. package/extensions/subagents/src/prompt.ts +153 -38
  36. package/extensions/subagents/src/result-artifact.ts +142 -0
  37. package/extensions/subagents/src/runtime.ts +8 -5
  38. package/extensions/subagents/src/ui/takeover.ts +84 -109
  39. package/extensions/subagents/src/ui/transcript.ts +76 -42
  40. package/extensions/subagents/src/ui/wait-result.ts +1 -1
  41. package/extensions/tasks/ui.ts +79 -62
  42. package/extensions/ui-customization/footer.ts +7 -4
  43. package/extensions/user-input-fold/index.ts +185 -0
  44. package/extensions/workflows/artifacts.ts +35 -0
  45. package/extensions/workflows/controller.ts +14 -2
  46. package/extensions/workflows/coordinator.ts +64 -0
  47. package/extensions/workflows/dashboard.ts +353 -173
  48. package/extensions/workflows/handoff.ts +62 -20
  49. package/extensions/workflows/index.ts +647 -387
  50. package/extensions/workflows/model.ts +57 -15
  51. package/extensions/workflows/navigation.ts +33 -14
  52. package/extensions/workflows/prompt.ts +104 -8
  53. package/extensions/workflows/replay-safety.ts +16 -6
  54. package/extensions/workflows/result-delivery.ts +189 -0
  55. package/extensions/workflows/sandbox-child.cjs +11 -0
  56. package/package.json +1 -1
  57. package/skills/subagents/SKILL.md +2 -2
  58. package/skills/workflows/REFERENCE.md +7 -4
  59. package/skills/workflows/SKILL.md +53 -10
  60. 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 withCompactBashRenderer(
48
- definition: ToolDefinition<any, any, any>,
49
- ): ToolDefinition<any, any, any> {
50
- const renderCall = definition.renderCall;
51
- const renderResult = definition.renderResult;
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 the TUI renderers. The wrapped definitions are Pi's native
84
- * Bash/Write/Edit tools, so schemas, execution, mutation queues, diffs, and
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
- pi.registerTool(withCompactBashRenderer(createBashToolDefinition(ctx.cwd)));
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
- withCompactCallRenderer(createWriteToolDefinition(ctx.cwd)),
53
+ compact(
54
+ createEditToolDefinition(ctx.cwd),
55
+ display.fileMutationDisplay !== "full",
56
+ ),
102
57
  );
103
- pi.registerTool(withCompactCallRenderer(createEditToolDefinition(ctx.cwd)));
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 { keyHint, type Theme } from "@earendil-works/pi-coding-agent";
3
- import {
4
- truncateToWidth,
5
- visibleWidth,
6
- type Component,
7
- } from "@earendil-works/pi-tui";
8
-
9
- // Native Write/Edit renderings include their title and context rows. Three
10
- // visible rows keep the operation identifiable while making repeated file
11
- // mutations substantially quieter than Claude Code's default preview.
12
- export const FILE_MUTATION_PREVIEW_LINES = 3;
13
- export const BASH_OUTPUT_PREVIEW_LINES = 1;
14
-
15
- function expandHint(hidden: number) {
16
- return `… ${hidden} more line${hidden === 1 ? "" : "s"} · ${keyHint("app.tools.expand", "to expand")}`;
17
- }
18
-
19
- /** Keep a long command identifiable without allowing it to wrap for many rows. */
20
- export function singleLineRenderedComponent(
21
- component: Component,
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
- /** Preserve one output row, warnings, and final timing in compact Bash mode. */
40
- export function compactBashRenderedComponent(
41
- component: Component,
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
- maximum = BASH_OUTPUT_PREVIEW_LINES,
289
+ cwd: string,
44
290
  ): Component {
45
291
  return {
46
292
  render(width) {
47
- const rendered = component.render(width);
48
- const visible = rendered.filter(
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
- ...preview,
73
- ...(hidden > 0 ? [theme.fg("dim", expandHint(hidden))] : []),
74
- ...metadata,
75
- ...(status ? [status] : []),
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
- export function compactRenderedComponent(
85
- component: Component,
86
- theme: Theme,
87
- maximum = FILE_MUTATION_PREVIEW_LINES,
88
- background?: (text: string) => string,
89
- ): Component {
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
- render(width) {
92
- const lines = component.render(width);
93
- if (lines.length <= maximum) return lines;
94
- const hidden = lines.length - maximum;
95
- const hint = theme.fg("dim", expandHint(hidden));
96
- const paddedHint =
97
- hint + " ".repeat(Math.max(0, width - visibleWidth(hint)));
98
- return [
99
- ...lines.slice(0, maximum),
100
- background ? background(paddedHint) : hint,
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
- invalidate() {
104
- component.invalidate();
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 fallback in this repo's `bin/` directory (silently), and only
7
- * when neither exists is an official release downloaded into `bin/` — the
8
- * single case that shows a UI notification. Tools await that initialization
9
- * before executing, and report a clear error if it failed.
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
- repositoryBinDir,
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 ${repositoryBinDir()}`,
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 = repositoryBinDir();
140
+ const binDir = managedBinDir();
140
141
  const target = currentTarget();
141
142
  const initializers = makeBinaryInitializers(binDir, target, liveBinaryEnv);
142
143