@oh-my-pi/pi-coding-agent 16.2.9 → 16.2.12

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 (110) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3464 -3306
  3. package/dist/types/cli/bench-cli.d.ts +0 -5
  4. package/dist/types/cli/dry-balance-cli.d.ts +0 -5
  5. package/dist/types/cli/models-cli.d.ts +1 -1
  6. package/dist/types/config/inline-tool-descriptors-mode.d.ts +1 -2
  7. package/dist/types/config/model-registry.d.ts +1 -23
  8. package/dist/types/config/model-resolver.d.ts +10 -15
  9. package/dist/types/config/models-config-schema.d.ts +0 -6
  10. package/dist/types/config/models-config.d.ts +0 -6
  11. package/dist/types/config/settings-schema.d.ts +31 -1
  12. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +1 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +15 -0
  14. package/dist/types/extensibility/skills.d.ts +29 -0
  15. package/dist/types/lsp/client.d.ts +9 -3
  16. package/dist/types/modes/components/todo-reminder.d.ts +3 -1
  17. package/dist/types/modes/interactive-mode.d.ts +0 -1
  18. package/dist/types/modes/skill-command.d.ts +1 -1
  19. package/dist/types/modes/types.d.ts +0 -1
  20. package/dist/types/stt/asr-client.d.ts +7 -3
  21. package/dist/types/task/spawn-policy.d.ts +17 -0
  22. package/dist/types/task/spawn-policy.test.d.ts +1 -0
  23. package/dist/types/task/types.d.ts +8 -2
  24. package/dist/types/tools/__tests__/eval-description.test.d.ts +1 -0
  25. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +2 -2
  26. package/dist/types/tools/browser/registry.d.ts +2 -0
  27. package/dist/types/tools/browser/run-cancellation.d.ts +4 -0
  28. package/dist/types/tools/browser/tab-supervisor.d.ts +30 -1
  29. package/dist/types/tools/eval.d.ts +3 -6
  30. package/dist/types/tools/write.d.ts +1 -1
  31. package/dist/types/utils/image-vision-fallback.d.ts +1 -1
  32. package/package.json +14 -13
  33. package/scripts/bundle-dist.ts +23 -4
  34. package/scripts/generate-docs-index.ts +116 -24
  35. package/src/advisor/__tests__/advisor.test.ts +1 -1
  36. package/src/async/job-manager.ts +27 -3
  37. package/src/auto-thinking/classifier.ts +1 -1
  38. package/src/cli/bench-cli.ts +3 -12
  39. package/src/cli/dry-balance-cli.ts +1 -6
  40. package/src/cli/grep-cli.ts +1 -1
  41. package/src/cli/models-cli.ts +3 -81
  42. package/src/commands/models.ts +1 -2
  43. package/src/commit/model-selection.ts +4 -4
  44. package/src/config/inline-tool-descriptors-mode.ts +1 -2
  45. package/src/config/model-discovery.ts +150 -82
  46. package/src/config/model-registry.ts +11 -225
  47. package/src/config/model-resolver.ts +23 -135
  48. package/src/config/models-config-schema.ts +0 -22
  49. package/src/config/prompt-templates.ts +20 -0
  50. package/src/config/settings-schema.ts +35 -1
  51. package/src/config/settings.ts +60 -24
  52. package/src/eval/__tests__/agent-bridge.test.ts +17 -3
  53. package/src/eval/agent-bridge.ts +7 -9
  54. package/src/eval/completion-bridge.ts +1 -1
  55. package/src/eval/py/__tests__/runner-shell-output.test.ts +157 -0
  56. package/src/eval/py/runner.py +169 -18
  57. package/src/extensibility/extensions/model-api.ts +1 -3
  58. package/src/extensibility/extensions/runner.ts +62 -5
  59. package/src/extensibility/skills.ts +77 -0
  60. package/src/internal-urls/docs-index.generated.txt +2 -2
  61. package/src/lsp/client.ts +162 -45
  62. package/src/lsp/config.ts +17 -3
  63. package/src/lsp/index.ts +31 -21
  64. package/src/main.ts +0 -1
  65. package/src/mcp/transports/http.ts +19 -17
  66. package/src/mcp/transports/stdio.test.ts +51 -1
  67. package/src/mcp/transports/stdio.ts +19 -9
  68. package/src/memories/index.ts +0 -1
  69. package/src/mnemopi/backend.ts +1 -1
  70. package/src/modes/acp/acp-agent.ts +6 -9
  71. package/src/modes/components/model-selector.ts +32 -186
  72. package/src/modes/components/todo-reminder.ts +5 -1
  73. package/src/modes/controllers/event-controller.ts +1 -16
  74. package/src/modes/controllers/selector-controller.ts +57 -36
  75. package/src/modes/interactive-mode.ts +0 -6
  76. package/src/modes/rpc/rpc-mode.ts +5 -8
  77. package/src/modes/skill-command.ts +8 -20
  78. package/src/modes/types.ts +0 -1
  79. package/src/prompts/system/subagent-system-prompt.md +2 -2
  80. package/src/prompts/system/tool-call-loop-redirect.md +8 -0
  81. package/src/prompts/tools/eval.md +2 -2
  82. package/src/prompts/tools/task.md +1 -1
  83. package/src/sdk.ts +2 -9
  84. package/src/session/agent-session.ts +89 -8
  85. package/src/session/session-context.ts +2 -1
  86. package/src/session/session-manager.ts +2 -1
  87. package/src/session/unexpected-stop-classifier.ts +1 -1
  88. package/src/stt/asr-client.ts +87 -27
  89. package/src/stt/downloader.ts +8 -2
  90. package/src/task/executor.ts +1 -3
  91. package/src/task/index.ts +37 -32
  92. package/src/task/spawn-policy.test.ts +63 -0
  93. package/src/task/spawn-policy.ts +58 -0
  94. package/src/task/types.ts +77 -6
  95. package/src/tools/__tests__/eval-description.test.ts +19 -0
  96. package/src/tools/ast-grep.ts +34 -12
  97. package/src/tools/browser/cmux/cmux-tab.ts +66 -24
  98. package/src/tools/browser/registry.ts +25 -0
  99. package/src/tools/browser/run-cancellation.ts +47 -0
  100. package/src/tools/browser/tab-supervisor.ts +120 -7
  101. package/src/tools/browser/tab-worker.ts +22 -10
  102. package/src/tools/browser.ts +1 -0
  103. package/src/tools/eval.ts +15 -10
  104. package/src/tools/grep.ts +11 -8
  105. package/src/tools/inspect-image.ts +1 -1
  106. package/src/tools/write.ts +49 -9
  107. package/src/utils/commit-message-generator.ts +0 -1
  108. package/src/utils/git.ts +22 -1
  109. package/src/utils/image-vision-fallback.ts +2 -3
  110. package/src/utils/title-generator.ts +1 -1
