@oh-my-pi/pi-coding-agent 15.0.0 → 15.0.2

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 (165) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +10 -10
  4. package/scripts/build-binary.ts +5 -0
  5. package/src/autoresearch/helpers.ts +17 -0
  6. package/src/autoresearch/tools/log-experiment.ts +9 -17
  7. package/src/autoresearch/tools/run-experiment.ts +2 -17
  8. package/src/capability/skill.ts +7 -0
  9. package/src/cli/list-models.ts +1 -1
  10. package/src/cli/shell-cli.ts +3 -13
  11. package/src/cli/update-cli.ts +1 -1
  12. package/src/cli.ts +10 -29
  13. package/src/commands/commit.ts +10 -0
  14. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  15. package/src/commit/analysis/conventional.ts +8 -66
  16. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  17. package/src/commit/pipeline.ts +2 -2
  18. package/src/commit/shared-llm.ts +89 -0
  19. package/src/config/config-file.ts +210 -0
  20. package/src/config/model-equivalence.ts +8 -11
  21. package/src/config/model-registry.ts +44 -3
  22. package/src/config/model-resolver.ts +1 -4
  23. package/src/config/settings-schema.ts +82 -1
  24. package/src/config/settings.ts +1 -1
  25. package/src/config.ts +3 -219
  26. package/src/discovery/claude-plugins.ts +19 -7
  27. package/src/edit/renderer.ts +7 -1
  28. package/src/eval/js/executor.ts +3 -0
  29. package/src/eval/js/shared/rewrite-imports.ts +2 -2
  30. package/src/eval/py/executor.ts +5 -0
  31. package/src/eval/py/runner.py +42 -11
  32. package/src/eval/py/runtime.ts +1 -0
  33. package/src/exa/factory.ts +2 -2
  34. package/src/exa/mcp-client.ts +74 -1
  35. package/src/exec/bash-executor.ts +5 -1
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +0 -11
  38. package/src/extensibility/extensions/get-commands-handler.ts +77 -0
  39. package/src/extensibility/extensions/runner.ts +1 -1
  40. package/src/extensibility/extensions/types.ts +89 -223
  41. package/src/extensibility/hooks/types.ts +89 -314
  42. package/src/extensibility/plugins/legacy-pi-compat.ts +48 -31
  43. package/src/extensibility/shared-events.ts +343 -0
  44. package/src/extensibility/skills.ts +9 -0
  45. package/src/goals/index.ts +3 -0
  46. package/src/goals/runtime.ts +500 -0
  47. package/src/goals/state.ts +37 -0
  48. package/src/goals/tools/goal-tool.ts +237 -0
  49. package/src/hashline/anchors.ts +2 -2
  50. package/src/hashline/input.ts +2 -1
  51. package/src/hashline/parser.ts +27 -3
  52. package/src/hindsight/mental-models.ts +1 -1
  53. package/src/internal-urls/agent-protocol.ts +1 -20
  54. package/src/internal-urls/artifact-protocol.ts +1 -19
  55. package/src/internal-urls/docs-index.generated.ts +11 -12
  56. package/src/internal-urls/registry-helpers.ts +25 -0
  57. package/src/internal-urls/router.ts +8 -0
  58. package/src/internal-urls/types.ts +21 -0
  59. package/src/lsp/config.ts +15 -6
  60. package/src/lsp/defaults.json +6 -2
  61. package/src/main.ts +11 -2
  62. package/src/mcp/oauth-flow.ts +20 -0
  63. package/src/modes/acp/acp-agent.ts +327 -95
  64. package/src/modes/components/assistant-message.ts +14 -8
  65. package/src/modes/components/bash-execution.ts +24 -63
  66. package/src/modes/components/custom-message.ts +14 -40
  67. package/src/modes/components/eval-execution.ts +27 -57
  68. package/src/modes/components/execution-shared.ts +102 -0
  69. package/src/modes/components/hook-message.ts +17 -49
  70. package/src/modes/components/mcp-add-wizard.ts +26 -5
  71. package/src/modes/components/message-frame.ts +88 -0
  72. package/src/modes/components/model-selector.ts +1 -1
  73. package/src/modes/components/session-observer-overlay.ts +6 -2
  74. package/src/modes/components/session-selector.ts +1 -1
  75. package/src/modes/components/status-line/segments.ts +93 -8
  76. package/src/modes/components/status-line/types.ts +4 -0
  77. package/src/modes/components/status-line.ts +28 -10
  78. package/src/modes/components/tool-execution.ts +7 -8
  79. package/src/modes/controllers/command-controller-shared.ts +108 -0
  80. package/src/modes/controllers/command-controller.ts +13 -4
  81. package/src/modes/controllers/event-controller.ts +36 -7
  82. package/src/modes/controllers/extension-ui-controller.ts +3 -2
  83. package/src/modes/controllers/input-controller.ts +13 -0
  84. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  85. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  86. package/src/modes/interactive-mode.ts +624 -52
  87. package/src/modes/print-mode.ts +16 -86
  88. package/src/modes/rpc/host-uris.ts +235 -0
  89. package/src/modes/rpc/rpc-mode.ts +41 -88
  90. package/src/modes/rpc/rpc-types.ts +57 -0
  91. package/src/modes/runtime-init.ts +116 -0
  92. package/src/modes/theme/defaults/dark-poimandres.json +3 -0
  93. package/src/modes/theme/defaults/light-poimandres.json +3 -0
  94. package/src/modes/theme/theme.ts +24 -6
  95. package/src/modes/types.ts +14 -3
  96. package/src/modes/utils/context-usage.ts +13 -13
  97. package/src/modes/utils/ui-helpers.ts +10 -3
  98. package/src/plan-mode/approved-plan.ts +35 -1
  99. package/src/prompts/goals/goal-budget-limit.md +16 -0
  100. package/src/prompts/goals/goal-continuation.md +28 -0
  101. package/src/prompts/goals/goal-mode-active.md +23 -0
  102. package/src/prompts/system/plan-mode-active.md +5 -5
  103. package/src/prompts/system/plan-mode-tool-decision-reminder.md +1 -1
  104. package/src/prompts/tools/bash.md +6 -0
  105. package/src/prompts/tools/github.md +4 -4
  106. package/src/prompts/tools/goal.md +13 -0
  107. package/src/prompts/tools/hashline.md +101 -117
  108. package/src/prompts/tools/read.md +55 -36
  109. package/src/prompts/tools/resolve.md +6 -5
  110. package/src/sdk.ts +12 -5
  111. package/src/session/agent-session.ts +428 -106
  112. package/src/session/blob-store.ts +36 -3
  113. package/src/session/messages.ts +67 -2
  114. package/src/session/session-manager.ts +131 -12
  115. package/src/session/session-storage.ts +33 -15
  116. package/src/session/streaming-output.ts +309 -13
  117. package/src/slash-commands/builtin-registry.ts +18 -0
  118. package/src/ssh/ssh-executor.ts +5 -0
  119. package/src/system-prompt.ts +4 -2
  120. package/src/task/discovery.ts +5 -2
  121. package/src/task/executor.ts +19 -8
  122. package/src/task/index.ts +3 -0
  123. package/src/task/render.ts +21 -15
  124. package/src/task/types.ts +4 -0
  125. package/src/tools/ast-edit.ts +21 -120
  126. package/src/tools/ast-grep.ts +21 -119
  127. package/src/tools/bash-command-fixup.ts +47 -0
  128. package/src/tools/bash-interactive.ts +9 -1
  129. package/src/tools/bash.ts +66 -19
  130. package/src/tools/browser/attach.ts +3 -3
  131. package/src/tools/browser/launch.ts +81 -18
  132. package/src/tools/browser/registry.ts +1 -5
  133. package/src/tools/browser/render.ts +2 -2
  134. package/src/tools/browser/tab-supervisor.ts +51 -14
  135. package/src/tools/conflict-detect.ts +15 -4
  136. package/src/tools/eval.ts +12 -2
  137. package/src/tools/find.ts +20 -38
  138. package/src/tools/gh.ts +44 -10
  139. package/src/tools/index.ts +22 -11
  140. package/src/tools/inspect-image.ts +3 -10
  141. package/src/tools/job.ts +16 -7
  142. package/src/tools/output-meta.ts +202 -37
  143. package/src/tools/path-utils.ts +125 -2
  144. package/src/tools/read.ts +548 -237
  145. package/src/tools/render-utils.ts +92 -0
  146. package/src/tools/renderers.ts +2 -0
  147. package/src/tools/resolve.ts +72 -44
  148. package/src/tools/search.ts +120 -186
  149. package/src/tools/ssh.ts +3 -2
  150. package/src/tools/write.ts +64 -9
  151. package/src/utils/file-mentions.ts +1 -1
  152. package/src/utils/image-loading.ts +7 -3
  153. package/src/utils/image-resize.ts +32 -43
  154. package/src/vim/parser.ts +0 -17
  155. package/src/vim/render.ts +1 -1
  156. package/src/vim/types.ts +1 -1
  157. package/src/web/search/providers/anthropic.ts +5 -0
  158. package/src/web/search/providers/exa.ts +3 -0
  159. package/src/web/search/providers/gemini.ts +40 -95
  160. package/src/web/search/providers/jina.ts +5 -2
  161. package/src/web/search/providers/zai.ts +5 -2
  162. package/src/prompts/tools/exit-plan-mode.md +0 -6
  163. package/src/tools/exit-plan-mode.ts +0 -97
  164. package/src/utils/fuzzy.ts +0 -108
  165. package/src/utils/image-convert.ts +0 -27
