@oh-my-pi/pi-coding-agent 15.10.11 → 15.11.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 (217) hide show
  1. package/CHANGELOG.md +103 -2
  2. package/dist/cli.js +5790 -5731
  3. package/dist/types/async/index.d.ts +0 -1
  4. package/dist/types/cli/args.d.ts +1 -0
  5. package/dist/types/cli/gallery-fixtures/types.d.ts +5 -0
  6. package/dist/types/cli-commands.d.ts +12 -0
  7. package/dist/types/commands/launch.d.ts +4 -0
  8. package/dist/types/config/api-key-resolver.d.ts +3 -0
  9. package/dist/types/config/keybindings.d.ts +6 -1
  10. package/dist/types/config/model-registry.d.ts +1 -0
  11. package/dist/types/config/model-resolver.d.ts +18 -0
  12. package/dist/types/config/settings-schema.d.ts +85 -34
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/edit/hashline/noop-loop-guard.d.ts +72 -0
  15. package/dist/types/eval/py/executor.d.ts +5 -0
  16. package/dist/types/eval/py/kernel.d.ts +6 -1
  17. package/dist/types/eval/py/runtime.d.ts +9 -0
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/export/html/template.generated.d.ts +1 -1
  20. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  21. package/dist/types/extensibility/extensions/runner.d.ts +3 -2
  22. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  23. package/dist/types/extensibility/shared-events.d.ts +2 -2
  24. package/dist/types/internal-urls/history-protocol.d.ts +14 -0
  25. package/dist/types/internal-urls/index.d.ts +1 -0
  26. package/dist/types/internal-urls/types.d.ts +1 -1
  27. package/dist/types/irc/bus.d.ts +66 -0
  28. package/dist/types/memory-backend/index.d.ts +1 -0
  29. package/dist/types/memory-backend/runtime.d.ts +4 -0
  30. package/dist/types/memory-backend/types.d.ts +66 -1
  31. package/dist/types/modes/components/agent-hub.d.ts +30 -0
  32. package/dist/types/modes/components/compaction-summary-message.d.ts +10 -4
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  34. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  35. package/dist/types/modes/components/ttsr-notification.d.ts +5 -1
  36. package/dist/types/modes/components/welcome.d.ts +3 -9
  37. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  38. package/dist/types/modes/index.d.ts +3 -3
  39. package/dist/types/modes/interactive-mode.d.ts +10 -4
  40. package/dist/types/modes/oauth-manual-input.d.ts +7 -0
  41. package/dist/types/modes/rpc/rpc-client.d.ts +39 -2
  42. package/dist/types/modes/rpc/rpc-mode.d.ts +31 -2
  43. package/dist/types/modes/rpc/rpc-subagents.d.ts +24 -0
  44. package/dist/types/modes/rpc/rpc-types.d.ts +75 -1
  45. package/dist/types/modes/setup-wizard/index.d.ts +5 -1
  46. package/dist/types/modes/setup-wizard/lazy.d.ts +2 -0
  47. package/dist/types/modes/theme/theme.d.ts +2 -1
  48. package/dist/types/modes/types.d.ts +5 -2
  49. package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
  50. package/dist/types/registry/agent-lifecycle.d.ts +51 -0
  51. package/dist/types/registry/agent-registry.d.ts +16 -5
  52. package/dist/types/secrets/index.d.ts +1 -1
  53. package/dist/types/secrets/obfuscator.d.ts +8 -2
  54. package/dist/types/session/agent-session.d.ts +49 -32
  55. package/dist/types/session/messages.d.ts +2 -4
  56. package/dist/types/session/session-history-format.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +21 -3
  58. package/dist/types/session/streaming-output.d.ts +46 -0
  59. package/dist/types/slash-commands/acp-builtins.d.ts +16 -0
  60. package/dist/types/slash-commands/builtin-registry.d.ts +1 -0
  61. package/dist/types/slash-commands/types.d.ts +1 -1
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +12 -2
  64. package/dist/types/task/index.d.ts +13 -6
  65. package/dist/types/task/output-manager.d.ts +0 -7
  66. package/dist/types/task/repair-args.d.ts +8 -7
  67. package/dist/types/task/types.d.ts +63 -51
  68. package/dist/types/thinking.d.ts +4 -0
  69. package/dist/types/tiny/title-client.d.ts +11 -0
  70. package/dist/types/tiny/title-protocol.d.ts +1 -0
  71. package/dist/types/tools/browser/tab-worker.d.ts +3 -1
  72. package/dist/types/tools/find.d.ts +0 -11
  73. package/dist/types/tools/grouped-file-output.d.ts +0 -49
  74. package/dist/types/tools/index.d.ts +7 -3
  75. package/dist/types/tools/irc.d.ts +76 -38
  76. package/dist/types/tools/job.d.ts +7 -1
  77. package/dist/types/utils/git.d.ts +15 -2
  78. package/dist/types/utils/title-generator.d.ts +3 -2
  79. package/examples/extensions/with-deps/package.json +1 -0
  80. package/package.json +11 -10
  81. package/scripts/bundle-dist.ts +28 -19
  82. package/src/async/index.ts +0 -1
  83. package/src/auto-thinking/classifier.ts +1 -0
  84. package/src/cli/args.ts +3 -0
  85. package/src/cli/gallery-cli.ts +1 -1
  86. package/src/cli/gallery-fixtures/agentic.ts +230 -115
  87. package/src/cli/gallery-fixtures/types.ts +5 -0
  88. package/src/cli-commands.ts +29 -0
  89. package/src/cli.ts +28 -15
  90. package/src/commands/launch.ts +4 -0
  91. package/src/commit/agentic/tools/analyze-file.ts +38 -19
  92. package/src/commit/model-selection.ts +3 -2
  93. package/src/config/api-key-resolver.ts +8 -6
  94. package/src/config/keybindings.ts +6 -1
  95. package/src/config/model-registry.ts +97 -30
  96. package/src/config/model-resolver.ts +60 -0
  97. package/src/config/settings-schema.ts +99 -55
  98. package/src/config/settings.ts +68 -3
  99. package/src/edit/hashline/execute.ts +39 -2
  100. package/src/edit/hashline/noop-loop-guard.ts +99 -0
  101. package/src/eval/__tests__/agent-bridge.test.ts +5 -3
  102. package/src/eval/agent-bridge.ts +3 -16
  103. package/src/eval/completion-bridge.ts +1 -0
  104. package/src/eval/js/shared/prelude.txt +1 -1
  105. package/src/eval/py/executor.ts +29 -7
  106. package/src/eval/py/index.ts +6 -1
  107. package/src/eval/py/kernel.ts +31 -11
  108. package/src/eval/py/prelude.py +5 -6
  109. package/src/eval/py/runtime.ts +37 -0
  110. package/src/exec/bash-executor.ts +82 -3
  111. package/src/export/html/template.generated.ts +1 -1
  112. package/src/export/html/template.js +38 -13
  113. package/src/extensibility/custom-tools/types.ts +2 -2
  114. package/src/extensibility/extensions/get-commands-handler.ts +2 -1
  115. package/src/extensibility/extensions/runner.ts +6 -1
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/shared-events.ts +2 -2
  118. package/src/hindsight/bank.ts +17 -2
  119. package/src/internal-urls/docs-index.generated.ts +11 -11
  120. package/src/internal-urls/history-protocol.ts +113 -0
  121. package/src/internal-urls/index.ts +1 -0
  122. package/src/internal-urls/router.ts +3 -1
  123. package/src/internal-urls/types.ts +1 -1
  124. package/src/irc/bus.ts +292 -0
  125. package/src/main.ts +26 -66
  126. package/src/memories/index.ts +2 -0
  127. package/src/memory-backend/index.ts +1 -0
  128. package/src/memory-backend/local-backend.ts +9 -0
  129. package/src/memory-backend/off-backend.ts +9 -0
  130. package/src/memory-backend/runtime.ts +66 -0
  131. package/src/memory-backend/types.ts +81 -1
  132. package/src/mnemopi/backend.ts +151 -4
  133. package/src/modes/acp/acp-agent.ts +119 -11
  134. package/src/modes/components/{session-observer-overlay.ts → agent-hub.ts} +586 -367
  135. package/src/modes/components/assistant-message.ts +19 -21
  136. package/src/modes/components/compaction-summary-message.ts +68 -32
  137. package/src/modes/components/custom-editor.ts +10 -0
  138. package/src/modes/components/footer.ts +3 -1
  139. package/src/modes/components/status-line/component.ts +118 -34
  140. package/src/modes/components/tool-execution.ts +31 -1
  141. package/src/modes/components/ttsr-notification.ts +72 -30
  142. package/src/modes/components/welcome.ts +9 -33
  143. package/src/modes/controllers/command-controller.ts +1 -1
  144. package/src/modes/controllers/event-controller.ts +65 -0
  145. package/src/modes/controllers/extension-ui-controller.ts +8 -8
  146. package/src/modes/controllers/input-controller.ts +19 -2
  147. package/src/modes/controllers/mcp-command-controller.ts +38 -3
  148. package/src/modes/controllers/selector-controller.ts +21 -17
  149. package/src/modes/index.ts +3 -21
  150. package/src/modes/interactive-mode.ts +47 -22
  151. package/src/modes/oauth-manual-input.ts +30 -3
  152. package/src/modes/rpc/rpc-client.ts +154 -3
  153. package/src/modes/rpc/rpc-mode.ts +97 -12
  154. package/src/modes/rpc/rpc-subagents.ts +265 -0
  155. package/src/modes/rpc/rpc-types.ts +81 -1
  156. package/src/modes/setup-wizard/index.ts +12 -2
  157. package/src/modes/setup-wizard/lazy.ts +16 -0
  158. package/src/modes/theme/theme.ts +18 -5
  159. package/src/modes/types.ts +5 -5
  160. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  161. package/src/modes/utils/ui-helpers.ts +51 -49
  162. package/src/prompts/system/irc-incoming.md +3 -4
  163. package/src/prompts/system/orchestrate-notice.md +2 -2
  164. package/src/prompts/system/subagent-system-prompt.md +0 -5
  165. package/src/prompts/system/system-prompt.md +1 -0
  166. package/src/prompts/system/workflow-notice.md +2 -2
  167. package/src/prompts/tools/eval.md +3 -3
  168. package/src/prompts/tools/irc.md +29 -19
  169. package/src/prompts/tools/read.md +2 -2
  170. package/src/prompts/tools/task-summary.md +5 -16
  171. package/src/prompts/tools/task.md +38 -29
  172. package/src/registry/agent-lifecycle.ts +218 -0
  173. package/src/registry/agent-registry.ts +16 -5
  174. package/src/sdk.ts +37 -10
  175. package/src/secrets/index.ts +8 -1
  176. package/src/secrets/obfuscator.ts +39 -18
  177. package/src/session/agent-session.ts +422 -291
  178. package/src/session/messages.ts +11 -78
  179. package/src/session/session-history-format.ts +246 -0
  180. package/src/session/session-manager.ts +59 -5
  181. package/src/session/streaming-output.ts +226 -10
  182. package/src/slash-commands/acp-builtins.ts +24 -0
  183. package/src/slash-commands/builtin-registry.ts +20 -0
  184. package/src/slash-commands/types.ts +1 -1
  185. package/src/system-prompt.ts +14 -0
  186. package/src/task/executor.ts +851 -461
  187. package/src/task/index.ts +721 -796
  188. package/src/task/output-manager.ts +0 -11
  189. package/src/task/render.ts +148 -63
  190. package/src/task/repair-args.ts +21 -9
  191. package/src/task/types.ts +82 -66
  192. package/src/thinking.ts +7 -0
  193. package/src/tiny/title-client.ts +34 -5
  194. package/src/tiny/title-protocol.ts +1 -1
  195. package/src/tiny/worker.ts +6 -4
  196. package/src/tools/ask.ts +4 -2
  197. package/src/tools/bash.ts +61 -10
  198. package/src/tools/browser/tab-worker.ts +26 -7
  199. package/src/tools/browser.ts +28 -1
  200. package/src/tools/find.ts +2 -27
  201. package/src/tools/grouped-file-output.ts +1 -118
  202. package/src/tools/image-gen.ts +11 -4
  203. package/src/tools/index.ts +17 -13
  204. package/src/tools/inspect-image.ts +1 -0
  205. package/src/tools/irc.ts +596 -171
  206. package/src/tools/job.ts +41 -7
  207. package/src/tools/read.ts +57 -1
  208. package/src/tools/renderers.ts +2 -0
  209. package/src/tools/resolve.ts +4 -1
  210. package/src/utils/commit-message-generator.ts +1 -0
  211. package/src/utils/git.ts +267 -13
  212. package/src/utils/title-generator.ts +24 -5
  213. package/dist/types/async/support.d.ts +0 -2
  214. package/dist/types/modes/components/session-observer-overlay.d.ts +0 -11
  215. package/dist/types/task/simple-mode.d.ts +0 -8
  216. package/src/async/support.ts +0 -5
  217. package/src/task/simple-mode.ts +0 -27
