@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.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 (254) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/cli.js +3817 -3735
  3. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
  4. package/dist/types/advisor/emission-guard.d.ts +73 -0
  5. package/dist/types/advisor/index.d.ts +1 -0
  6. package/dist/types/advisor/runtime.d.ts +7 -0
  7. package/dist/types/advisor/watchdog.d.ts +2 -0
  8. package/dist/types/cli/flag-tables.d.ts +1 -0
  9. package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
  10. package/dist/types/collab/display-name.d.ts +3 -0
  11. package/dist/types/collab/host.d.ts +0 -2
  12. package/dist/types/commands/worktree.d.ts +0 -3
  13. package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
  14. package/dist/types/config/model-discovery.d.ts +6 -1
  15. package/dist/types/config/model-registry.d.ts +6 -2
  16. package/dist/types/config/model-resolver.d.ts +12 -0
  17. package/dist/types/config/models-config-schema.d.ts +29 -2
  18. package/dist/types/config/models-config.d.ts +22 -1
  19. package/dist/types/config/settings-schema.d.ts +91 -21
  20. package/dist/types/config/settings.d.ts +2 -0
  21. package/dist/types/dap/config.d.ts +1 -1
  22. package/dist/types/edit/hashline/filesystem.d.ts +4 -2
  23. package/dist/types/edit/renderer.d.ts +4 -0
  24. package/dist/types/extensibility/extensions/types.d.ts +17 -17
  25. package/dist/types/extensibility/hooks/types.d.ts +10 -10
  26. package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
  27. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
  28. package/dist/types/internal-urls/index.d.ts +1 -0
  29. package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
  30. package/dist/types/internal-urls/router.d.ts +1 -1
  31. package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
  32. package/dist/types/internal-urls/types.d.ts +19 -2
  33. package/dist/types/irc/bus.d.ts +6 -0
  34. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
  35. package/dist/types/modes/components/custom-editor.d.ts +7 -0
  36. package/dist/types/modes/components/move-overlay.d.ts +23 -0
  37. package/dist/types/modes/components/plugin-selector.d.ts +2 -1
  38. package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
  39. package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
  40. package/dist/types/modes/components/settings-defs.d.ts +4 -1
  41. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  42. package/dist/types/modes/components/show-images-selector.d.ts +2 -1
  43. package/dist/types/modes/components/status-line/component.d.ts +2 -0
  44. package/dist/types/modes/components/status-line/types.d.ts +2 -0
  45. package/dist/types/modes/components/theme-selector.d.ts +2 -1
  46. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  47. package/dist/types/modes/controllers/command-controller.d.ts +10 -1
  48. package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
  49. package/dist/types/modes/interactive-mode.d.ts +10 -1
  50. package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
  51. package/dist/types/modes/running-subagent-badge.d.ts +6 -0
  52. package/dist/types/modes/theme/theme.d.ts +1 -1
  53. package/dist/types/modes/types.d.ts +9 -1
  54. package/dist/types/sdk.d.ts +2 -2
  55. package/dist/types/session/agent-session.d.ts +2 -1
  56. package/dist/types/session/session-manager.d.ts +13 -0
  57. package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
  58. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
  59. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
  60. package/dist/types/ssh/connection-manager.d.ts +2 -0
  61. package/dist/types/ssh/file-transfer.d.ts +79 -0
  62. package/dist/types/ssh/utils.d.ts +6 -0
  63. package/dist/types/system-prompt.d.ts +3 -0
  64. package/dist/types/task/executor.d.ts +16 -0
  65. package/dist/types/tiny/text.d.ts +1 -1
  66. package/dist/types/tools/builtin-names.d.ts +5 -1
  67. package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
  68. package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
  69. package/dist/types/tools/index.d.ts +3 -3
  70. package/dist/types/tools/path-utils.d.ts +29 -0
  71. package/dist/types/tools/read.d.ts +2 -2
  72. package/dist/types/tools/render-utils.d.ts +8 -0
  73. package/dist/types/tools/renderers.d.ts +11 -0
  74. package/dist/types/tools/ssh.d.ts +2 -0
  75. package/dist/types/tools/todo.d.ts +0 -16
  76. package/dist/types/tools/write.d.ts +2 -2
  77. package/dist/types/utils/active-repo-context.d.ts +8 -0
  78. package/dist/types/utils/image-resize.d.ts +1 -0
  79. package/dist/types/utils/markit-cache.d.ts +23 -0
  80. package/dist/types/utils/markit.d.ts +5 -1
  81. package/dist/types/utils/prompt-path.d.ts +1 -0
  82. package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
  83. package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
  84. package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
  85. package/dist/types/web/search/providers/xai.d.ts +13 -0
  86. package/dist/types/web/search/types.d.ts +18 -2
  87. package/package.json +30 -15
  88. package/scripts/build-binary.ts +9 -9
  89. package/scripts/bundle-dist.ts +2 -2
  90. package/src/advisor/__tests__/advisor.test.ts +40 -4
  91. package/src/advisor/__tests__/emission-guard.test.ts +147 -0
  92. package/src/advisor/advise-tool.ts +1 -1
  93. package/src/advisor/emission-guard.ts +172 -0
  94. package/src/advisor/index.ts +1 -0
  95. package/src/advisor/runtime.ts +11 -0
  96. package/src/advisor/watchdog.ts +12 -1
  97. package/src/cli/args.ts +5 -2
  98. package/src/cli/auth-broker-cli.ts +17 -0
  99. package/src/cli/config-cli.ts +4 -0
  100. package/src/cli/flag-tables.ts +7 -4
  101. package/src/cli/gallery-cli.ts +14 -2
  102. package/src/cli/gallery-fixtures/edit.ts +60 -0
  103. package/src/cli/gallery-fixtures/fs.ts +17 -17
  104. package/src/cli/gallery-fixtures/search.ts +4 -4
  105. package/src/cli/usage-cli.ts +20 -1
  106. package/src/cli/web-search-cli.ts +1 -1
  107. package/src/collab/display-name.ts +13 -0
  108. package/src/collab/guest.ts +5 -1
  109. package/src/collab/host.ts +2 -13
  110. package/src/commands/worktree.ts +6 -0
  111. package/src/config/inline-tool-descriptors-mode.ts +27 -0
  112. package/src/config/model-discovery.ts +77 -8
  113. package/src/config/model-registry.ts +89 -11
  114. package/src/config/model-resolver.ts +39 -1
  115. package/src/config/models-config-schema.ts +41 -5
  116. package/src/config/models-config.ts +4 -1
  117. package/src/config/settings-schema.ts +85 -27
  118. package/src/config/settings.ts +172 -1
  119. package/src/cursor.ts +1 -1
  120. package/src/dap/config.ts +152 -8
  121. package/src/dap/session.ts +1 -1
  122. package/src/discovery/helpers.ts +3 -1
  123. package/src/edit/hashline/diff.ts +14 -3
  124. package/src/edit/hashline/execute.ts +42 -6
  125. package/src/edit/hashline/filesystem.ts +38 -2
  126. package/src/edit/index.ts +1 -0
  127. package/src/edit/modes/patch.ts +11 -1
  128. package/src/edit/renderer.ts +140 -13
  129. package/src/eval/__tests__/agent-bridge.test.ts +101 -0
  130. package/src/eval/agent-bridge.ts +26 -3
  131. package/src/eval/js/tool-bridge.ts +2 -2
  132. package/src/export/html/index.ts +1 -1
  133. package/src/export/html/template.js +3 -1
  134. package/src/export/html/tool-views.generated.js +20 -20
  135. package/src/extensibility/extensions/types.ts +22 -22
  136. package/src/extensibility/hooks/types.ts +11 -11
  137. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +8 -3
  138. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -10
  139. package/src/extensibility/plugins/marketplace/manager.ts +22 -0
  140. package/src/extensibility/plugins/marketplace/types.ts +1 -0
  141. package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
  142. package/src/internal-urls/docs-index.generated.txt +2 -2
  143. package/src/internal-urls/docs-index.ts +2 -3
  144. package/src/internal-urls/index.ts +1 -0
  145. package/src/internal-urls/registry-helpers.ts +19 -4
  146. package/src/internal-urls/router.ts +5 -3
  147. package/src/internal-urls/ssh-protocol.ts +367 -0
  148. package/src/internal-urls/types.ts +19 -2
  149. package/src/irc/bus.ts +11 -3
  150. package/src/mcp/tool-bridge.ts +32 -2
  151. package/src/memories/index.ts +1 -1
  152. package/src/modes/acp/acp-event-mapper.ts +2 -2
  153. package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
  154. package/src/modes/components/agent-hub.ts +3 -0
  155. package/src/modes/components/agent-transcript-viewer.ts +9 -7
  156. package/src/modes/components/custom-editor.ts +90 -5
  157. package/src/modes/components/move-overlay.ts +282 -0
  158. package/src/modes/components/plan-review-overlay.ts +35 -35
  159. package/src/modes/components/plugin-selector.ts +6 -1
  160. package/src/modes/components/queue-mode-selector.ts +6 -1
  161. package/src/modes/components/select-list-mouse-routing.ts +35 -0
  162. package/src/modes/components/session-selector.ts +11 -10
  163. package/src/modes/components/settings-defs.ts +14 -1
  164. package/src/modes/components/settings-selector.ts +196 -29
  165. package/src/modes/components/show-images-selector.ts +6 -1
  166. package/src/modes/components/status-line/component.ts +108 -28
  167. package/src/modes/components/status-line/segments.ts +5 -1
  168. package/src/modes/components/status-line/types.ts +2 -0
  169. package/src/modes/components/theme-selector.ts +6 -1
  170. package/src/modes/components/thinking-selector.ts +6 -1
  171. package/src/modes/components/tool-execution.ts +25 -9
  172. package/src/modes/components/tree-selector.ts +5 -5
  173. package/src/modes/controllers/command-controller.ts +140 -47
  174. package/src/modes/controllers/event-controller.ts +59 -3
  175. package/src/modes/controllers/input-controller.ts +68 -2
  176. package/src/modes/controllers/selector-controller.ts +5 -2
  177. package/src/modes/controllers/streaming-reveal.ts +17 -0
  178. package/src/modes/controllers/tool-args-reveal.ts +1 -1
  179. package/src/modes/interactive-mode.ts +116 -50
  180. package/src/modes/internal-url-autocomplete.ts +17 -2
  181. package/src/modes/prompt-action-autocomplete.ts +3 -1
  182. package/src/modes/running-subagent-badge.ts +13 -0
  183. package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
  184. package/src/modes/setup-wizard/scenes/providers.ts +2 -1
  185. package/src/modes/setup-wizard/scenes/theme.ts +6 -12
  186. package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
  187. package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
  188. package/src/modes/theme/theme.ts +9 -1
  189. package/src/modes/types.ts +9 -1
  190. package/src/modes/utils/interactive-context-helpers.ts +1 -1
  191. package/src/priority.json +15 -0
  192. package/src/prompts/advisor/active-repo-watchdog.md +6 -0
  193. package/src/prompts/advisor/system.md +21 -7
  194. package/src/prompts/agents/designer.md +1 -1
  195. package/src/prompts/agents/explore.md +1 -1
  196. package/src/prompts/agents/librarian.md +2 -2
  197. package/src/prompts/agents/plan.md +2 -2
  198. package/src/prompts/agents/reviewer.md +1 -1
  199. package/src/prompts/agents/task.md +2 -2
  200. package/src/prompts/system/active-repo-context.md +4 -0
  201. package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
  202. package/src/prompts/system/plan-mode-active.md +3 -3
  203. package/src/prompts/system/project-prompt.md +2 -2
  204. package/src/prompts/system/system-prompt.md +6 -6
  205. package/src/prompts/tools/bash.md +2 -2
  206. package/src/prompts/tools/checkpoint.md +1 -1
  207. package/src/prompts/tools/{find.md → glob.md} +1 -1
  208. package/src/prompts/tools/{search.md → grep.md} +3 -3
  209. package/src/prompts/tools/read.md +4 -2
  210. package/src/sdk.ts +55 -25
  211. package/src/session/agent-session.ts +508 -132
  212. package/src/session/session-history-format.ts +2 -2
  213. package/src/session/session-manager.ts +46 -0
  214. package/src/slash-commands/builtin-registry.ts +138 -20
  215. package/src/slash-commands/helpers/usage-report.ts +23 -2
  216. package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
  217. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
  218. package/src/ssh/connection-manager.ts +12 -15
  219. package/src/ssh/file-transfer.ts +209 -0
  220. package/src/ssh/utils.ts +24 -0
  221. package/src/system-prompt.ts +56 -28
  222. package/src/task/executor.ts +96 -36
  223. package/src/task/index.ts +3 -3
  224. package/src/task/render.ts +14 -13
  225. package/src/task/worktree.ts +38 -1
  226. package/src/tiny/text.ts +49 -4
  227. package/src/tiny/worker.ts +3 -3
  228. package/src/tools/bash.ts +2 -2
  229. package/src/tools/builtin-names.ts +26 -2
  230. package/src/tools/{find.ts → glob.ts} +48 -42
  231. package/src/tools/{search.ts → grep.ts} +311 -129
  232. package/src/tools/index.ts +13 -14
  233. package/src/tools/irc.ts +6 -2
  234. package/src/tools/path-utils.ts +64 -1
  235. package/src/tools/read.ts +35 -6
  236. package/src/tools/render-utils.ts +14 -0
  237. package/src/tools/renderers.ts +15 -4
  238. package/src/tools/ssh.ts +17 -2
  239. package/src/tools/todo.ts +4 -26
  240. package/src/tools/write.ts +25 -9
  241. package/src/utils/active-repo-context.ts +143 -0
  242. package/src/utils/image-resize.ts +88 -7
  243. package/src/utils/lang-from-path.ts +3 -3
  244. package/src/utils/markit-cache.ts +166 -0
  245. package/src/utils/markit.ts +86 -18
  246. package/src/utils/prompt-path.ts +3 -0
  247. package/src/utils/title-generator.ts +1 -1
  248. package/src/web/search/provider.ts +54 -34
  249. package/src/web/search/providers/duckduckgo.ts +140 -0
  250. package/src/web/search/providers/firecrawl.ts +144 -0
  251. package/src/web/search/providers/tinyfish.ts +159 -0
  252. package/src/web/search/providers/xai.ts +292 -0
  253. package/src/web/search/providers/zai.ts +142 -56
  254. package/src/web/search/types.ts +6 -2
