@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -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 { truncateToWidth, type Component } from "@earendil-works/pi-tui";
6
+ import type { Component } from "@earendil-works/pi-tui";
9
7
  import type { TSchema } from "typebox";
10
- import { spinnerFrame } from "../shared/spinner.ts";
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 contentWidth = width - HORIZONTAL_PADDING.length * 2;
294
- if (contentWidth <= 0) return [];
295
- return [
296
- `${HORIZONTAL_PADDING}${truncateToWidth(
297
- activityText(name, args, state, theme, cwd),
298
- contentWidth,
299
- "…",
300
- )}${HORIZONTAL_PADDING}`,
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
  };
@@ -20,7 +20,15 @@ import { execFile } from "node:child_process";
20
20
  import { tmpdir } from "node:os";
21
21
  import { dirname, join } from "node:path";
22
22
  import { promisify } from "node:util";
23
- import { Crypto, Data, Effect, Encoding, FileSystem, Stream } from "effect";
23
+ import {
24
+ Crypto,
25
+ Data,
26
+ Effect,
27
+ Encoding,
28
+ FileSystem,
29
+ Layer,
30
+ Stream,
31
+ } from "effect";
24
32
  import { FetchHttpClient, HttpClient } from "effect/unstable/http";
25
33
  import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
26
34
 
@@ -161,6 +169,12 @@ export class InstallError extends Data.TaggedError("InstallError")<{
161
169
  readonly cause?: unknown;
162
170
  }> {}
163
171
 