package/src/tools/job.ts CHANGED
@@ -3,7 +3,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import { Text } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
5
  import * as z from "zod/v4";
6
- import { type AsyncJob, type AsyncJobManager, isBackgroundJobSupportEnabled } from "../async";
6
+ import type { AsyncJob, AsyncJobManager } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
9
  import jobDescription from "../prompts/tools/job.md" with { type: "text" };
@@ -65,6 +65,19 @@ export interface JobToolDetails {
65
65
  cancelled?: { id: string; status: CancelStatus }[];
66
66
  }
67
67
 
68
+ /**
69
+ * A poll snapshot where every watched job is still running and nothing was
70
+ * cancelled — pure "still waiting" noise once a newer poll exists. The TUI
71
+ * keeps such a block un-finalized (displaceable) so a follow-up `job` call
72
+ * replaces it instead of stacking another waiting frame in the transcript.
73
+ */
74
+ export function isWaitingPollDetails(details: unknown): boolean {
75
+ const d = details as JobToolDetails | undefined;
76
+ if (!d || !Array.isArray(d.jobs) || d.jobs.length === 0) return false;
77
+ if (d.cancelled?.length) return false;
78
+ return d.jobs.every(job => job?.status === "running");
79
+ }
80
+
68
81
  export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
69
82
  readonly name = "job";