@@ -36,6 +36,7 @@ import {
36
36
  loadPuppeteerInWorker,
37
37
  } from "./launch";
38
38
  import { extractReadableFromHtml, type ReadableFormat } from "./readable";
39
+ import { waitForBrowserRun } from "./run-cancellation";
39
40
  import type {
40
41
  Observation,
41
42
  ObservationEntry,
@@ -481,13 +482,13 @@ async function clickQueryHandlerText(
481
482
  const target = await resolveActionableQueryHandlerClickTarget(handles);
482
483
  if (!target) {
483
484
  lastReason = handles.length ? "no-visible-candidate" : "no-matches";
484
- await Bun.sleep(100);
485
+ await untilAborted(clickSignal, () => Bun.sleep(100));
485
486
  continue;
486
487
  }
487
488
  const actionability = await isClickActionable(target);
488
489
  if (!actionability.ok) {
489
490
  lastReason = actionability.reason;
490
- await Bun.sleep(100);
491
+ await untilAborted(clickSignal, () => Bun.sleep(100));
491
492
  continue;
492
493
  }
493
494
  try {
@@ -495,7 +496,7 @@ async function clickQueryHandlerText(
495
496
  return;
496
497
  } catch (err) {
497
498
  lastReason = err instanceof Error ? err.message : String(err);
498
- await Bun.sleep(100);
499
+ await untilAborted(clickSignal, () => Bun.sleep(100));
499
500
  }
500
501
  } finally {
501
502
  await Promise.all(handles.map(async handle => handle.dispose().catch(() => undefined)));
@@ -515,6 +516,7 @@ export interface InflightOp {
515
516
  interface ActiveRun {
516
517
  id: string;
517
518
  ac: AbortController;
519
+ signal: AbortSignal;
518
520
  displays: RunResultOk["displays"];
519
521
  screenshots: ScreenshotResult[];
520
522
  pendingTools: Map<string, { resolve(value: unknown): void; reject(error: Error): void }>;
@@ -697,12 +699,14 @@ export class WorkerCore {
697
699
  }
698
700
  const timeoutSignal = AbortSignal.timeout(msg.timeoutMs);
699
701
  const ac = new AbortController();
700
- const signal = AbortSignal.any([timeoutSignal, ac.signal]);
702
+ const runAc = new AbortController();
703
+ const signal = AbortSignal.any([timeoutSignal, ac.signal, runAc.signal]);
701
704
  const displays: RunResultOk["displays"] = [];
702
705
  const screenshots: ScreenshotResult[] = [];
703
706
  const active: ActiveRun = {
704
707
  id: msg.id,
705
708
  ac,
709
+ signal,
706
710
  displays,
707
711
  screenshots,
708
712
  pendingTools: new Map(),
@@ -724,7 +728,7 @@ export class WorkerCore {
724
728
  assert: (cond: unknown, text?: string): void => {
725
729
  if (!cond) throw new ToolError(text ?? "Assertion failed");
726
730
  },
727
- wait: (ms: number): Promise<void> => Bun.sleep(ms),
731
+ wait: (ms: number): Promise<void> => waitForBrowserRun(ms, signal),
728
732
  });
729
733
  const { promise: cancelRejection, reject: rejectCancel } = Promise.withResolvers<never>();
730
734
  const onCancel = (): void => {
@@ -767,6 +771,7 @@ export class WorkerCore {
767
771
  this.#transport.send({ type: "result", id: msg.id, ok: false, error: errorPayload(error) });
768
772
  } finally {
769
773
  if (this.#active?.id === msg.id) this.#active = null;
774
+ runAc.abort(new ToolAbortError("Browser run ended"));
770
775
  }
771
776
  }
772
777
 
@@ -783,11 +788,18 @@ export class WorkerCore {
783
788
  const active = this.#active;
784
789
  if (!active) return null;
785
790
  return {
786
- // console.* output stays on the supervisor log channel — matches pre-runtime behavior
787
- // where browser cells didn't surface `console.log` to the model.
788
- onText: chunk => this.#log("debug", chunk.replace(/\n$/, "")),
789
- onDisplay: output => this.#pushDisplay(active.displays, output),
790
- callTool: (name, args) => this.#callTool(active, name, args),
791
+ onText: chunk => {
792
+ throwIfAborted(active.signal);
793
+ this.#log("debug", chunk.replace(/\n$/, ""));
794
+ },
795
+ onDisplay: output => {
796
+ throwIfAborted(active.signal);
797
+ this.#pushDisplay(active.displays, output);
798
+ },
799
+ callTool: (name, args) => {
800
+ throwIfAborted(active.signal);
801
+ return this.#callTool(active, name, args);
802
+ },
791
803
  };
792
804
  }
793
805
 
@@ -262,6 +262,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
262
262
  timeoutMs,
263
263
  dialogs: params.dialogs,
264
264
  signal,
265
+ ownerSessionId: this.session.getSessionId?.() ?? undefined,
265
266
  }),
266
267
  );
267
268
  const tab = result.tab;
package/src/tools/eval.ts CHANGED
@@ -10,6 +10,7 @@ import { defaultEvalSessionId } from "../eval/session-id";
10
10
  import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
11
11
  import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
12
12
  import { DEFAULT_MAX_BYTES, OutputSink, type OutputSummary, TailBuffer } from "../session/streaming-output";
13
+ import { resolveSpawnPolicy } from "../task/spawn-policy";
13
14
  import { webpExclusionForModel } from "../utils/image-loading";
14
15
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
15
16
  import type { ToolSession } from ".";
@@ -164,13 +165,10 @@ export interface EvalToolDescriptionOptions {
164
165
  rb?: boolean;
165
166
  jl?: boolean;
166
167
  /**
167
- * Whether `agent()` is allowed in this session. Driven by the parent's
168
- * spawn policy (`getSessionSpawns`). Defaults to `true` for backward
169
- * compatibility — when the session forbids spawning, the prelude doc
170
- * omits the `agent()` entry so the model does not promise itself a
171
- * helper that will only ever throw "spawns disabled".
168
+ * Parent spawn policy (`getSessionSpawns`). `true`/omitted means unrestricted,
169
+ * `false`/`""` hides `agent()`, and a comma list drives the advertised default.
172
170
  */
173
- spawns?: boolean;
171
+ spawns?: boolean | string | null;
174
172
  }
175
173
 
176
174
  export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
@@ -178,8 +176,16 @@ export function getEvalToolDescription(options: EvalToolDescriptionOptions = {})
178
176
  const js = options.js ?? true;
179
177
  const rb = options.rb ?? false;
180
178
  const jl = options.jl ?? false;
181
- const spawns = options.spawns ?? true;
182
- return prompt.render(evalDescription, { py, js, rb, jl, spawns });
179
+ const spawnPolicy = resolveSpawnPolicy(options.spawns ?? true);
180
+ return prompt.render(evalDescription, {
181
+ py,
182
+ js,
183
+ rb,
184
+ jl,
185
+ spawns: spawnPolicy.enabled,
186
+ spawnDefaultAgent: spawnPolicy.defaultAgent,
187
+ spawnAllowedAgentsText: spawnPolicy.allowedPromptText,
188
+ });
183
189
  }
184
190
 
185
191
  export interface EvalToolOptions {
@@ -294,13 +300,12 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
294
300
  if (!this.session) return getEvalToolDescription();
295
301
  const backends = resolveEvalBackends(this.session);
296
302
  const sessionSpawns = this.session.getSessionSpawns?.() ?? "*";
297
- const spawnsAllowed = sessionSpawns !== "" && sessionSpawns !== null;
298
303
  return getEvalToolDescription({
299
304
  py: backends.python,
300
305
  js: backends.js,
301
306
  rb: backends.ruby,
302
307
  jl: backends.julia,
303
- spawns: spawnsAllowed,
308
+ spawns: sessionSpawns,
304
309
  });
305
310
  }
306
311
  /** All reuse-chain examples; the `examples` getter filters by enabled languages. */
package/src/tools/grep.ts CHANGED
@@ -122,8 +122,10 @@ export const SINGLE_FILE_MATCHES = 200;
122
122
  * pagination headroom so the caller can see total file count. */
123
123
  const INTERNAL_TOTAL_CAP = 2000;
124
124
  /** Mirrors `MAX_FILE_BYTES` in `crates/pi-natives/src/grep.rs`. Native grep
125
- * silently returns no matches for files larger than this; surface a warning
126
- * when the caller explicitly targeted such a file so they know to chunk it. */
125
+ * searches only the first `MAX_FILE_BYTES` of a larger file (a leading mmap
126
+ * window) and drops the rest; matches beyond the window are not returned. We
127
+ * surface a partial-coverage note when the caller explicitly targeted such a
128
+ * file so they know matches past the window are not shown. */
127
129
  const NATIVE_GREP_MAX_FILE_BYTES = 4 * 1024 * 1024;
128
130
  /** Wall-clock budget for a single native grep invocation. Without it, an
129
131
  * aborted or runaway search (huge tree, network mount) keeps burning CPU on
@@ -1260,8 +1262,9 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
1260
1262
  const { record: recordFile, list: fileList } = createFileRecorder();
1261
1263
  const fileMatchCounts = new Map<string, number>();
1262
1264
  // Detect explicit file targets that exceed the native grep size cap.
1263
- // Native silently returns no matches above the cap; without this note the
1264
- // caller sees "no matches" for a literal pattern that visibly exists.
1265
+ // Native searches only their first NATIVE_GREP_MAX_FILE_BYTES; without
1266
+ // this note the caller might miss that matches beyond the window
1267
+ // (or "no matches") reflect partial coverage, not the whole file.
1265
1268
  const oversizedNote = await (async (): Promise<string | undefined> => {
1266
1269
  const explicitFileTargets: string[] = [];
1267
1270
  if (exactFilePaths) {
@@ -1285,13 +1288,13 @@ export class GrepTool implements AgentTool<typeof searchSchema, GrepToolDetails>
1285
1288
  );
1286
1289
  if (oversized.length === 0) return undefined;
1287
1290
  const limitMb = Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024));
1288
- return `Skipped oversized files (>${limitMb}MB grep limit; split the file or narrow with \`read\`): ${oversized.join(", ")}`;
1291
+ return `Searched only the first ${limitMb}MB of large files (matches past the ${limitMb}MB window are not shown; use \`read\` for the rest): ${oversized.join(", ")}`;
1289
1292
  })();
1290
- // Directory/multi-target scopes: native grep counts oversized skips but
1291
- // cannot name them; explicit-file scopes are covered (with names) above.
1293
+ // Directory/multi-target scopes: native counts files it could not map
1294
+ // even a prefix of (rare mmap failures), but cannot name them.
1292
1295
  const oversizedScanNote =
1293
1296
  !oversizedNote && skippedOversizedCount > 0
1294
- ? `Skipped ${skippedOversizedCount} oversized file(s) (>${Math.floor(NATIVE_GREP_MAX_FILE_BYTES / (1024 * 1024))}MB grep limit); target them directly with \`read\``
1297
+ ? `Skipped ${skippedOversizedCount} unreadable large file(s); target them directly with \`read\``
1295
1298
  : undefined;
1296
1299
  const archiveNote =
1297
1300
  archiveUnreadable.length > 0
@@ -152,7 +152,7 @@ export class InspectImageTool implements AgentTool<typeof inspectImageSchema, In
152
152
  const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
153
153
  if (!pattern) return undefined;
154
154
  const expanded = expandRoleAlias(pattern, this.session.settings);
155
- return resolveModelFromString(expanded, availableModels, matchPreferences, modelRegistry);
155
+ return resolveModelFromString(expanded, availableModels, matchPreferences);
156
156
  };
157
157
 
158
158
  const activeModelPattern = this.session.getActiveModelString?.() ?? this.session.getModelString?.();
@@ -52,6 +52,7 @@ import { enforcePlanModeWrite, resolvePlanPath, unwrapHashlineHeaderPath } from
52
52
  import {
53
53
  cachedRenderedString,
54
54
  createRenderedStringCache,
55
+ Ellipsis,
55
56
  formatDiagnostics,
56
57
  formatErrorDetail,
57
58
  formatExpandHint,
@@ -61,6 +62,8 @@ import {
61
62
  type RenderedStringCache,
62
63
  replaceTabs,
63
64
  shortenPath,
65
+ TRUNCATE_LENGTHS,
66
+ truncateToWidth,
64
67
  } from "./render-utils";
65
68
  import {
66
69
  deleteRowByKey,
@@ -165,6 +168,18 @@ function appendNoteToResult(result: AgentToolResult<WriteToolDetails>, note: str
165
168
  }
166
169
  }
167
170
 
171
+ function emitWriteProgress(
172
+ onUpdate: AgentToolUpdateCallback<WriteToolDetails> | undefined,
173
+ content: string,
174
+ displayPath: string,
175
+ resolvedPath?: string,
176
+ ): void {
177
+ onUpdate?.({
178
+ content: [{ type: "text", text: `Writing ${content.length} bytes to ${shortenPath(displayPath)}...` }],
179
+ details: resolvedPath ? { resolvedPath } : {},
180
+ });
181
+ }
182
+
168
183
  /**
169
184
  * If `content` begins with a `#!` shebang, ensure the file is executable.
170
185
  *
@@ -781,7 +796,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
781
796
  _toolCallId: string,
782
797
  { path: rawPath, content }: WriteParams,
783
798
  signal?: AbortSignal,
784
- _onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
799
+ onUpdate?: AgentToolUpdateCallback<WriteToolDetails>,
785
800
  context?: AgentToolContext,
786
801
  ): Promise<AgentToolResult<WriteToolDetails>> {
787
802
  // Strip a hashline `[path#TAG]` wrapper up front so every downstream
@@ -807,6 +822,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
807
822
  // Handler-owned writes (vault:// notes, host URIs) mutate user
808
823
  // data outside the local sandbox — plan mode must reject them.
809
824
  enforcePlanModeWrite(this.session, path, { op: "update" });
825
+ emitWriteProgress(onUpdate, cleanContent, path);
810
826
  await handler.write(parsed, cleanContent, { cwd: this.session.cwd, signal });
811
827
  let resultText = `Successfully wrote ${cleanContent.length} bytes to ${path}`;
812
828
  if (stripped) {
@@ -826,6 +842,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
826
842
  `Conflict URI scope '/${conflictUri.scope}' is read-only — read \`conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`,
827
843
  );
828
844
  }
845
+ emitWriteProgress(onUpdate, cleanContent, path);
829
846
  const result =
830
847
  conflictUri.id === "*"
831
848
  ? await this.#resolveAllConflicts(cleanContent, stripped, signal)
@@ -844,6 +861,14 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
844
861
  op: resolvedArchivePath.exists ? "update" : "create",
845
862
  });
846
863
 
864
+ emitWriteProgress(
865
+ onUpdate,
866
+ cleanContent,
867
+ `${formatPathRelativeToCwd(resolvedArchivePath.absolutePath, this.session.cwd)}:${
868
+ resolvedArchivePath.archiveSubPath
869
+ }`,
870
+ resolvedArchivePath.absolutePath,
871
+ );
847
872
  const archiveResult = await this.#writeArchiveEntry(cleanContent, resolvedArchivePath);
848
873
  if (stripped) {
849
874
  const firstText = archiveResult.content.find(
@@ -861,6 +886,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
861
886
  if (resolvedSqlitePath) {
862
887
  enforcePlanModeWrite(this.session, resolvedSqlitePath.sqlitePath, { op: "update" });
863
888
 
889
+ emitWriteProgress(onUpdate, cleanContent, path, resolvedSqlitePath.absolutePath);
864
890
  const sqliteResult = await this.#writeSqliteRow(path, cleanContent, resolvedSqlitePath);
865
891
  if (stripped) {
866
892
  const firstText = sqliteResult.content.find(
@@ -883,11 +909,13 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
883
909
  await assertEditableFile(absolutePath, path);
884
910
  }
885
911
 
912
+ const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
913
+ emitWriteProgress(onUpdate, cleanContent, displayPath, absolutePath);
914
+
886
915
  // Try ACP bridge first for editor-visible filesystem paths. Internal
887
916
  // artifacts such as local:// plans are owned by OMP, not the editor.
888
917
  if (await routeWriteThroughBridge(this.session, path, absolutePath, cleanContent)) {
889
918
  const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
890
- const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
891
919
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
892
920
  const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
893
921
  let resultText = header ? `${header}\n${writeLine}` : writeLine;
@@ -908,7 +936,6 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
908
936
  this.session.bumpFileMutationVersion?.(absolutePath);
909
937
  const madeExecutable = await maybeMarkExecutableForShebang(absolutePath, cleanContent);
910
938
 
911
- const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
912
939
  const header = maybeWriteSnapshotHeader(this.session, absolutePath, cleanContent);
913
940
  const writeLine = `Successfully wrote ${cleanContent.length} bytes to ${displayPath}`;
914
941
  let resultText = header ? `${header}\n${writeLine}` : writeLine;
@@ -1128,14 +1155,19 @@ export const writeToolRenderer = {
1128
1155
  }));
1129
1156
  }
1130
1157
 
1158
+ const isPartial = options.isPartial === true;
1159
+ const progressText = result.content?.find(c => c.type === "text")?.text ?? "";
1131
1160
  const lineCount = countLines(fileContent);
1132
1161
  const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
1133
- const execSuffix = result.details?.madeExecutable
1134
- ? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
1135
- : "";
1162
+ const execSuffix =
1163
+ !isPartial && result.details?.madeExecutable
1164
+ ? `${uiTheme.fg("dim", " · ")}${uiTheme.fg("success", "made executable!")}`
1165
+ : "";
1136
1166
  const header = renderStatusLine(
1137
1167
  {
1138
- iconOverride: uiTheme.styledSymbol("tool.write", "accent"),
1168
+ icon: isPartial ? "running" : undefined,
1169
+ iconOverride: isPartial ? undefined : uiTheme.styledSymbol("tool.write", "accent"),
1170
+ spinnerFrame: options.spinnerFrame,
1139
1171
  title: "Write",
1140
1172
  description: `${langIcon} ${pathDisplay}${lineSuffix}${execSuffix}`,
1141
1173
  },
@@ -1147,7 +1179,15 @@ export const writeToolRenderer = {
1147
1179
  return framedBlock(uiTheme, width => {
1148
1180
  const { expanded } = options;
1149
1181
  let body = renderContentPreview(fileContent, expanded, lang, uiTheme, previewCache);
1150
- if (diagnostics) {
1182
+ if (isPartial && progressText) {
1183
+ const safeProgressText = truncateToWidth(
1184
+ replaceTabs(progressText),
1185
+ TRUNCATE_LENGTHS.LINE,
1186
+ Ellipsis.Unicode,
1187
+ );
1188
+ body = `${uiTheme.fg("muted", safeProgressText)}${body ? `\n${body}` : ""}`;
1189
+ }
1190
+ if (!isPartial && diagnostics) {
1151
1191
  const diagText = formatDiagnostics(diagnostics, expanded, uiTheme, fp =>
1152
1192
  uiTheme.getLangIcon(getLanguageFromPath(fp)),
1153
1193
  );
@@ -1162,7 +1202,7 @@ export const writeToolRenderer = {
1162
1202
  return {
1163
1203
  header,
1164
1204
  sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
1165
- state: "success",
1205
+ state: isPartial ? "pending" : "success",
1166
1206
  borderColor: "borderMuted",
1167
1207
  width,
1168
1208
  };
@@ -55,7 +55,6 @@ function getSmolModelCandidates(
55
55
  const configuredSmol = resolveModelRoleValue(settings.getModelRole("smol"), availableModels, {
56
56
  settings,
57
57
  matchPreferences,
58
- modelRegistry: registry,
59
58
  });
60
59
  addCandidate(configuredSmol.model, configuredSmol.thinkingLevel);
61
60
 
package/src/utils/git.ts CHANGED
@@ -175,6 +175,14 @@ const SHORT_LIVED_GIT_CONFIG: readonly (readonly [key: string, value: string])[]
175
175
  ["core.untrackedCache", "false"],
176
176
  ];
177
177
  const REMOTE_ALREADY_EXISTS = /remote .* already exists/i;
178
+ const AMBIENT_GIT_ENV = {
179
+ GIT_DIR: undefined,
180
+ GIT_COMMON_DIR: undefined,
181
+ GIT_WORK_TREE: undefined,
182
+ GIT_INDEX_FILE: undefined,
183
+ GIT_OBJECT_DIRECTORY: undefined,
184
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: undefined,
185
+ } satisfies Record<string, undefined>;
178
186
 
179
187
  interface CommandOptions {
180
188
  readonly env?: Record<string, string | undefined>;
@@ -190,6 +198,15 @@ function normalizeStdin(input: CommandOptions["stdin"]): "ignore" | Uint8Array {
190
198
  return new Uint8Array(input);
191
199
  }
192
200
 
201
+ function buildGitEnv(overrides?: Record<string, string | undefined>): Record<string, string | undefined> {
202
+ return {
203
+ ...process.env,
204
+ GIT_OPTIONAL_LOCKS: "0",
205
+ ...AMBIENT_GIT_ENV,
206
+ ...overrides,
207
+ };
208
+ }
209
+
193
210
  function ensureAvailable(): void {
194
211
  if (!$which("git")) {
195
212
  throw new Error("git is not installed.");
@@ -211,7 +228,7 @@ async function git(cwd: string, args: readonly string[], options: CommandOptions
211
228
  const commandArgs = withShortLivedGitConfig(options.readOnly ? withNoOptionalLocks(args) : [...args]);
212
229
  const child = Bun.spawn(["git", ...commandArgs], {
213
230
  cwd,
214
- env: options.env ? { ...process.env, GIT_OPTIONAL_LOCKS: "0", ...options.env } : undefined,
231
+ env: buildGitEnv(options.env),
215
232
  signal: options.signal,
216
233
  stdin: normalizeStdin(options.stdin),
217
234
  stdout: "pipe",
@@ -707,6 +724,7 @@ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState |
707
724
  const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", "HEAD"]));
708
725
  const symResult = Bun.spawnSync(["git", ...symArgs], {
709
726
  cwd: repository.repoRoot,
727
+ env: buildGitEnv(),
710
728
  stdout: "pipe",
711
729
  stderr: "pipe",
712
730
  windowsHide: true,
@@ -715,6 +733,7 @@ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState |
715
733
  const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", "HEAD"]));
716
734
  const revResult = Bun.spawnSync(["git", ...revArgs], {
717
735
  cwd: repository.repoRoot,
736
+ env: buildGitEnv(),
718
737
  stdout: "pipe",
719
738
  stderr: "pipe",
720
739
  windowsHide: true,
@@ -748,6 +767,7 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
748
767
  const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", targetRef]));
749
768
  const symResult = Bun.spawnSync(["git", ...symArgs], {
750
769
  cwd: repository.repoRoot,
770
+ env: buildGitEnv(),
751
771
  stdout: "pipe",
752
772
  stderr: "pipe",
753
773
  windowsHide: true,
@@ -759,6 +779,7 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
759
779
  const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", targetRef]));
760
780
  const revResult = Bun.spawnSync(["git", ...revArgs], {
761
781
  cwd: repository.repoRoot,
782
+ env: buildGitEnv(),
762
783
  stdout: "pipe",
763
784
  stderr: "pipe",
764
785
  windowsHide: true,
@@ -39,8 +39,7 @@ const DESCRIPTION_UNAVAILABLE_NOTE =
39
39
  "[Image description unavailable: the vision model returned no usable text. The image was saved for further analysis.]";
40
40
 
41
41
  /** Registry surface needed to resolve a vision model and authorize requests. */
42
- export type VisionFallbackRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey" | "resolver"> &
43
- Partial<Pick<ModelRegistry, "resolveCanonicalModel" | "getCanonicalVariants" | "getCanonicalId">>;
42
+ export type VisionFallbackRegistry = Pick<ModelRegistry, "getAvailable" | "getApiKey" | "resolver">;
44
43
 
45
44
  export interface DescribeAttachedImagesDeps {
46
45
  /** Active (text-only) model the prompt is destined for. */
@@ -108,7 +107,7 @@ function resolveVisionModel(deps: DescribeAttachedImagesDeps): Model<Api> | unde
108
107
  const resolvePattern = (pattern: string | undefined): Model<Api> | undefined => {
109
108
  if (!pattern) return undefined;
110
109
  const expanded = expandRoleAlias(pattern, deps.settings);
111
- const model = resolveModelFromString(expanded, available, preferences, deps.modelRegistry);
110
+ const model = resolveModelFromString(expanded, available, preferences);
112
111
  return model?.input.includes("image") ? model : undefined;
113
112
  };
114
113
  return (
@@ -72,7 +72,7 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
72
72
  const availableModels = registry.getAvailable();
73
73
  if (availableModels.length === 0) return undefined;
74
74
 
75
- const titleModel = resolveRoleSelection(["tiny", "commit", "smol"], settings, availableModels, registry)?.model;
75
+ const titleModel = resolveRoleSelection(["tiny", "commit", "smol"], settings, availableModels)?.model;
76
76
  if (titleModel) return titleModel;
77
77
 
78
78
  if (currentModel) return currentModel;