@@ -0,0 +1,237 @@
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
+ import type { Component } from "@oh-my-pi/pi-tui";
3
+ import { Text } from "@oh-my-pi/pi-tui";
4
+ import { formatNumber, prompt } from "@oh-my-pi/pi-utils";
5
+ import { type Static, Type } from "@sinclair/typebox";
6
+ import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
7
+ import type { Theme, ThemeColor } from "../../modes/theme/theme";
8
+ import goalDescription from "../../prompts/tools/goal.md" with { type: "text" };
9
+ import { formatDuration } from "../../slash-commands/helpers/format";
10
+ import type { ToolSession } from "../../tools";
11
+ import { formatErrorMessage, TRUNCATE_LENGTHS } from "../../tools/render-utils";
12
+ import { ToolError } from "../../tools/tool-errors";
13
+ import { renderStatusLine, truncateToWidth } from "../../tui";
14
+ import { completionBudgetReport, remainingTokens } from "../runtime";
15
+ import type { Goal, GoalStatus, GoalToolDetails } from "../state";
16
+
17
+ const goalSchema = Type.Object({
18
+ op: Type.Union([Type.Literal("create"), Type.Literal("get"), Type.Literal("complete")], {
19
+ description: "Goal operation.",
20
+ }),
21
+ objective: Type.Optional(Type.String({ description: "Goal objective. Required when op=create." })),
22
+ token_budget: Type.Optional(
23
+ Type.Integer({
24
+ description: "Optional positive token budget. Only honored when op=create.",
25
+ }),
26
+ ),
27
+ });
28
+
29
+ export type GoalToolInput = Static<typeof goalSchema>;
30
+
31
+ export interface GoalToolResponse {
32
+ goal: Goal | null;
33
+ remainingTokens: number | null;
34
+ completionBudgetReport: string | null;
35
+ }
36
+
37
+ export function buildGoalToolResponse(
38
+ goal: Goal | null | undefined,
39
+ options?: { includeCompletionReport?: boolean },
40
+ ): GoalToolResponse {
41
+ const resolvedGoal = goal ?? null;
42
+ return {
43
+ goal: resolvedGoal,
44
+ remainingTokens: remainingTokens(resolvedGoal),
45
+ completionBudgetReport:
46
+ options?.includeCompletionReport && resolvedGoal?.status === "complete"
47
+ ? completionBudgetReport(resolvedGoal)
48
+ : null,
49
+ };
50
+ }
51
+
52
+ function validateCreateParams(params: GoalToolInput): { objective: string; tokenBudget?: number } {
53
+ const objective = params.objective?.trim();
54
+ if (!objective) {
55
+ throw new ToolError("objective is required when op=create");
56
+ }
57
+ const tokenBudget = params.token_budget;
58
+ if (tokenBudget !== undefined && (!Number.isInteger(tokenBudget) || tokenBudget <= 0)) {
59
+ throw new ToolError("token_budget must be a positive integer when provided");
60
+ }
61
+ return { objective, tokenBudget };
62
+ }
63
+
64
+ export class GoalTool implements AgentTool<typeof goalSchema, GoalToolDetails> {
65
+ readonly name = "goal";
66
+ readonly label = "Goal";
67
+ readonly description = prompt.render(goalDescription);
68
+ readonly parameters = goalSchema;
69
+ readonly strict = true;
70
+ readonly intent = "omit" as const;
71
+ readonly #session: ToolSession;
72
+
73
+ constructor(session: ToolSession) {
74
+ this.#session = session;
75
+ }
76
+
77
+ async execute(
78
+ _toolCallId: string,
79
+ params: GoalToolInput,
80
+ _signal?: AbortSignal,
81
+ _onUpdate?: AgentToolUpdateCallback<GoalToolDetails>,
82
+ _context?: AgentToolContext,
83
+ ): Promise<AgentToolResult<GoalToolDetails>> {
84
+ const runtime = this.#session.getGoalRuntime?.();
85
+ if (!runtime) {
86
+ throw new ToolError("Goal mode is not active.");
87
+ }
88
+
89
+ let response: GoalToolResponse;
90
+ if (params.op === "create") {
91
+ const created = await runtime.createGoal(validateCreateParams(params));
92
+ response = buildGoalToolResponse(created.goal);
93
+ } else if (params.op === "get") {
94
+ const state = this.#session.getGoalModeState?.();
95
+ response = buildGoalToolResponse(state?.enabled ? state.goal : null);
96
+ } else {
97
+ const completed = await runtime.completeGoalFromTool();
98
+ response = buildGoalToolResponse(completed, { includeCompletionReport: true });
99
+ }
100
+ let text: string;
101
+ if (response.goal) {
102
+ text = `Goal: ${response.goal.objective}\nStatus: ${response.goal.status}\nTokens: ${response.goal.tokensUsed} used`;
103
+ if (response.goal.tokenBudget !== undefined) {
104
+ text += ` / ${response.goal.tokenBudget} budget`;
105
+ }
106
+ if (response.remainingTokens !== null) {
107
+ text += `\nRemaining tokens: ${response.remainingTokens}`;
108
+ }
109
+ if (response.completionBudgetReport) {
110
+ text += `\n\n${response.completionBudgetReport}`;
111
+ }
112
+ } else {
113
+ text = "No active goal.";
114
+ }
115
+ return {
116
+ content: [{ type: "text", text }],
117
+ details: {
118
+ op: params.op,
119
+ goal: response.goal,
120
+ remainingTokens: response.remainingTokens,
121
+ completionBudgetReport: response.completionBudgetReport,
122
+ },
123
+ };
124
+ }
125
+ }
126
+
127
+ function describeOp(op: string | undefined): string {
128
+ switch (op) {
129
+ case "create":
130
+ return "set";
131
+ case "complete":
132
+ return "complete";
133
+ case "get":
134
+ return "check";
135
+ default:
136
+ return op ?? "?";
137
+ }
138
+ }
139
+
140
+ function goalBadgeColor(status: GoalStatus): ThemeColor {
141
+ switch (status) {
142
+ case "complete":
143
+ return "success";
144
+ case "budget-limited":
145
+ return "warning";
146
+ case "paused":
147
+ case "dropped":
148
+ return "muted";
149
+ default:
150
+ return "accent";
151
+ }
152
+ }
153
+
154
+ interface GoalRenderArgs {
155
+ op?: GoalToolInput["op"];
156
+ objective?: string;
157
+ token_budget?: number;
158
+ }
159
+
160
+ export const goalToolRenderer = {
161
+ renderCall(args: GoalRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
162
+ const description = describeOp(args.op);
163
+ const meta: string[] = [];
164
+ const trimmedObjective = args.objective?.trim();
165
+ if (args.op === "create" && trimmedObjective) {
166
+ const objective = truncateToWidth(trimmedObjective, TRUNCATE_LENGTHS.TITLE);
167
+ meta.push(uiTheme.italic(uiTheme.fg("muted", `"${objective}"`)));
168
+ }
169
+ if (args.op === "create" && args.token_budget !== undefined) {
170
+ meta.push(`budget ${formatNumber(args.token_budget)}`);
171
+ }
172
+ const text = renderStatusLine({ icon: "pending", title: "Goal", description, meta }, uiTheme);
173
+ return new Text(text, 0, 0);
174
+ },
175
+
176
+ renderResult(
177
+ result: { content: Array<{ type: string; text?: string }>; details?: GoalToolDetails; isError?: boolean },
178
+ _options: RenderResultOptions,
179
+ uiTheme: Theme,
180
+ args?: GoalRenderArgs,
181
+ ): Component {
182
+ const fallbackText = result.content?.find(c => c.type === "text")?.text ?? "";
183
+ const details = result.details;
184
+ const op = details?.op ?? args?.op;
185
+ const description = describeOp(op);
186
+
187
+ if (result.isError) {
188
+ const header = renderStatusLine({ icon: "error", title: "Goal", description }, uiTheme);
189
+ const body = formatErrorMessage(fallbackText || "Goal tool failed", uiTheme);
190
+ return new Text([header, body].join("\n"), 0, 0);
191
+ }
192
+
193
+ const goal = details?.goal ?? null;
194
+ if (!goal) {
195
+ const header = renderStatusLine({ icon: "warning", title: "Goal", description }, uiTheme);
196
+ const body = uiTheme.fg("muted", "No active goal.");
197
+ return new Text([header, body].join("\n"), 0, 0);
198
+ }
199
+
200
+ const lines: string[] = [];
201
+ lines.push(
202
+ renderStatusLine(
203
+ {
204
+ icon: "success",
205
+ title: "Goal",
206
+ description,
207
+ badge: { label: goal.status, color: goalBadgeColor(goal.status) },
208
+ },
209
+ uiTheme,
210
+ ),
211
+ );
212
+
213
+ const objectiveText = truncateToWidth(goal.objective.trim(), TRUNCATE_LENGTHS.LONG);
214
+ lines.push(` ${uiTheme.italic(uiTheme.fg("muted", `"${objectiveText}"`))}`);
215
+
216
+ const used = formatNumber(goal.tokensUsed);
217
+ const tokensLine =
218
+ goal.tokenBudget !== undefined
219
+ ? `${used} / ${formatNumber(goal.tokenBudget)} tokens (${formatNumber(Math.max(0, goal.tokenBudget - goal.tokensUsed))} left)`
220
+ : `${used} tokens`;
221
+ lines.push(` ${uiTheme.fg("dim", tokensLine)}`);
222
+
223
+ if (goal.timeUsedSeconds > 0) {
224
+ lines.push(` ${uiTheme.fg("dim", `${formatDuration(goal.timeUsedSeconds * 1000)} elapsed`)}`);
225
+ }
226
+
227
+ const report = details?.completionBudgetReport;
228
+ if (report) {
229
+ lines.push("");
230
+ lines.push(uiTheme.italic(uiTheme.fg("muted", report)));
231
+ }
232
+
233
+ return new Text(lines.join("\n"), 0, 0);
234
+ },
235
+
236
+ mergeCallAndResult: true,
237
+ };
@@ -63,9 +63,9 @@ export class HashlineMismatchError extends Error {
63
63
  }