70
83
  readonly approval = "read" as const;
@@ -78,11 +91,6 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
78
91
  this.description = prompt.render(jobDescription);
79
92
  }
80
93
 
81
- static createIf(session: ToolSession): JobTool | null {
82
- if (!isBackgroundJobSupportEnabled(session.settings)) return null;
83
- return new JobTool(session);
84
- }
85
-
86
94
  async execute(
87
95
  _toolCallId: string,
88
96
  params: JobParams,
@@ -378,6 +386,30 @@ function statusToColor(status: JobSnapshot["status"]): ToolUIColor {
378
386
  }
379
387
  }
380
388
 
389
+ /**
390
+ * Task job results are delivered in the model-facing `<task-result>` envelope
391
+ * (prompts/tools/task-summary.md) so the parent agent can parse status and the
392
+ * `agent://` pointer. The wrapper markup is noise to a human — preview the
393
+ * inner <output>/<preview> body instead.
394
+ */
395
+ function stripTaskResultEnvelope(text: string): string {
396
+ if (!text.startsWith("<task-result")) return text;
397
+ const body = /<(output|preview)(?:\s[^>]*)?>\n?([\s\S]*?)\n?<\/\1>/.exec(text)?.[2];
398
+ return body?.trim() || text;
399
+ }
400
+
401
+ /**
402
+ * Pretty-printed JSON output wastes the collapsed one-line preview on a lone
403
+ * "{" — flatten structured-looking bodies onto a single line. Slice first:
404
+ * downstream truncation keeps at most a few hundred columns, so collapsing
405
+ * whitespace across a multi-KB body would be pure waste.
406
+ */
407
+ function flattenStructuredPreview(text: string): string {
408
+ const first = text[0];
409
+ if (first !== "{" && first !== "[") return text;
410
+ return text.slice(0, PREVIEW_LINES_EXPANDED * PREVIEW_LINE_WIDTH * 2).replace(/\s+/g, " ");
411
+ }
412
+
381
413
  function describeTarget(args: JobRenderArgs | undefined): string {
382
414
  const poll = args?.poll ?? [];
383
415
  const cancel = args?.cancel ?? [];
@@ -494,7 +526,9 @@ export const jobToolRenderer = {
494
526
  lines.push(` ${uiTheme.fg("toolOutput", visibleLabelLines[i]!)}`);
495
527
  }
496
528
 
497
- const preview = job.errorText?.trim() || job.resultText?.trim();
529
+ const preview = flattenStructuredPreview(
530
+ stripTaskResultEnvelope(job.errorText?.trim() || job.resultText?.trim() || ""),
531
+ );
498
532
  if (preview) {
499
533
  const maxLines = expanded ? PREVIEW_LINES_EXPANDED : PREVIEW_LINES_COLLAPSED;
500
534
  const previewLines = getPreviewLines(preview, maxLines, PREVIEW_LINE_WIDTH, Ellipsis.Unicode);
package/src/tools/read.ts CHANGED
@@ -736,6 +736,17 @@ interface ResolvedSqliteReadPath {
736
736
  /** Per-execute memo of suffix-glob lookups; `null` records a confirmed miss. */
737
737
  type SuffixMatchCache = Map<string, { absolutePath: string; displayPath: string } | null>;
738
738
 
739
+ /**
740
+ * Repeated whole-file reads of the same path pin stale copies in context.
741
+ * From this per-session read count onward, file reads carry a trailing nudge
742
+ * to prefer narrower re-reads.
743
+ */
744
+ const REPEAT_READ_NOTICE_THRESHOLD = 3;
745
+
746
+ function formatRepeatReadNotice(count: number): string {
747
+ return `[note: read #${count} of this file this session — after edits, prefer the context echoed in the edit result or a narrow range re-read]`;
748
+ }
749
+
739
750
  /**
740
751
  * Read tool implementation.
741
752
  *
@@ -754,6 +765,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
754
765
  readonly #autoResizeImages: boolean;
755
766
  readonly #defaultLimit: number;
756
767
  readonly #inspectImageEnabled: boolean;
768
+ /** Successful file reads per resolved base path (selector stripped) this session. */
769
+ readonly #readCounts = new Map<string, number>();
757
770
 
758
771
  constructor(private readonly session: ToolSession) {
759
772
  const displayMode = resolveFileDisplayMode(session);
@@ -772,6 +785,19 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
772
785
  });
773
786
  }
774
787
 
788
+ /**
789
+ * Count a file read of `absolutePath` and return the repeat-read nudge once
790
+ * the per-session count reaches {@link REPEAT_READ_NOTICE_THRESHOLD}.
791
+ * Non-file sources (URLs, internal resources, directories, archives,
792
+ * SQLite, images) are never counted.
793
+ */
794
+ #repeatReadNotice(absolutePath: string): string | undefined {
795
+ const count = (this.#readCounts.get(absolutePath) ?? 0) + 1;
796
+ this.#readCounts.set(absolutePath, count);
797
+ if (count < REPEAT_READ_NOTICE_THRESHOLD) return undefined;
798
+ return formatRepeatReadNotice(count);
799
+ }
800
+
775
801
  async #tryReadDelimitedPaths(
776
802
  readPath: string,
777
803
  signal?: AbortSignal,
@@ -948,6 +974,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
948
974
  ignoreResultLimits?: boolean;
949
975
  raw?: boolean;
950
976
  immutable?: boolean;
977
+ /** Trailing repeat-read nudge; appended at the very end of the text. */
978
+ repeatNotice?: string;
951
979
  },