@@ -18,6 +18,7 @@ import {
18
18
  formatExpandHint,
19
19
  formatMoreItems,
20
20
  formatStatusIcon,
21
+ previewLine,
21
22
  replaceTabs,
22
23
  type ToolUIStatus,
23
24
  truncateToWidth,
@@ -541,7 +542,7 @@ function renderTaskCallLines(args: Partial<TaskParams> | undefined, theme: Theme
541
542
  if (idLabel || desc) {
542
543
  let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel || "agent"))}`;
543
544
  if (desc) {
544
- line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
545
+ line += `: ${theme.fg("muted", previewLine(desc, 64))}`;
545
546
  }
546
547
  lines.push(line);
547
548
  }
@@ -574,7 +575,7 @@ function renderTaskItemLines(tasks: TaskItem[] | undefined, theme: Theme): strin
574
575
  let line = `${bullet} ${theme.fg("accent", theme.bold(idLabel))}`;
575
576
  const desc = typeof task?.description === "string" ? task.description.trim() : "";
576
577
  if (desc) {
577
- line += `: ${theme.fg("muted", truncateToWidth(replaceTabs(desc), 64))}`;
578
+ line += `: ${theme.fg("muted", previewLine(desc, 64))}`;
578
579
  }
579
580
  if (task?.isolated === true) {
580
581
  line += theme.fg("dim", " [isolated]");
@@ -707,7 +708,8 @@ function renderAgentProgress(
707
708
  : "accent";
708
709
 
709
710
  // Main status line: id: description [status] · stats · ⟨agent⟩
710
- const description = progress.description?.trim();
711
+ const trimmedDescription = progress.description?.trim();
712
+ const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
711
713
  const displayId = formatTaskId(progress.id);
712
714
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
713
715
  const indent = prefix ? `${prefix} ` : "";
@@ -748,7 +750,7 @@ function renderAgentProgress(
748
750
  const showBadge = settings.get("task.showResolvedModelBadge");
749
751
  if (progress.status === "running") {
750
752
  if (!description) {
751
- const taskPreview = truncateToWidth(progress.assignment ?? progress.task, 40);
753
+ const taskPreview = previewLine(progress.assignment ?? progress.task, 40);
752
754
  statusLine += ` ${theme.fg("muted", taskPreview)}`;
753
755
  }
754
756
  statusLine = appendAgentStats(statusLine, { ...progress, showResolvedModelBadge: showBadge }, theme);
@@ -766,7 +768,7 @@ function renderAgentProgress(
766
768
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("muted", progress.currentTool)}`;
767
769
  const toolDetail = progress.lastIntent ?? progress.currentToolArgs;