172
+ export class BinaryDownloadError extends Data.TaggedError(
173
+ "BinaryDownloadError",
174
+ )<{
175
+ readonly message: string;
176
+ }> {}
177
+
164
178
  export interface BinaryEnv {
165
179
  /** True when the executable runs and supports the flags this tool requires. */
166
180
  readonly probe: (command: string, tool: ToolName) => Effect.Effect<boolean>;
@@ -237,9 +251,9 @@ export function readBoundedResponse<E, R>(
237
251
  return Effect.gen(function* () {
238
252
  const declaredLength = Number(response.headers["content-length"]);
239
253
  if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
240
- return yield* Effect.fail(
241
- new Error(`download exceeds the ${maxBytes}-byte size limit`),
242
- );
254
+ return yield* new BinaryDownloadError({
255
+ message: `download exceeds the ${maxBytes}-byte size limit`,
256
+ });
243
257
  }
244
258
 
245
259
  const result = yield* Stream.runFoldEffect(
@@ -249,7 +263,9 @@ export function readBoundedResponse<E, R>(
249
263
  const totalBytes = accumulator.totalBytes + chunk.byteLength;
250
264
  if (totalBytes > maxBytes) {
251
265
  return Effect.fail(
252
- new Error(`download exceeds the ${maxBytes}-byte size limit`),
266
+ new BinaryDownloadError({
267
+ message: `download exceeds the ${maxBytes}-byte size limit`,
268
+ }),
253
269
  );
254
270
  }
255
271
  return Effect.sync(() => {
@@ -272,9 +288,9 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
272
288
 
273
289
  for (let redirects = 0; redirects <= MAX_DOWNLOAD_REDIRECTS; redirects++) {
274
290
  if (url.protocol !== "https:") {
275
- return yield* Effect.fail(
276
- new Error(`refusing non-HTTPS download URL: ${url.href}`),
277
- );
291
+ return yield* new BinaryDownloadError({
292
+ message: `refusing non-HTTPS download URL: ${url.href}`,
293
+ });
278
294
  }
279
295
 
280
296
  const result = yield* Effect.scoped(
@@ -283,31 +299,27 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
283
299
  if ([301, 302, 303, 307, 308].includes(response.status)) {
284
300
  const location = response.headers.location;
285
301
  if (!location) {
286
- return yield* Effect.fail(
287
- new Error(`redirect from ${url.href} had no location header`),
288
- );
302
+ return yield* new BinaryDownloadError({
303
+ message: `redirect from ${url.href} had no location header`,
304
+ });
289
305
  }
290
306
  if (redirects === MAX_DOWNLOAD_REDIRECTS) {
291
- return yield* Effect.fail(
292
- new Error(
293
- `download exceeded ${MAX_DOWNLOAD_REDIRECTS} redirects`,
294
- ),
295
- );
307
+ return yield* new BinaryDownloadError({
308
+ message: `download exceeded ${MAX_DOWNLOAD_REDIRECTS} redirects`,
309
+ });
296
310
  }
297
311
  if (!URL.canParse(location, url)) {
298
- return yield* Effect.fail(
299
- new Error(
300
- `download returned an invalid redirect URL: ${location}`,
301
- ),
302
- );
312
+ return yield* new BinaryDownloadError({
313
+ message: `download returned an invalid redirect URL: ${location}`,
314
+ });
303
315
  }
304
316
  return { _tag: "Redirect" as const, url: new URL(location, url) };
305
317
  }
306
318
 
307
319
  if (response.status < 200 || response.status >= 300) {
308
- return yield* Effect.fail(
309
- new Error(`download failed with HTTP ${response.status}`),
310
- );
320
+ return yield* new BinaryDownloadError({
321
+ message: `download failed with HTTP ${response.status}`,
322
+ });
311
323
  }
312
324
  return {
313
325
  _tag: "Complete" as const,
@@ -320,7 +332,9 @@ function downloadAsset(client: HttpClient.HttpClient, initialUrl: URL) {
320
332
  url = result.url;
321
333
  }
322
334
 
323
- return yield* Effect.fail(new Error("download redirect handling failed"));
335
+ return yield* new BinaryDownloadError({
336
+ message: "download redirect handling failed",
337
+ });
324
338
  });
325
339
  }
326
340
 
@@ -344,9 +358,9 @@ export const liveBinaryEnv: BinaryEnv = {
344
358
  install: (asset, destination) => {
345
359
  const install = Effect.gen(function* () {
346
360
  if (!URL.canParse(asset.url)) {
347
- return yield* Effect.fail(
348
- new Error(`invalid download URL: ${asset.url}`),
349
- );
361
+ return yield* new BinaryDownloadError({
362
+ message: `invalid download URL: ${asset.url}`,
363
+ });
350
364
  }
351
365
 
352
366
  const url = new URL(asset.url);
@@ -360,11 +374,9 @@ export const liveBinaryEnv: BinaryEnv = {
360
374
  const digestBytes = yield* crypto.digest("SHA-256", bytes);
361
375
  const digest = Encoding.encodeHex(digestBytes);
362
376
  if (digest !== asset.sha256) {
363
- return yield* Effect.fail(
364
- new Error(
365
- `SHA-256 mismatch for ${asset.fileName}: expected ${asset.sha256}, received ${digest}`,
366
- ),
367
- );
377
+ return yield* new BinaryDownloadError({
378
+ message: `SHA-256 mismatch for ${asset.fileName}: expected ${asset.sha256}, received ${digest}`,
379
+ });
368
380
  }
369
381
 
370
382
  const workDir = yield* fs.makeTempDirectoryScoped({
@@ -384,9 +396,9 @@ export const liveBinaryEnv: BinaryEnv = {
384
396
  }),
385
397
  ).pipe(Effect.timeout(60_000));
386
398
  if (tarExitCode !== ChildProcessSpawner.ExitCode(0)) {
387
- return yield* Effect.fail(
388
- new Error(`tar failed with exit code ${tarExitCode}`),
389
- );
399
+ return yield* new BinaryDownloadError({
400
+ message: `tar failed with exit code ${tarExitCode}`,
401
+ });
390
402
  }
391
403
 
392
404
  const extracted = join(workDir, asset.archiveDir, asset.binaryName);
@@ -401,13 +413,17 @@ export const liveBinaryEnv: BinaryEnv = {
401
413
  yield* fs.rename(stagedDestination, destination);
402
414
  });
403
415
 
404
- return install.pipe(
405
- Effect.scoped,
406
- Effect.provide(NodeServices.layer),
407
- Effect.provide(NodeHttpClient.layerFetch),
408
- Effect.provideService(FetchHttpClient.RequestInit, {
416
+ const InstallLayer = Layer.mergeAll(
417
+ NodeServices.layer,
418
+ NodeHttpClient.layerFetch,
419
+ Layer.succeed(FetchHttpClient.RequestInit, {
409
420
  redirect: "manual",
410
421
  }),
422
+ );
423
+
424
+ return install.pipe(
425
+ Effect.scoped,
426
+ Effect.provide(InstallLayer),
411
427
  Effect.mapError(
412
428
  (cause) =>
413
429
  new InstallError({
@@ -13,15 +13,13 @@
13
13
 
14
14
  import * as fs from "node:fs";
15
15
  import * as path from "node:path";
16
- import { StringEnum } from "@earendil-works/pi-ai";
17
16
  import type {
18
17
  AgentToolResult,
19
18
  ExtensionAPI,
20
- ExtensionContext,
21
19
  } from "@earendil-works/pi-coding-agent";
22
20
  import { Text } from "@earendil-works/pi-tui";
23
21
  import { Cause, Effect, Exit } from "effect";
24
- import { type Static, Type } from "typebox";
22
+ import { Type } from "typebox";
25
23
  import { formatCapturedOutput } from "../file-search/src/output.ts";
26
24
  import { sanitizeTerminalText } from "../shared/terminal-text.ts";
27
25
  import {