952
980
  ): AgentToolResult<ReadToolDetails> {
953
981
  const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
@@ -1092,6 +1120,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1092
1120
  : formatLineEntries(buildLineEntries(endLine), startLineDisplay);
1093
1121
  }
1094
1122
 
1123
+ if (options.repeatNotice) {
1124
+ outputText += `\n${options.repeatNotice}`;
1125
+ }
1095
1126
  resultBuilder.text(outputText);
1096
1127
  if (truncationInfo) {
1097
1128
  resultBuilder.truncation(truncationInfo.result, truncationInfo.options);
@@ -1117,6 +1148,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1117
1148
  entityLabel: string;
1118
1149
  raw?: boolean;
1119
1150
  immutable?: boolean;
1151
+ /** Trailing repeat-read nudge; appended at the very end of the text. */
1152
+ repeatNotice?: string;
1120
1153
  },
1121
1154
  ): AgentToolResult<ReadToolDetails> {
1122
1155
  const displayMode = resolveFileDisplayMode(this.session, { raw: options.raw, immutable: options.immutable });
@@ -1177,8 +1210,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1177
1210
  const bound = range.endLine !== undefined ? `${range.startLine}-${range.endLine}` : `${range.startLine}`;
1178
1211
  notices.push(`[Range ${bound} is beyond end of ${options.entityLabel} (${totalLines} lines total); skipped]`);
1179
1212
  }
1180
- const finalText =
1213
+ let finalText =
1181
1214
  notices.length > 0 ? (outputText ? `${outputText}\n${notices.join("\n")}` : notices.join("\n")) : outputText;
1215
+ if (options.repeatNotice) {
1216
+ finalText = finalText ? `${finalText}\n${options.repeatNotice}` : options.repeatNotice;
1217
+ }
1182
1218
  resultBuilder.text(finalText);
1183
1219
  return resultBuilder.done();
1184
1220
  }
@@ -1196,6 +1232,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1196
1232
  parsed: ParsedSelector,
1197
1233
  displayMode: { hashLines: boolean; lineNumbers: boolean },
1198
1234
  suffixResolution: { from: string; to: string } | undefined,
1235
+ repeatNotice: string | undefined,
1199
1236
  signal: AbortSignal | undefined,
1200
1237
  ): Promise<{
1201
1238
  outputText: string;
@@ -1215,6 +1252,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1215
1252
  sourcePath: absolutePath,
1216
1253
  entityLabel: "file",
1217
1254
  raw: rawSelector,
1255
+ repeatNotice,
1218
1256
  });
1219
1257
  if (suffixResolution) {
1220
1258
  const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
@@ -1896,6 +1934,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1896
1934
  let details: ReadToolDetails = {};
1897
1935
  let sourcePath: string | undefined;
1898
1936
  let columnTruncated = 0;
1937
+ let repeatNotice: string | undefined;
1899
1938
  let truncationInfo:
1900
1939
  | { result: TruncationResult; options: { direction: "head"; startLine?: number; totalFileLines?: number } }
1901
1940
  | undefined;
@@ -1960,11 +1999,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1960
1999
  }
1961
2000
  } else if (isNotebookPath(absolutePath) && !isRawSelector(parsed)) {
1962
2001
  const notebookText = await readEditableNotebookText(absolutePath, localReadPath);
2002
+ repeatNotice = this.#repeatReadNotice(absolutePath);
1963
2003
  if (isMultiRange(parsed) && parsed.kind === "lines") {
1964
2004
  return this.#buildInMemoryMultiRangeResult(notebookText, parsed.ranges, {
1965
2005
  details: { resolvedPath: absolutePath },
1966
2006
  sourcePath: absolutePath,
1967
2007
  entityLabel: "notebook",
2008
+ repeatNotice,
1968
2009
  });