768
770
  if (toolDetail) {
769
- toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
771
+ toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
770
772
  }
771
773
  if (progress.currentToolStartMs) {
772
774
  const elapsed = Date.now() - progress.currentToolStartMs;
@@ -781,7 +783,7 @@ function renderAgentProgress(
781
783
  let toolLine = `${continuePrefix}${theme.tree.hook} ${theme.fg("dim", recent.tool)}`;
782
784
  const toolDetail = progress.lastIntent ?? recent.args;
783
785
  if (toolDetail) {
784
- toolLine += `: ${theme.fg("dim", truncateToWidth(replaceTabs(toolDetail), 40))}`;
786
+ toolLine += `: ${theme.fg("dim", previewLine(toolDetail, 40))}`;
785
787
  }
786
788
  lines.push(toolLine);
787
789
  }
@@ -795,12 +797,12 @@ function renderAgentProgress(
795
797
  const waitLabel = remainingMs > 0 ? `in ${formatDuration(remainingMs)}` : "now";
796
798
  const summary =
797
799
  `retrying ${progress.retryState.attempt}/${progress.retryState.maxAttempts} ${waitLabel}: ` +
798
- truncateToWidth(replaceTabs(progress.retryState.errorMessage), 60);
800
+ previewLine(progress.retryState.errorMessage, 60);
799
801
  lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("warning", summary)}`);
800
802
  } else if (progress.retryFailure && progress.status !== "running") {
801
803
  const summary = `auto-retry gave up after ${progress.retryFailure.attempt} attempt${
802
804
  progress.retryFailure.attempt === 1 ? "" : "s"
803
- }: ${truncateToWidth(replaceTabs(progress.retryFailure.errorMessage), 80)}`;
805
+ }: ${previewLine(progress.retryFailure.errorMessage, 80)}`;
804
806
  lines.push(`${continuePrefix}${theme.tree.hook} ${theme.fg("error", summary)}`);
805
807
  }