64
64
 
65
65
  private static rejectionHeader(mismatches: HashMismatch[]): string[] {
66
- const noun = mismatches.length > 1 ? "lines have" : "line has";
66
+ const noun = mismatches.length > 1 ? "anchors do" : "anchor does";
67
67
  return [
68
- `Edit rejected: ${mismatches.length} ${noun} changed since the last read (marked *).`,
68
+ `Edit rejected: ${mismatches.length} ${noun} not match the current file (marked *).`,
69
69
  "The edit was NOT applied, please use the updated file content shown below, and issue another edit tool-call.",
70
70
  ];
71
71
  }
@@ -108,7 +108,8 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
108
108
 
109
109
  const flush = () => {
110
110
  if (currentPath.length === 0) return;
111
- sections.push({ path: currentPath, diff: currentLines.join("\n") });
111
+ const hasOps = currentLines.some(rawLine => stripTrailingCarriageReturn(rawLine).trim().length > 0);
112
+ if (hasOps) sections.push({ path: currentPath, diff: currentLines.join("\n") });
112
113
  currentLines = [];
113
114
  };
114
115
 
@@ -86,9 +86,33 @@ function collectPayload(
86
86
  let index = startIndex;
87
87
  while (index < lines.length) {
88
88
  const line = stripTrailingCarriageReturn(lines[index]);
89
- if (!line.startsWith(HL_EDIT_SEP)) break;
90
- payload.push(line.slice(1));
91
- index++;
89
+ if (line.startsWith(HL_EDIT_SEP)) {
90
+ payload.push(line.slice(1));
91
+ index++;
92
+ continue;
93
+ }
94
+ // Silently recover from a missing payload prefix on an otherwise blank
95
+ // line: if more payload follows (possibly past further blanks), treat
96
+ // each intervening blank as an empty `${HL_EDIT_SEP}` payload line.
97
+ // Additionally, when the op explicitly requires payload (`+`/`<`) and
98
+ // we have not collected any yet, accept the blank(s) themselves as the
99
+ // empty payload — common typo of forgetting the `${HL_EDIT_SEP}` prefix
100
+ // when inserting a blank line.
101
+ if (line.length === 0) {
102
+ let lookahead = index + 1;
103
+ while (lookahead < lines.length && stripTrailingCarriageReturn(lines[lookahead]).length === 0) {
104
+ lookahead++;
105
+ }
106
+ const followedByPayload =
107
+ lookahead < lines.length && stripTrailingCarriageReturn(lines[lookahead]).startsWith(HL_EDIT_SEP);
108
+ const acceptBareBlank = requirePayload && payload.length === 0;
109
+ if (followedByPayload || acceptBareBlank) {
110
+ for (let j = index; j < lookahead; j++) payload.push("");
111
+ index = lookahead;
112
+ continue;
113
+ }
114
+ }
115
+ break;
92
116
  }
93
117
  if (payload.length === 0 && requirePayload) {
94
118
  throw new Error(`line ${opLineNum}: + and < operations require at least one ${HL_EDIT_SEP}TEXT payload line.`);
@@ -379,4 +379,4 @@ export const MENTAL_MODEL_FIRST_TURN_DEADLINE_MS = 1500;
379
379
  export const MENTAL_MODEL_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
380
380
 
381
381
  /** Need-only export of the raw seed list for tests. */
382
- export const __builtinSeedsForTest: ReadonlyArray<Readonly<RawSeed>> = BUILTIN_SEEDS;
382
+ export const builtinSeedsForTest: ReadonlyArray<Readonly<RawSeed>> = BUILTIN_SEEDS;
@@ -14,29 +14,10 @@
14
14
  import * as fs from "node:fs/promises";
15
15
  import * as path from "node:path";
16
16
  import { isEnoent } from "@oh-my-pi/pi-utils";
17
- import { AgentRegistry } from "../registry/agent-registry";
18
17
  import { applyQuery, pathToQuery } from "./json-query";
18
+ import { artifactsDirsFromRegistry } from "./registry-helpers";
19
19
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
20
20
 
21
- /**
22
- * Snapshot of artifacts dirs for every registered session, deduped.
23
- *
24
- * Prefers `sessionManager.getArtifactsDir()` because subagents adopt the
25
- * parent's manager and report the parent's dir there; dedup then collapses
26
- * the whole agent tree to one entry. Falls back to the raw session file
27
- * when no live session reference is attached.
28
- */
29
- function artifactsDirsFromRegistry(): string[] {
30
- const dirs: string[] = [];
31
- for (const ref of AgentRegistry.global().list()) {
32
- const dir =
33
- ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
34
- if (!dir) continue;
35
- if (!dirs.includes(dir)) dirs.push(dir);
36
- }
37
- return dirs;
38
- }
39
-
40
21
  /**
41
22
  * Handler for agent:// URLs.
42
23
  *
@@ -12,27 +12,9 @@
12
12
  import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
  import { isEnoent } from "@oh-my-pi/pi-utils";
15
- import { AgentRegistry } from "../registry/agent-registry";
15
+ import { artifactsDirsFromRegistry } from "./registry-helpers";
16
16
  import type { InternalResource, InternalUrl, ProtocolHandler } from "./types";
17
17
 
18
- /**
19
- * Snapshot of artifacts dirs across all registered sessions, deduped.
20
- *
21
- * Subagents adopt their parent's `ArtifactManager`, so their
22
- * `sessionManager.getArtifactsDir()` returns the parent's dir; dedup
23
- * collapses parent + N subagents to a single entry.
24
- */
25
- function artifactsDirsFromRegistry(): string[] {
26
- const dirs: string[] = [];
27
- for (const ref of AgentRegistry.global().list()) {
28
- const dir =
29
- ref.session?.sessionManager.getArtifactsDir() ?? (ref.sessionFile ? ref.sessionFile.slice(0, -6) : null);
30
- if (!dir) continue;
31
- if (!dirs.includes(dir)) dirs.push(dir);
32
- }
33
- return dirs;
34
- }
35
-
36
18
  export class ArtifactProtocolHandler implements ProtocolHandler {
37
19
  readonly scheme = "artifact";
38
20
  readonly immutable = true;