1969
2010
  }
1970
2011
  const { offset, limit } = selToOffsetLimit(parsed);
@@ -1972,11 +2013,13 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1972
2013
  details: { resolvedPath: absolutePath },
1973
2014
  sourcePath: absolutePath,
1974
2015
  entityLabel: "notebook",
2016
+ repeatNotice,
1975
2017
  });
1976
2018
  } else if (shouldConvertWithMarkit) {
1977
2019
  // Convert document via markit.
1978
2020
  const result = await convertFileWithMarkit(absolutePath, signal);
1979
2021
  if (result.ok) {
2022
+ repeatNotice = this.#repeatReadNotice(absolutePath);
1980
2023
  // Route the converted markdown through the in-memory text builder
1981
2024
  // so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
1982
2025
  // raw mode apply against the converted output. Without this,
@@ -1987,6 +2030,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1987
2030
  details: { resolvedPath: absolutePath },
1988
2031
  sourcePath: absolutePath,
1989
2032
  entityLabel: "document",
2033
+ repeatNotice,
1990
2034
  });
1991
2035
  }
1992
2036
  const { offset, limit } = selToOffsetLimit(parsed);
@@ -1995,6 +2039,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1995
2039
  sourcePath: absolutePath,
1996
2040
  entityLabel: "document",
1997
2041
  raw: isRawSelector(parsed),
2042
+ repeatNotice,
1998
2043
  });
1999
2044
  } else if (result.error) {
2000
2045
  content = [{ type: "text", text: `[Cannot read ${ext} file: ${result.error || "conversion failed"}]` }];
@@ -2002,6 +2047,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2002
2047
  content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
2003
2048
  }
2004
2049
  } else {
2050
+ repeatNotice = this.#repeatReadNotice(absolutePath);
2005
2051
  if (
2006
2052
  parsed.kind === "none" &&
2007
2053
  this.session.settings.get("read.summarize.enabled") &&
@@ -2043,6 +2089,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2043
2089
  parsed,
2044
2090
  displayMode,
2045
2091
  suffixResolution,
2092
+ repeatNotice,
2046
2093
  undefined, // plain-file read: deterministic and fast, never abort mid-read
2047
2094
  );
2048
2095
  if (multiResult.bridgeResult) return multiResult.bridgeResult;
@@ -2066,6 +2113,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2066
2113
  sourcePath: absolutePath,
2067
2114
  entityLabel: "file",
2068
2115
  raw: isRawSelector(parsed),
2116
+ repeatNotice,
2069
2117
  });
2070
2118
  if (suffixResolution) {
2071
2119
  const notice = `[Path '${suffixResolution.from}' not found; resolved to '${suffixResolution.to}' via suffix match]`;
@@ -2367,6 +2415,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
2367
2415
  content = [{ type: "text", text: notice }, ...content];
2368
2416
  }
2369
2417
  }
2418
+ if (repeatNotice) {
2419
+ // Trailing nudge goes at the very end of the textual result so it never
2420
+ // disturbs hashline tag headers or inline notices.
2421
+ const lastText = content.findLast((c): c is TextContent => c.type === "text");
2422
+ if (lastText) {
2423
+ lastText.text = `${lastText.text}\n${repeatNotice}`;
2424
+ }
2425
+ }
2370
2426
  const resultBuilder = toolResult(details).content(content);