806
808
 
@@ -1029,7 +1031,8 @@ function renderAgentResult(
1029
1031
  : "failed";
1030
1032
 
1031
1033
  // Main status line: id: description [status] · stats · ⟨agent⟩
1032
- const description = result.description?.trim();
1034
+ const trimmedDescription = result.description?.trim();
1035
+ const description = trimmedDescription ? previewLine(trimmedDescription, 64) : undefined;
1033
1036
  const displayId = formatTaskId(result.id);
1034
1037
  const titlePart = description ? `${theme.bold(displayId)}: ${description}` : displayId;
1035
1038
  let statusLine = `${prefix ? `${prefix} ` : ""}${theme.fg(iconColor, icon)} ${theme.fg(
@@ -1062,7 +1065,7 @@ function renderAgentResult(
1062
1065
 
1063
1066
  if (aborted && result.abortReason) {
1064
1067
  lines.push(
1065
- `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", truncateToWidth(replaceTabs(result.abortReason), 80))}`,
1068
+ `${continuePrefix}${theme.fg("error", theme.status.aborted)} ${theme.fg("dim", previewLine(result.abortReason, 80))}`,
1066
1069
  );
1067
1070
  }
1068
1071
  // Check for review result (yield with review schema + report_finding)
@@ -1174,9 +1177,7 @@ function renderAgentResult(
1174
1177
 
1175
1178
  // Error message
1176
1179
  if (result.error && (!success || mergeFailed) && (!aborted || result.error !== result.abortReason)) {
1177
- lines.push(
1178
- `${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", truncateToWidth(replaceTabs(result.error), 70))}`,
1179
- );
1180
+ lines.push(`${continuePrefix}${theme.fg(mergeFailed ? "warning" : "error", previewLine(result.error, 70))}`);
1180
1181
  }
1181
1182
 
1182
1183
  return lines;
@@ -169,6 +169,39 @@ export interface NestedRepoPatch {
169
169
  patch: string;
170
170
  }
171
171
 
172
+ function unquoteGitDiffPath(rawPath: string): string {
173
+ let value = rawPath;
174
+ if (value.startsWith('"') && value.endsWith('"')) {
175
+ try {
176
+ value = JSON.parse(value) as string;
177
+ } catch {
178
+ value = value.slice(1, -1);
179
+ }
180
+ }
181
+ return value.replace(/^[ab]\//, "");
182
+ }
183
+
184
+ function parseDiffGitLinePaths(line: string): string[] {
185
+ if (!line.startsWith("diff --git ")) return [];
186
+ const rest = line.slice("diff --git ".length);
187
+ const quoted = rest.match(/^("(?:\\.|[^"])+"|\/dev\/null) ("(?:\\.|[^"])+"|\/dev\/null)$/);
188
+ const parts = quoted ? [quoted[1], quoted[2]] : rest.split(" ");
189
+ if (parts.length < 2) return [];
190
+ const paths = parts
191
+ .slice(0, 2)
192
+ .map(unquoteGitDiffPath)
193
+ .filter(file => file && file !== "/dev/null");
194
+ return [...new Set(paths)];
195
+ }
196
+
197
+ function patchTouchedFiles(patch: string): string[] {
198
+ const files = new Set<string>();
199
+ for (const line of patch.split("\n")) {
200
+ for (const file of parseDiffGitLinePaths(line)) files.add(file);
201
+ }
202
+ return [...files];
203
+ }
204
+
172
205
  export interface DeltaPatchResult {
173
206
  rootPatch: string;
174
207
  nestedPatches: NestedRepoPatch[];
@@ -234,6 +267,7 @@ export async function applyNestedPatches(
234
267
  }
235
268
 
236
269
  const combinedDiff = repoPatches.map(p => p.patch).join("\n");
270
+ const touchedFiles = [...new Set(repoPatches.flatMap(p => patchTouchedFiles(p.patch)))];
237
271
 
238
272
  // Preserve any pre-existing dirty state (tracked + untracked) so we
239
273
  // commit only the agent delta, not the user's in-flight work.
@@ -246,8 +280,11 @@ export async function applyNestedPatches(
246
280
  await git.patch.applyText(nestedDir, patch);
247
281
  }
248
282
  if ((await git.status(nestedDir)).trim().length > 0) {
283
+ if (touchedFiles.length === 0) {
284
+ throw new Error(`Nested repo patch for ${relativePath} did not include stageable file paths.`);
285
+ }
249
286
  const msg = (await commitMessage?.(combinedDiff)) ?? "changes from isolated task(s)";
250
- await git.stage.files(nestedDir);
287
+ await git.stage.files(nestedDir, touchedFiles);
251
288
  await git.commit(nestedDir, msg);
252
289
  }
253
290
  } finally {
package/src/tiny/text.ts CHANGED
@@ -153,7 +153,7 @@ export function isLowSignalTitleInput(message: string): boolean {
153
153
  */
154
154
  export const NO_TITLE_SENTINEL = "none";
155
155
 
156
- export function normalizeGeneratedTitle(value: string | null | undefined): string | null {
156
+ export function normalizeGeneratedTitle(value: string | null | undefined, sourceText?: string): string | null {
157
157
  const firstLine = value?.trim().split(/\r?\n/, 1)[0]?.trim();
158
158
  if (!firstLine) return null;
159
159
  const title = firstLine
@@ -161,9 +161,54 @@ export function normalizeGeneratedTitle(value: string | null | undefined): strin
161
161
  .replace(/[.!?]$/, "")
162
162
  .trim();
163
163
  if (!title || title.toLowerCase() === NO_TITLE_SENTINEL) return null;
164
- return titleCase(title);
164
+ return sourceText === undefined ? title : reconcileTitleCasing(title, sourceText);
165
165
  }
166
166
 
167
- function titleCase(value: string): string {
168
- return value.replace(/\b\p{Ll}/gu, c => c.toUpperCase());
167
+ /**
168
+ * Reconcile a generated title's casing against the user's own message.
169
+ *
170
+ * The title prompt asks for sentence case, but small title models still mangle
171
+ * casing two ways: they sprout stray interior capitals on ordinary words
172
+ * (`daemon` → `dAemon`) and they flatten proper nouns the user cares about
173
+ * (`TinyVMM` → `tinyvmm`). The user's message is the source of truth, so per
174
+ * title token:
175
+ * 1. typed verbatim in the message → keep it (the user established the casing);
176
+ * 2. else the message has the same word with *distinctive* casing
177
+ * (`TinyVMM`, `iOS`, `API`) → adopt the user's casing (restoration);
178
+ * 3. else it's a camelCase artifact (lowercase word + stray interior capital,
179
+ * `dAemon`) the user never wrote → lowercase it;
180
+ * 4. else leave it — preserves model-cased proper nouns like `GitHub`, `OAuth`.
181
+ *
182
+ * Restoration is limited to distinctively cased source tokens so a sentence that
183
+ * merely *starts* with `For` can't force a mid-title `for` to `For`.
184
+ */
185
+ function reconcileTitleCasing(title: string, sourceText: string): string {
186
+ const verbatim = new Set<string>();
187
+ const distinctive = new Map<string, string>();
188
+ for (const [token] of sourceText.matchAll(TITLE_WORD)) {
189
+ verbatim.add(token);
190
+ if (isDistinctiveCasing(token)) {
191
+ const lower = token.toLowerCase();
192
+ if (!distinctive.has(lower)) distinctive.set(lower, token);
193
+ }
194
+ }
195
+ return title.replace(TITLE_WORD, token => {
196
+ if (verbatim.has(token)) return token;
197
+ const restored = distinctive.get(token.toLowerCase());
198
+ if (restored) return restored;
199
+ return isCamelArtifact(token) ? token.toLowerCase() : token;
200
+ });
201
+ }
202
+
203
+ /** Casing richer than a leading capital — interior or repeated uppercase
204
+ * (`TinyVMM`, `iOS`, `API`). Worth restoring from the user's message. */
205
+ function isDistinctiveCasing(token: string): boolean {
206
+ return /\p{L}\p{Lu}/u.test(token);
207
+ }
208
+
209
+ /** A lowercase word carrying a stray interior capital (`dAemon`, `cReate`): the
210
+ * model-mangled shape we flatten when the user never wrote it. PascalCase proper
211
+ * nouns (`GitHub`, `OAuth`) start uppercase and are left untouched. */
212
+ function isCamelArtifact(token: string): boolean {
213
+ return /^\p{Ll}/u.test(token) && /\p{Lu}/u.test(token);
169
214
  }
@@ -217,14 +217,14 @@ function buildPrompt(generator: TextGenerationPipeline, message: string, systemP
217
217
  return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}${TITLE_PREFILL}`;
218
218
  }
219
219
 
220
- function extractTinyTitle(text: string): string | null {
220
+ function extractTinyTitle(text: string, sourceText: string): string | null {
221
221
  const titleStart = text.lastIndexOf(TITLE_PREFILL);
222
222
  const withoutPrefix = titleStart >= 0 ? text.slice(titleStart + TITLE_PREFILL.length) : text;
223
223
  const closeIndex = withoutPrefix.indexOf(TITLE_CLOSE);
224
224
  const withoutClose = closeIndex >= 0 ? withoutPrefix.slice(0, closeIndex) : withoutPrefix;
225
225
  const tagIndex = withoutClose.indexOf("<");
226
226
  const withoutTag = tagIndex >= 0 ? withoutClose.slice(0, tagIndex) : withoutClose;
227
- return normalizeGeneratedTitle(withoutTag);
227
+ return normalizeGeneratedTitle(withoutTag, sourceText);
228
228
  }
229
229
 
230
230
  async function generateTitle(
@@ -249,7 +249,7 @@ async function generateTitle(
249
249
  return_full_text: false,
250
250
  stopping_criteria: createStopOnTextCriteria(transformers, generator.tokenizer, TITLE_CLOSE),
251
251
  })) as TextGenerationStringOutput;
252
- return extractTinyTitle(output[0]?.generated_text ?? "");
252
+ return extractTinyTitle(output[0]?.generated_text ?? "", message);
253
253
  }
254
254
 
255
255
  function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string): string {
package/src/tools/bash.ts CHANGED
@@ -399,8 +399,8 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
399
399
  autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
400
400
  hasAstGrep: this.session.settings.get("astGrep.enabled"),
401
401
  hasAstEdit: this.session.settings.get("astEdit.enabled"),
402
- hasSearch: this.session.settings.get("search.enabled"),
403
- hasFind: this.session.settings.get("find.enabled"),
402
+ hasGrep: this.session.settings.get("grep.enabled"),
403
+ hasGlob: this.session.settings.get("glob.enabled"),
404
404
  });
405
405
  }
406
406
 
@@ -9,8 +9,8 @@ export const BUILTIN_TOOL_NAMES = [
9
9
  "eval",
10
10
  "ssh",
11
11
  "github",
12
- "find",
13
- "search",
12
+ "glob",
13
+ "grep",
14
14
  "lsp",
15
15
  "inspect_image",
16
16
  "browser",
@@ -32,3 +32,27 @@ export const BUILTIN_TOOL_NAMES = [
32
32
  ] as const;
33
33
 
34
34
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
35
+
36
+ const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = new Map([
37
+ ["search", "grep"],
38
+ ["find", "glob"],
39
+ ]);
40
+
41
+ /** Return the canonical tool name for current and legacy built-in tool IDs. */
42
+ export function normalizeToolName(name: string): string {
43
+ const normalized = name.toLowerCase();
44
+ return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(normalized) ?? normalized;
45
+ }
46
+
47
+ /** Normalize and deduplicate tool names while preserving first-seen order. */
48
+ export function normalizeToolNames(names: Iterable<string>): string[] {
49
+ const out: string[] = [];
50
+ const seen = new Set<string>();
51
+ for (const name of names) {
52
+ const normalized = normalizeToolName(name);
53
+ if (seen.has(normalized)) continue;
54
+ seen.add(normalized);
55
+ out.push(normalized);
56
+ }
57
+ return out;
58
+ }
@@ -10,7 +10,7 @@ import { type } from "arktype";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { InternalUrlRouter } from "../internal-urls";
12
12
  import type { Theme } from "../modes/theme/theme";
13
- import findDescription from "../prompts/tools/find.md" with { type: "text" };
13
+ import globDescription from "../prompts/tools/glob.md" with { type: "text" };
14
14
  import { type TruncationResult, truncateHead } from "../session/streaming-output";
15
15
  import { Ellipsis, fileHyperlink, renderFileList, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
16
16
  import type { ToolSession } from ".";
@@ -20,6 +20,7 @@ import {
20
20
  expandDelimitedPathEntries,
21
21
  formatPathRelativeToCwd,
22
22
  hasGlobPathChars,
23
+ isSshUrl,
23
24
  normalizePathLikeInput,
24
25
  parseFindPattern,
25
26
  partitionExistingPaths,
@@ -47,13 +48,13 @@ const findSchema = type({
47
48
  "limit?": type("number").describe("max results"),
48
49
  });
49
50
 
50
- export type FindToolInput = typeof findSchema.infer;
51
+ export type GlobToolInput = typeof findSchema.infer;
51
52
 
52
53
  const DEFAULT_LIMIT = 200;
53
54
  const MAX_LIMIT = 200;
54
55
  const DEFAULT_GLOB_TIMEOUT_MS = 5000;
55
56
 
56
- export interface FindToolDetails {
57
+ export interface GlobToolDetails {
57
58
  truncation?: TruncationResult;
58
59
  resultLimitReached?: number;
59
60
  meta?: OutputMeta;
@@ -76,7 +77,7 @@ export interface FindToolDetails {
76
77
  * Pluggable operations for the find tool.
77
78
  * Override these to delegate file search to remote systems (e.g., SSH).
78
79
  */
79
- export interface FindOperations {
80
+ export interface GlobOperations {
80
81
  /** Check if path exists */
81
82
  exists: (absolutePath: string) => Promise<boolean> | boolean;
82
83
  /** Optional stat for distinguishing files vs directories. */
@@ -87,28 +88,28 @@ export interface FindOperations {
87
88
  glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
88
89
  }
89
90
 
90
- export interface FindToolOptions {
91
+ export interface GlobToolOptions {
91
92
  /** Custom operations for find. Default: local filesystem + rg */
92
- operations?: FindOperations;
93
+ operations?: GlobOperations;
93
94
  }
94
95
 
95
- interface FindTarget {
96
+ interface GlobTarget {
96
97
  searchPath: string;
97
98
  globPattern: string;
98
99
  hasGlob: boolean;
99
100
  }
100
101
 
101
- export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
102
- readonly name = "find";
102
+ export class GlobTool implements AgentTool<typeof findSchema, GlobToolDetails> {
103
+ readonly name = "glob";
103
104
  readonly approval = "read" as const;
104
105
  readonly loadMode = "essential";
105
- readonly label = "Find";
106
+ readonly label = "Glob";
106
107
  readonly description: string;
107
108
  readonly parameters = findSchema;
108
109
 
109
110
  readonly examples: readonly ToolExample<typeof findSchema.infer>[] = [
110
111
  {
111
- caption: "Find files",
112
+ caption: "Glob files",
112
113
  call: { paths: ["src/**/*.ts"] },
113
114
  },
114
115
  {
@@ -116,33 +117,33 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
116
117
  call: { paths: ["src/**/*.ts", "test/**/*.ts"] },
117
118
  },
118
119
  {
119
- caption: "Find gitignored files like .env",
120
+ caption: "Glob gitignored files like .env",
120
121
  call: { paths: [".env*"], gitignore: false },
121
122
  },
122
123
  {
123
- caption: "Find directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
124
+ caption: "Glob directories matching a name (returns both files and dirs; directories are suffixed with `/`)",
124
125
  call: { paths: ["**/tests"] },
125
126
  },
126
127
  ];
127
128
  readonly strict = true;
128
129
 
129
- readonly #customOps?: FindOperations;
130
+ readonly #customOps?: GlobOperations;
130
131
 
131
132
  constructor(
132
133
  private readonly session: ToolSession,
133
- options?: FindToolOptions,
134
+ options?: GlobToolOptions,
134
135
  ) {
135
136
  this.#customOps = options?.operations;
136
- this.description = prompt.render(findDescription);
137
+ this.description = prompt.render(globDescription);
137
138
  }
138
139
 
139
140
  async execute(
140
141
  _toolCallId: string,
141
142
  params: typeof findSchema.infer,
142
143
  signal?: AbortSignal,
143
- onUpdate?: AgentToolUpdateCallback<FindToolDetails>,
144
+ onUpdate?: AgentToolUpdateCallback<GlobToolDetails>,
144
145
  _context?: AgentToolContext,
145
- ): Promise<AgentToolResult<FindToolDetails>> {
146
+ ): Promise<AgentToolResult<GlobToolDetails>> {
146
147
  const { paths, limit, hidden, gitignore } = params;
147
148
 
148
149
  return untilAborted(signal, async () => {
@@ -158,6 +159,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
158
159
  normalizedPatterns.push(rawPattern);
159
160
  continue;
160
161
  }
162
+ if (isSshUrl(rawPattern)) {
163
+ throw new ToolError(
164
+ `find cannot operate on a remote ssh:// path: ${rawPattern}. ssh:// has no local file to glob; use \`read ${rawPattern}\` to list or inspect the remote path.`,
165
+ );
166
+ }
161
167
  if (hasGlobPathChars(rawPattern)) {
162
168
  throw new ToolError(`Glob patterns are not supported for internal URLs: ${rawPattern}`);
163
169
  }
@@ -194,7 +200,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
194
200
 
195
201
  const multiPattern = await resolveExplicitFindPatterns(effectivePatterns, this.session.cwd);
196
202
  const isSingle = !multiPattern;
197
- const targets: FindTarget[] = multiPattern
203
+ const targets: GlobTarget[] = multiPattern
198
204
  ? multiPattern.targets.map(target => ({
199
205
  searchPath: resolveToCwd(target.basePath, this.session.cwd),
200
206
  globPattern: target.globPattern,
@@ -242,11 +248,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
242
248
  const buildResult = (
243
249
  files: string[],
244
250
  opts?: { notice?: string; forceTruncated?: boolean },
245
- ): AgentToolResult<FindToolDetails> => {
251
+ ): AgentToolResult<GlobToolDetails> => {
246
252
  const notice = opts?.notice;
247
253
  const forceTruncated = opts?.forceTruncated ?? false;
248
254
  if (files.length === 0) {
249
- const details: FindToolDetails = {
255
+ const details: GlobToolDetails = {
250
256
  scopePath,
251
257
  fileCount: 0,
252
258
  files: [],
@@ -272,7 +278,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
272
278
  const rawOutput = trailingNotes.length > 0 ? `${baseOutput}\n\n${trailingNotes.join("\n")}` : baseOutput;
273
279
  const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
274
280
 
275
- const details: FindToolDetails = {
281
+ const details: GlobToolDetails = {
276
282
  scopePath,
277
283
  fileCount: limited.length,
278
284
  files: limited,
@@ -337,7 +343,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
337
343
  const now = Date.now();
338
344
  if (now - lastUpdate < updateIntervalMs) return;
339
345
  lastUpdate = now;
340
- const details: FindToolDetails = {
346
+ const details: GlobToolDetails = {
341
347
  scopePath,
342
348
  fileCount: onUpdateMatches.length,
343
349
  files: onUpdateMatches.slice(),
@@ -362,7 +368,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
362
368
  };
363
369
 
364
370
  let timedOut = false;
365
- const runTarget = async (target: FindTarget): Promise<Array<{ path: string; mtime: number }>> => {
371
+ const runTarget = async (target: GlobTarget): Promise<Array<{ path: string; mtime: number }>> => {
366
372
  throwIfAborted(signal);
367
373
  let stat: fs.Stats;
368
374
  try {
@@ -435,7 +441,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
435
441
  partial.sort((a, b) => b.m - a.m);
436
442
  const sortedPaths = partial.map(entry => entry.p);
437
443
  const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
438
- const notice = `find timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
444
+ const notice = `glob timed out after ${seconds}s; returning ${sortedPaths.length} partial matches — narrow the pattern instead of retrying blindly`;
439
445
  return buildResult(sortedPaths, { notice, forceTruncated: true });
440
446
  }
441
447
 
@@ -461,33 +467,33 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
461
467
  // TUI Renderer
462
468
  // =============================================================================
463
469
 
464
- interface FindRenderArgs {
470
+ interface GlobRenderArgs {
465
471
  paths?: string | string[];
466
472
  limit?: number;
467
473
  }
468
474
 
469
- function formatFindRenderPaths(paths: FindRenderArgs["paths"]): string | undefined {
475
+ function formatGlobRenderPaths(paths: GlobRenderArgs["paths"]): string | undefined {
470
476
  return Array.isArray(paths) ? paths.join(", ") : paths;
471
477
  }
472
478
 
473
479
  const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
474
480
 
475
- function findStatusIcon(uiTheme: Theme): string {
481
+ function globStatusIcon(uiTheme: Theme): string {
476
482
  return uiTheme.fg("toolTitle", uiTheme.symbol("icon.search"));
477
483
  }
478
484
 
479
- export const findToolRenderer = {
485
+ export const globToolRenderer = {
480
486
  inline: true,
481
- renderCall(args: FindRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
487
+ renderCall(args: GlobRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
482
488
  const meta: string[] = [];
483
489
  if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
484
490
 
485
491
  const text = renderStatusLine(
486
492
  {
487
493
  icon: "pending",
488
- title: "Find",
494
+ title: "Glob",
489
495
  titleColor: "toolTitle",
490
- description: formatFindRenderPaths(args.paths) || "*",
496
+ description: formatGlobRenderPaths(args.paths) || "*",
491
497
  meta,
492
498
  },
493
499
  uiTheme,
@@ -496,10 +502,10 @@ export const findToolRenderer = {
496
502
  },
497
503
 
498
504
  renderResult(
499
- result: { content: Array<{ type: string; text?: string }>; details?: FindToolDetails; isError?: boolean },
505
+ result: { content: Array<{ type: string; text?: string }>; details?: GlobToolDetails; isError?: boolean },
500
506
  options: RenderResultOptions,
501
507
  uiTheme: Theme,
502
- args?: FindRenderArgs,
508
+ args?: GlobRenderArgs,
503
509
  ): Component {
504
510
  const details = result.details;
505
511
 
@@ -524,10 +530,10 @@ export const findToolRenderer = {
524
530
  const lines = textContent.split("\n").filter(l => l.trim());
525
531
  const header = renderStatusLine(
526
532
  {
527
- iconOverride: findStatusIcon(uiTheme),
528
- title: "Find",
533
+ iconOverride: globStatusIcon(uiTheme),
534
+ title: "Glob",
529
535
  titleColor: "toolTitle",
530
- description: formatFindRenderPaths(args?.paths),
536
+ description: formatGlobRenderPaths(args?.paths),
531
537
  meta: [formatCount("file", lines.length)],
532
538
  },
533
539
  uiTheme,
@@ -565,9 +571,9 @@ export const findToolRenderer = {
565
571
  const header = renderStatusLine(
566
572
  {
567
573
  icon: "warning",
568
- title: "Find",
574
+ title: "Glob",
569
575
  titleColor: "toolTitle",
570
- description: formatFindRenderPaths(args?.paths),
576
+ description: formatGlobRenderPaths(args?.paths),
571
577
  meta: ["0 files"],
572
578
  },
573
579
  uiTheme,
@@ -581,10 +587,10 @@ export const findToolRenderer = {
581
587
  if (truncated) meta.push(uiTheme.fg("warning", "truncated"));
582
588
  const header = renderStatusLine(
583
589
  {
584
- ...(truncated ? { icon: "warning" as const } : { iconOverride: findStatusIcon(uiTheme) }),
585
- title: "Find",
590
+ ...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
591
+ title: "Glob",
586
592
  titleColor: "toolTitle",
587
- description: formatFindRenderPaths(args?.paths),
593
+ description: formatGlobRenderPaths(args?.paths),
588
594
  meta,
589
595
  },
590
596
  uiTheme,