2371
2427
  if (sourcePath) {
2372
2428
  resultBuilder.sourcePath(sourcePath);
@@ -21,6 +21,7 @@ import { evalToolRenderer } from "./eval-render";
21
21
  import { findToolRenderer } from "./find";
22
22
  import { githubToolRenderer } from "./gh-renderer";
23
23
  import { inspectImageToolRenderer } from "./inspect-image-renderer";
24
+ import { ircToolRenderer } from "./irc";
24
25
  import { jobToolRenderer } from "./job";
25
26
  import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
26
27
  import { readToolRenderer } from "./read";
@@ -58,6 +59,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
58
59
  search: searchToolRenderer as ToolRenderer,
59
60
  lsp: lspToolRenderer as ToolRenderer,
60
61
  inspect_image: inspectImageToolRenderer as ToolRenderer,
62
+ irc: ircToolRenderer as ToolRenderer,
61
63
  read: readToolRenderer as ToolRenderer,
62
64
  job: jobToolRenderer as ToolRenderer,
63
65
  resolve: resolveToolRenderer as ToolRenderer,
@@ -241,7 +241,10 @@ export const resolveToolRenderer = {
241
241
  const isApply = action === "apply" && !result.isError;
242
242
  const isFailedApply = action === "apply" && result.isError;
243
243
  const bgColor = result.isError ? "error" : isApply ? "success" : "warning";
244
- const icon = isApply ? uiTheme.styledSymbol("tool.resolve", "accent") : uiTheme.status.error;
244
+ // Bare symbol: the line is wrapped in inverse(fg(...)), so any embedded fg
245
+ // reset (styledSymbol/status glyphs carry their own \x1b[39m) would drop the
246
+ // inverse block back to the default background mid-line.
247
+ const icon = uiTheme.symbol(isApply ? "tool.resolve" : "status.error");
245
248
  const verb = isApply ? "Accept" : isFailedApply ? "Failed" : "Discard";
246
249
  const separator = ": ";
247
250
  const separatorIndex = label.indexOf(separator);
@@ -115,6 +115,7 @@ export async function generateCommitMessage(
115
115
  apiKey: registry.resolver(candidate.model.provider, {
116
116
  sessionId,
117
117
  baseUrl: candidate.model.baseUrl,
118
+ modelId: candidate.model.id,
118
119
  }),
119
120
  maxTokens,
120
121
  reasoning: toReasoningEffort(candidate.thinkingLevel),
package/src/utils/git.ts CHANGED
@@ -27,6 +27,7 @@ export interface GitRepository {
27
27
  gitEntryPath: string;
28
28
  headPath: string;
29
29
  repoRoot: string;
30
+ isReftable?: boolean;
30
31
  }
31
32
 
32
33
  export interface GitStatusSummary {
@@ -476,6 +477,31 @@ async function resolveCommonDir(gitDir: string): Promise<string> {
476
477
  if (!relative) return gitDir;
477
478
  return path.resolve(gitDir, relative);
478
479
  }
480
+ function isLinkedWorktree(repository: GitRepository): boolean {
481
+ return (
482
+ repository.gitDir !== repository.commonDir &&
483
+ getEntryTypeSync(path.join(repository.gitDir, "commondir")) === "file"
484
+ );
485
+ }
486
+
487
+ async function isLinkedWorktreeAsync(repository: GitRepository): Promise<boolean> {
488
+ return (
489
+ repository.gitDir !== repository.commonDir &&
490
+ (await getEntryType(path.join(repository.gitDir, "commondir"))) === "file"
491
+ );
492
+ }
493
+
494
+ function primaryRootFromRepositorySync(repository: GitRepository): string {
495
+ if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
496
+ if (isLinkedWorktree(repository)) return repository.commonDir;
497
+ return repository.repoRoot;
498
+ }
499
+
500
+ async function primaryRootFromRepository(repository: GitRepository): Promise<string> {
501
+ if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
502
+ if (await isLinkedWorktreeAsync(repository)) return repository.commonDir;
503
+ return repository.repoRoot;
504
+ }
479
505
 
480
506
  function resolveRepoFromEntrySync(repoRoot: string, gitEntryPath: string, entryType: EntryType): GitRepository | null {
481
507
  const gitDir = resolveGitDirSync(gitEntryPath, entryType);
@@ -560,7 +586,174 @@ function parsePackedRefs(content: string | null, targetRef: string): string | nu
560
586
  return null;
561
587
  }
562
588
 
589
+ function stripGitConfigComments(line: string): string {
590
+ let clean = "";
591
+ let inQuotes = false;
592
+ for (let i = 0; i < line.length; i++) {
593
+ const char = line[i];
594
+ if (char === '"') {
595
+ inQuotes = !inQuotes;
596
+ clean += char;
597
+ } else if (!inQuotes && (char === ";" || char === "#")) {
598
+ break;
599
+ } else {
600
+ clean += char;
601
+ }
602
+ }
603
+ return clean.trim();
604
+ }
605
+
606
+ function parseGitConfigHasReftable(content: string): boolean {
607
+ let inExtensions = false;
608
+ for (const line of content.split("\n")) {
609
+ const trimmed = stripGitConfigComments(line);
610
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
611
+ const section = trimmed.slice(1, -1).trim().toLowerCase();
612
+ inExtensions = section === "extensions";
613
+ } else if (inExtensions) {
614
+ const eqIndex = trimmed.indexOf("=");
615
+ if (eqIndex !== -1) {
616
+ const key = trimmed.slice(0, eqIndex).trim().toLowerCase();
617
+ let value = trimmed.slice(eqIndex + 1).trim();
618
+ if (key === "refstorage") {
619
+ if (value.startsWith('"') && value.endsWith('"')) {
620
+ value = value.slice(1, -1).trim();
621
+ }
622
+ const lowerValue = value.toLowerCase();
623
+ if (lowerValue === "reftable" || lowerValue.startsWith("reftable:")) {
624
+ return true;
625
+ }
626
+ }
627
+ }
628
+ }
629
+ }
630
+ return false;
631
+ }
632
+
633
+ function isReftableRepoSync(repository: GitRepository): boolean {
634
+ if (repository.isReftable !== undefined) return repository.isReftable;
635
+ const configPath = path.join(repository.commonDir, "config");
636
+ const content = readOptionalTextSync(configPath);
637
+ repository.isReftable = content ? parseGitConfigHasReftable(content) : false;
638
+ return repository.isReftable;
639
+ }
640
+
641
+ async function isReftableRepo(repository: GitRepository): Promise<boolean> {
642
+ if (repository.isReftable !== undefined) return repository.isReftable;
643
+ const configPath = path.join(repository.commonDir, "config");
644
+ const content = await readOptionalText(configPath);
645
+ repository.isReftable = content ? parseGitConfigHasReftable(content) : false;
646
+ return repository.isReftable;
647
+ }
648
+
649
+ async function resolveHeadStateReftable(repository: GitRepository, signal?: AbortSignal): Promise<GitHeadState | null> {
650
+ throwIfAborted(signal);
651
+ const symResult = await git(repository.repoRoot, ["symbolic-ref", "HEAD"], { readOnly: true, signal }).catch(err => {
652
+ if (signal?.aborted || (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))) {
653
+ throw err;
654
+ }
655
+ return null;
656
+ });
657
+ throwIfAborted(signal);
658
+ const revResult = await git(repository.repoRoot, ["rev-parse", "--verify", "HEAD"], {
659
+ readOnly: true,
660
+ signal,
661
+ }).catch(err => {
662
+ if (signal?.aborted || (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))) {
663
+ throw err;
664
+ }
665
+ return null;
666
+ });
667
+ const commit = revResult && revResult.exitCode === 0 ? revResult.stdout.trim() || null : null;
668
+
669
+ if (symResult && symResult.exitCode === 0) {
670
+ const ref = symResult.stdout.trim();
671
+ const branchName = ref.startsWith(LOCAL_BRANCH_PREFIX) ? ref.slice(LOCAL_BRANCH_PREFIX.length) : null;
672
+ return {
673
+ ...repository,
674
+ kind: "ref",
675
+ ref,
676
+ branchName,
677
+ commit,
678
+ headContent: `${HEAD_REF_PREFIX} ${ref}`,
679
+ };
680
+ }
681
+
682
+ return {
683
+ ...repository,
684
+ kind: "detached",
685
+ commit,
686
+ headContent: commit || "",
687
+ };
688
+ }
689
+
690
+ function resolveHeadStateReftableSync(repository: GitRepository): GitHeadState | null {
691
+ ensureAvailable();
692
+ const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", "HEAD"]));
693
+ const symResult = Bun.spawnSync(["git", ...symArgs], {
694
+ cwd: repository.repoRoot,
695
+ stdout: "pipe",
696
+ stderr: "pipe",
697
+ windowsHide: true,
698
+ });
699
+
700
+ const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", "HEAD"]));
701
+ const revResult = Bun.spawnSync(["git", ...revArgs], {
702
+ cwd: repository.repoRoot,
703
+ stdout: "pipe",
704
+ stderr: "pipe",
705
+ windowsHide: true,
706
+ });
707
+ const commit = revResult.exitCode === 0 ? new TextDecoder().decode(revResult.stdout).trim() || null : null;
708
+
709
+ if (symResult.exitCode === 0) {
710
+ const ref = new TextDecoder().decode(symResult.stdout).trim();
711
+ const branchName = ref.startsWith(LOCAL_BRANCH_PREFIX) ? ref.slice(LOCAL_BRANCH_PREFIX.length) : null;
712
+ return {
713
+ ...repository,
714
+ kind: "ref",
715
+ ref,
716
+ branchName,
717
+ commit,
718
+ headContent: `${HEAD_REF_PREFIX} ${ref}`,
719
+ };
720
+ }
721
+
722
+ return {
723
+ ...repository,
724
+ kind: "detached",
725
+ commit,
726
+ headContent: commit || "",
727
+ };
728
+ }
729
+
563
730
  function readRefSync(repository: GitRepository, targetRef: string): string | null {
731
+ if (isReftableRepoSync(repository)) {
732
+ ensureAvailable();
733
+ const symArgs = withShortLivedGitConfig(withNoOptionalLocks(["symbolic-ref", targetRef]));
734
+ const symResult = Bun.spawnSync(["git", ...symArgs], {
735
+ cwd: repository.repoRoot,
736
+ stdout: "pipe",
737
+ stderr: "pipe",
738
+ windowsHide: true,
739
+ });
740
+ if (symResult.exitCode === 0) {
741
+ const stdoutText = new TextDecoder().decode(symResult.stdout).trim();
742
+ return `${HEAD_REF_PREFIX} ${stdoutText}`;
743
+ }
744
+ const revArgs = withShortLivedGitConfig(withNoOptionalLocks(["rev-parse", "--verify", targetRef]));
745
+ const revResult = Bun.spawnSync(["git", ...revArgs], {
746
+ cwd: repository.repoRoot,
747
+ stdout: "pipe",
748
+ stderr: "pipe",
749
+ windowsHide: true,
750
+ });
751
+ if (revResult.exitCode === 0) {
752
+ return new TextDecoder().decode(revResult.stdout).trim() || null;
753
+ }
754
+ return null;
755
+ }
756
+
564
757
  for (const dir of getRefLookupDirs(repository)) {
565
758
  const value = normalizeRefValue(readOptionalTextSync(path.join(dir, targetRef)));
566
759
  if (value) return value;
@@ -572,7 +765,42 @@ function readRefSync(repository: GitRepository, targetRef: string): string | nul
572
765
  return null;
573
766
  }
574
767
 
575
- async function readRef(repository: GitRepository, targetRef: string): Promise<string | null> {
768
+ async function readRef(repository: GitRepository, targetRef: string, signal?: AbortSignal): Promise<string | null> {
769
+ if (await isReftableRepo(repository)) {
770
+ throwIfAborted(signal);
771
+ const symResult = await git(repository.repoRoot, ["symbolic-ref", targetRef], { readOnly: true, signal }).catch(
772
+ err => {
773
+ if (
774
+ signal?.aborted ||
775
+ (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))
776
+ ) {
777
+ throw err;
778
+ }
779
+ return null;
780
+ },
781
+ );
782
+ if (symResult && symResult.exitCode === 0) {
783
+ return `${HEAD_REF_PREFIX} ${symResult.stdout.trim()}`;
784
+ }
785
+ throwIfAborted(signal);
786
+ const revResult = await git(repository.repoRoot, ["rev-parse", "--verify", targetRef], {
787
+ readOnly: true,
788
+ signal,
789
+ }).catch(err => {
790
+ if (
791
+ signal?.aborted ||
792
+ (err instanceof Error && (err.name === "AbortError" || err.name === "ToolAbortError"))
793
+ ) {
794
+ throw err;
795
+ }
796
+ return null;
797
+ });
798
+ if (revResult && revResult.exitCode === 0) {
799
+ return revResult.stdout.trim() || null;
800
+ }
801
+ return null;
802
+ }
803
+
576
804
  for (const dir of getRefLookupDirs(repository)) {
577
805
  const value = normalizeRefValue(await readOptionalText(path.join(dir, targetRef)));
578
806
  if (value) return value;
@@ -997,7 +1225,7 @@ export const branch = {
997
1225
  const repository = await resolveRepository(cwd);
998
1226
  if (repository) {
999
1227
  for (const refPath of DEFAULT_BRANCH_REFS) {
1000
- const target = await readRef(repository, refPath);
1228
+ const target = await readRef(repository, refPath, signal);
1001
1229
  const branchName = parseDefaultBranchRef(refPath, target);
1002
1230
  if (branchName) return branchName;
1003
1231
  }
@@ -1095,7 +1323,7 @@ export const ref = {
1095
1323
  async exists(cwd: string, refName: string, signal?: AbortSignal): Promise<boolean> {
1096
1324
  if (refName === "HEAD") return (await head.sha(cwd, signal)) !== null;
1097
1325
  const repository = await resolveRepository(cwd);
1098
- if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName)) !== null;
1326
+ if (repository && refName.startsWith("refs/")) return (await readRef(repository, refName, signal)) !== null;
1099
1327
  const result = await git(cwd, ["show-ref", "--verify", "--quiet", refName], { readOnly: true, signal });
1100
1328
  return result.exitCode === 0;
1101
1329
  },
@@ -1104,7 +1332,7 @@ export const ref = {
1104
1332
  async resolve(cwd: string, refName: string, signal?: AbortSignal): Promise<string | null> {
1105
1333
  if (refName === "HEAD") return head.sha(cwd, signal);
1106
1334
  const repository = await resolveRepository(cwd);
1107
- if (repository && refName.startsWith("refs/")) return readRef(repository, refName);
1335
+ if (repository && refName.startsWith("refs/")) return readRef(repository, refName, signal);
1108
1336
  const result = await git(cwd, ["rev-parse", refName], { readOnly: true, signal });
1109
1337
  if (result.exitCode !== 0) return null;
1110
1338
  return result.stdout.trim() || null;
@@ -1397,9 +1625,12 @@ export const ls = {
1397
1625
 
1398
1626
  export const head = {
1399
1627
  /** Full HEAD state (branch, commit, repo info). */
1400
- async resolve(cwd: string): Promise<GitHeadState | null> {
1628
+ async resolve(cwd: string, signal?: AbortSignal): Promise<GitHeadState | null> {
1401
1629
  const repository = await resolveRepository(cwd);
1402
1630
  if (!repository) return null;
1631
+ if (await isReftableRepo(repository)) {
1632
+ return resolveHeadStateReftable(repository, signal);
1633
+ }
1403
1634
  const content = await readOptionalText(repository.headPath);
1404
1635
  if (content === null) return null;
1405
1636
  return parseHeadState(repository, content);
@@ -1409,6 +1640,9 @@ export const head = {
1409
1640
  resolveSync(cwd: string): GitHeadState | null {
1410
1641
  const repository = resolveRepositorySync(cwd);
1411
1642
  if (!repository) return null;
1643
+ if (isReftableRepoSync(repository)) {
1644
+ return resolveHeadStateReftableSync(repository);
1645
+ }
1412
1646
  const content = readOptionalTextSync(repository.headPath);
1413
1647
  if (content === null) return null;
1414
1648
  return parseHeadStateSync(repository, content);
@@ -1416,7 +1650,7 @@ export const head = {
1416
1650
 
1417
1651
  /** Current HEAD commit SHA. */
1418
1652
  async sha(cwd: string, signal?: AbortSignal): Promise<string | null> {
1419
- const headState = await head.resolve(cwd);
1653
+ const headState = await head.resolve(cwd, signal);
1420
1654
  if (headState?.commit) return headState.commit;
1421
1655
  const result = await git(cwd, ["rev-parse", "HEAD"], { readOnly: true, signal });
1422
1656
  if (result.exitCode !== 0) return null;
@@ -1445,13 +1679,10 @@ export const repo = {
1445
1679
  return result.stdout.trim() || null;
1446
1680
  },
1447
1681
 
1448
- /** Resolve the primary repository root (not a worktree the main checkout). */
1682
+ /** Resolve the primary checkout root, or the shared common dir for bare-repo worktrees. */
1449
1683
  async primaryRoot(cwd: string, signal?: AbortSignal): Promise<string | null> {
1450
1684
  const repository = await resolveRepository(cwd);
1451
- if (repository) {
1452
- if (path.basename(repository.commonDir) === ".git") return path.dirname(repository.commonDir);
1453
- return repository.repoRoot;
1454
- }
1685
+ if (repository) return primaryRootFromRepository(repository);
1455
1686
  const repoRoot = await repo.root(cwd, signal);
1456
1687
  if (!repoRoot) return null;
1457
1688
  const commonDir = await runText(repoRoot, ["rev-parse", "--path-format=absolute", "--git-common-dir"], {
@@ -1462,6 +1693,19 @@ export const repo = {
1462
1693
  return repoRoot;
1463
1694
  },
1464
1695
 
1696
+ /**
1697
+ * Sync sibling of {@link primaryRoot}. Resolves only via on-disk `.git`/
1698
+ * `commondir` walking — no subprocess fallback — so it stays usable from
1699
+ * paths where async I/O is impractical (e.g. `computeBankScope`). Returns
1700
+ * `null` when `cwd` is outside a repository. Bare-repo worktrees resolve to
1701
+ * the shared common dir (`foo.git`) because they have no primary checkout.
1702
+ */
1703
+ primaryRootSync(cwd: string): string | null {
1704
+ const repository = resolveRepositorySync(cwd);
1705
+ if (!repository) return null;
1706
+ return primaryRootFromRepositorySync(repository);
1707
+ },
1708
+
1465
1709
  /** Full GitRepository metadata (sync). */
1466
1710
  resolveSync(cwd: string): GitRepository | null {
1467
1711
  return resolveRepositorySync(cwd);
@@ -1471,11 +1715,21 @@ export const repo = {
1471
1715
  resolve(cwd: string): Promise<GitRepository | null> {
1472
1716
  return resolveRepository(cwd);
1473
1717
  },
1718
+
1719
+ /** Check if the repository uses the reftable reference storage format (sync). */
1720
+ isReftableSync(repository: GitRepository): boolean {
1721
+ return isReftableRepoSync(repository);
1722
+ },
1723
+
1724
+ /** Check if the repository uses the reftable reference storage format. */
1725
+ isReftable(repository: GitRepository): Promise<boolean> {
1726
+ return isReftableRepo(repository);
1727
+ },
1474
1728
  };
1475
1729
 
1476
1730
  // Helper used during head resolution — defined here to reference `head` namespace.
1477
- async function resolveHead(cwd: string): Promise<GitHeadState | null> {
1478
- return head.resolve(cwd);
1731
+ async function resolveHead(cwd: string, signal?: AbortSignal): Promise<GitHeadState | null> {
1732
+ return head.resolve(cwd, signal);
1479
1733
  }
1480
1734
 
1481
1735
  // ════════════════════════════════════════════════════════════════════════════