@oh-my-pi/pi-coding-agent 17.2.13 → 17.2.15

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 (239) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{CHANGELOG-d8xh7keh.md → CHANGELOG-fk29zh06.md} +30 -0
  3. package/dist/cli.js +5114 -5417
  4. package/dist/types/cleanse/agent.d.ts +3 -0
  5. package/dist/types/cleanse/checkers.d.ts +21 -1
  6. package/dist/types/cleanse/index.d.ts +4 -0
  7. package/dist/types/cleanse/parsers.d.ts +3 -1
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/cleanse-picker.d.ts +17 -0
  10. package/dist/types/cli/command-help.d.ts +3 -0
  11. package/dist/types/cli/progress-reporter.d.ts +19 -0
  12. package/dist/types/commands/cleanse.d.ts +11 -0
  13. package/dist/types/commands/compress.d.ts +37 -0
  14. package/dist/types/commands/launch-help.d.ts +3 -0
  15. package/dist/types/commands/launch.d.ts +3 -0
  16. package/dist/types/compress/index.d.ts +27 -0
  17. package/dist/types/compress/protocol.d.ts +49 -0
  18. package/dist/types/compress/session.d.ts +15 -0
  19. package/dist/types/compress/types.d.ts +54 -0
  20. package/dist/types/config/settings-schema.d.ts +10 -0
  21. package/dist/types/internal-urls/local-protocol.d.ts +8 -0
  22. package/dist/types/mcp/transports/header-policy.d.ts +8 -0
  23. package/dist/types/mcp/transports/http.d.ts +2 -0
  24. package/dist/types/mcp/types.d.ts +19 -0
  25. package/dist/types/modes/components/late-diagnostics-message.d.ts +2 -0
  26. package/dist/types/modes/components/todo-reminder.d.ts +2 -0
  27. package/dist/types/modes/components/tool-activity.d.ts +17 -0
  28. package/dist/types/modes/components/transcript-container.d.ts +2 -0
  29. package/dist/types/modes/components/ttsr-notification.d.ts +2 -0
  30. package/dist/types/modes/interactive-mode.d.ts +3 -1
  31. package/dist/types/modes/types.d.ts +3 -1
  32. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -1
  33. package/dist/types/modes/utils/ui-helpers.d.ts +3 -1
  34. package/dist/types/session/agent-session-types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +2 -0
  36. package/dist/types/session/session-tools.d.ts +13 -0
  37. package/dist/types/tools/builtin-names.d.ts +1 -2
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/think.d.ts +41 -0
  40. package/dist/types/utils/zip.d.ts +19 -9
  41. package/package.json +13 -13
  42. package/src/cleanse/agent.ts +67 -2
  43. package/src/cleanse/checkers.ts +252 -21
  44. package/src/cleanse/index.ts +90 -36
  45. package/src/cleanse/parsers.ts +243 -20
  46. package/src/cleanse/prompts/discovery.md +74 -0
  47. package/src/cli/args.ts +3 -0
  48. package/src/cli/cleanse-picker.ts +86 -0
  49. package/src/cli/command-help.ts +4 -0
  50. package/src/cli/flag-tables.ts +1 -0
  51. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  52. package/src/{cleanse/progress.ts → cli/progress-reporter.ts} +12 -7
  53. package/src/cli-commands.ts +5 -0
  54. package/src/commands/cleanse.ts +19 -4
  55. package/src/commands/compress.ts +45 -0
  56. package/src/commands/launch-help.ts +3 -0
  57. package/src/commit/agentic/prompts/analyze-file.md +6 -6
  58. package/src/commit/agentic/prompts/session-user.md +4 -4
  59. package/src/compress/index.ts +318 -0
  60. package/src/compress/prompts/request.md +11 -0
  61. package/src/compress/prompts/review.md +17 -0
  62. package/src/compress/prompts/system.md +81 -0
  63. package/src/compress/protocol.ts +210 -0
  64. package/src/compress/session.ts +72 -0
  65. package/src/compress/types.ts +59 -0
  66. package/src/config/settings-schema.ts +11 -0
  67. package/src/discovery/builtin-rules/go-add-cleanup.md +8 -8
  68. package/src/discovery/builtin-rules/go-exp-promoted.md +8 -8
  69. package/src/discovery/builtin-rules/go-ioutil.md +12 -12
  70. package/src/discovery/builtin-rules/go-new-expr.md +8 -8
  71. package/src/discovery/builtin-rules/go-range-int.md +5 -5
  72. package/src/discovery/builtin-rules/rs-box-leak.md +7 -7
  73. package/src/discovery/builtin-rules/rs-future-prelude.md +4 -4
  74. package/src/discovery/builtin-rules/rs-parking-lot.md +6 -6
  75. package/src/discovery/builtin-rules/ts-no-any.md +5 -5
  76. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +8 -8
  77. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +13 -14
  78. package/src/discovery/builtin-rules/ts-no-local-is-record.md +7 -7
  79. package/src/discovery/builtin-rules/ts-no-test-timers.md +3 -9
  80. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +7 -7
  81. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +2 -2
  82. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +3 -9
  83. package/src/discovery/builtin-rules/ts-set-map.md +2 -4
  84. package/src/internal-urls/local-protocol.ts +42 -0
  85. package/src/live/prompts/live-instructions.md +10 -10
  86. package/src/main.ts +4 -0
  87. package/src/mcp/client.ts +7 -3
  88. package/src/mcp/transports/header-policy.ts +28 -0
  89. package/src/mcp/transports/http.ts +200 -35
  90. package/src/mcp/types.ts +21 -0
  91. package/src/modes/components/chat-transcript-builder.ts +17 -5
  92. package/src/modes/components/late-diagnostics-message.ts +12 -1
  93. package/src/modes/components/todo-reminder.ts +12 -0
  94. package/src/modes/components/tool-activity.ts +45 -0
  95. package/src/modes/components/transcript-container.ts +16 -0
  96. package/src/modes/components/ttsr-notification.ts +12 -0
  97. package/src/modes/controllers/event-controller.ts +3 -6
  98. package/src/modes/controllers/input-controller.ts +6 -6
  99. package/src/modes/controllers/selector-controller.ts +8 -6
  100. package/src/modes/interactive-mode.ts +5 -40
  101. package/src/modes/print-mode.ts +13 -54
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  104. package/src/modes/utils/ui-helpers.ts +17 -7
  105. package/src/prompts/advisor/active-repo-watchdog.md +3 -4
  106. package/src/prompts/advisor/advise-tool.md +3 -3
  107. package/src/prompts/advisor/context-files.md +1 -1
  108. package/src/prompts/advisor/system.md +45 -65
  109. package/src/prompts/agents/designer.md +44 -44
  110. package/src/prompts/agents/init.md +18 -18
  111. package/src/prompts/agents/librarian.md +31 -31
  112. package/src/prompts/agents/reviewer.md +34 -39
  113. package/src/prompts/agents/security-reviewer.md +3 -5
  114. package/src/prompts/agents/task.md +11 -12
  115. package/src/prompts/bench.md +2 -5
  116. package/src/prompts/ci-green-request.md +13 -15
  117. package/src/prompts/dry-balance-bench.md +5 -5
  118. package/src/prompts/goals/goal-budget-limit.md +4 -5
  119. package/src/prompts/goals/goal-continuation.md +11 -11
  120. package/src/prompts/goals/goal-mode-active.md +7 -7
  121. package/src/prompts/goals/goal-todo-context.md +2 -2
  122. package/src/prompts/goals/guided-goal-interview.md +18 -24
  123. package/src/prompts/memories/read-path.md +9 -9
  124. package/src/prompts/memories/stage_one_system.md +9 -11
  125. package/src/prompts/review-custom-request.md +8 -11
  126. package/src/prompts/review-headless-request.md +3 -10
  127. package/src/prompts/security/scan-coordinator.md +6 -5
  128. package/src/prompts/security/validate-request.md +4 -7
  129. package/src/prompts/skills/user-invocation.md +2 -2
  130. package/src/prompts/steering/parent-irc.md +1 -1
  131. package/src/prompts/steering/user-interjection.md +1 -3
  132. package/src/prompts/system/active-repo-context.md +4 -2
  133. package/src/prompts/system/agent-creation-architect.md +22 -37
  134. package/src/prompts/system/agent-creation-user.md +3 -3
  135. package/src/prompts/system/auto-continue.md +1 -1
  136. package/src/prompts/system/auto-thinking-difficulty-local.md +5 -7
  137. package/src/prompts/system/auto-thinking-difficulty.md +8 -10
  138. package/src/prompts/system/autolearn-guidance-learn.md +2 -1
  139. package/src/prompts/system/autolearn-guidance.md +5 -4
  140. package/src/prompts/system/autolearn-nudge-autocontinue.md +3 -3
  141. package/src/prompts/system/background-tan-dispatch.md +3 -3
  142. package/src/prompts/system/btw-user.md +2 -2
  143. package/src/prompts/system/commit-message-system.md +8 -6
  144. package/src/prompts/system/eager-task.md +3 -3
  145. package/src/prompts/system/empty-stop-retry.md +1 -1
  146. package/src/prompts/system/gemini-tool-call-reminder.md +5 -5
  147. package/src/prompts/system/interrupted-thinking.md +3 -3
  148. package/src/prompts/system/irc-autoreply.md +1 -1
  149. package/src/prompts/system/irc-incoming.md +3 -3
  150. package/src/prompts/system/manual-continue.md +3 -3
  151. package/src/prompts/system/mcp-xdev-guidance.md +2 -2
  152. package/src/prompts/system/memory-consolidation-system.md +2 -2
  153. package/src/prompts/system/mid-run-todo-nudge.md +1 -1
  154. package/src/prompts/system/orchestrate-notice.md +25 -25
  155. package/src/prompts/system/personalities/default.md +9 -9
  156. package/src/prompts/system/personalities/friendly.md +11 -11
  157. package/src/prompts/system/personalities/pragmatic.md +8 -8
  158. package/src/prompts/system/plan-mode-active.md +61 -64
  159. package/src/prompts/system/plan-mode-approved.md +10 -11
  160. package/src/prompts/system/plan-mode-compact-instructions.md +11 -11
  161. package/src/prompts/system/plan-mode-reference.md +5 -5
  162. package/src/prompts/system/plan-yolo-handoff.md +2 -2
  163. package/src/prompts/system/prewalk-checklist.md +5 -5
  164. package/src/prompts/system/prewalk-continue.md +1 -1
  165. package/src/prompts/system/prewalk-plan.md +8 -9
  166. package/src/prompts/system/project-prompt.md +11 -12
  167. package/src/prompts/system/recap-user.md +1 -1
  168. package/src/prompts/system/resolve-device-reminder.md +1 -1
  169. package/src/prompts/system/rewind-report.md +3 -3
  170. package/src/prompts/system/side-channel-no-tools.md +3 -1
  171. package/src/prompts/system/snapcompact-context-stub.md +1 -1
  172. package/src/prompts/system/snapcompact-system-frames-note.md +1 -1
  173. package/src/prompts/system/snapcompact-system-stub.md +1 -1
  174. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  175. package/src/prompts/system/speech-rewrite.md +11 -13
  176. package/src/prompts/system/subagent-async-pending.md +5 -5
  177. package/src/prompts/system/subagent-system-prompt.md +5 -15
  178. package/src/prompts/system/subagent-user-prompt.md +1 -1
  179. package/src/prompts/system/subagent-yield-reminder.md +12 -12
  180. package/src/prompts/system/system-prompt.md +110 -136
  181. package/src/prompts/system/tan-context-switch.md +8 -14
  182. package/src/prompts/system/task-label.md +3 -3
  183. package/src/prompts/system/thinking-loop-redirect.md +6 -6
  184. package/src/prompts/system/title-marker-instruction.md +2 -1
  185. package/src/prompts/system/title-system.md +3 -5
  186. package/src/prompts/system/ttsr-interrupt.md +3 -3
  187. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  188. package/src/prompts/system/ultrathink-notice.md +1 -1
  189. package/src/prompts/system/unexpected-stop-classifier.md +3 -3
  190. package/src/prompts/system/vibe-mode-active.md +15 -15
  191. package/src/prompts/system/web-search.md +16 -16
  192. package/src/prompts/system/workflow-notice.md +35 -33
  193. package/src/prompts/system/xdev-mount-notice.md +4 -4
  194. package/src/prompts/tools/apply-patch.md +22 -23
  195. package/src/prompts/tools/approve.md +5 -0
  196. package/src/prompts/tools/ask.md +10 -10
  197. package/src/prompts/tools/checkpoint.md +7 -7
  198. package/src/prompts/tools/computer.md +16 -16
  199. package/src/prompts/tools/github.md +12 -12
  200. package/src/prompts/tools/goal.md +8 -9
  201. package/src/prompts/tools/grep.md +5 -5
  202. package/src/prompts/tools/image-attachment-describe-system.md +6 -6
  203. package/src/prompts/tools/image-attachment-describe.md +3 -8
  204. package/src/prompts/tools/image-gen.md +4 -4
  205. package/src/prompts/tools/inspect-image-system.md +10 -10
  206. package/src/prompts/tools/inspect-image.md +10 -13
  207. package/src/prompts/tools/learn.md +4 -4
  208. package/src/prompts/tools/manage-skill.md +9 -6
  209. package/src/prompts/tools/memory-edit.md +8 -8
  210. package/src/prompts/tools/recall.md +4 -4
  211. package/src/prompts/tools/reflect.md +2 -2
  212. package/src/prompts/tools/replace.md +13 -11
  213. package/src/prompts/tools/retain.md +3 -4
  214. package/src/prompts/tools/rewind.md +7 -8
  215. package/src/prompts/tools/rewrite.md +12 -0
  216. package/src/prompts/tools/security-publish.md +5 -1
  217. package/src/prompts/tools/security-scan.md +10 -1
  218. package/src/prompts/tools/task-async-contract.md +7 -1
  219. package/src/prompts/tools/todo.md +27 -25
  220. package/src/prompts/tools/vibe-kill.md +2 -2
  221. package/src/prompts/tools/vibe-list.md +2 -2
  222. package/src/prompts/tools/vibe-send.md +6 -7
  223. package/src/prompts/tools/vibe-spawn.md +8 -6
  224. package/src/prompts/tools/web-search.md +5 -5
  225. package/src/sdk.ts +10 -1
  226. package/src/session/agent-session-types.ts +2 -0
  227. package/src/session/agent-session.ts +28 -0
  228. package/src/session/session-handoff.ts +21 -0
  229. package/src/session/session-tools.ts +47 -0
  230. package/src/tools/builtin-names.ts +1 -2
  231. package/src/tools/index.ts +10 -0
  232. package/src/tools/read-format.ts +8 -3
  233. package/src/tools/read.ts +18 -7
  234. package/src/tools/renderers.ts +2 -0
  235. package/src/tools/think.ts +84 -0
  236. package/src/tools/write.ts +1 -1
  237. package/src/utils/file-mentions.ts +8 -2
  238. package/src/utils/zip.ts +858 -94
  239. package/dist/types/cleanse/progress.d.ts +0 -14
@@ -21,6 +21,7 @@ import { usesCodexTaskPrompt } from "../task/prompt-policy";
21
21
  import { isMCPToolName, normalizeToolNames } from "../tools/builtin-names";
22
22
  import { computerExposureMode } from "../tools/computer/exposure";
23
23
  import { wrapToolWithMetaNotice } from "../tools/output-meta";
24
+ import { supportsExternalThinking } from "../tools/think";
24
25
  import { ToolAbortError, ToolError } from "../tools/tool-errors";
25
26
  import { isMountableUnderXdev, listXdevTools, type XdevState, xdevDocsFor, xdevEntries } from "../tools/xdev";
26
27
  import { type EditMode, resolveEditMode } from "../utils/edit-mode";
@@ -68,6 +69,8 @@ interface SessionToolsOptions {
68
69
  toolRegistry?: Map<string, AgentTool>;
69
70
  createVibeTools?: () => AgentTool[];
70
71
  createComputerTool?: () => Promise<AgentTool | null>;
72
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
73
+ createThinkTool?: () => Promise<AgentTool | null>;
71
74
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
72
75
  createInspectImageTool?: () => Promise<AgentTool | null>;
73
76
  builtInToolNames?: Iterable<string>;
@@ -184,6 +187,7 @@ export class SessionTools {
184
187
  #toolRegistry: Map<string, AgentTool>;
185
188
  #createVibeTools: (() => AgentTool[]) | undefined;
186
189
  #createComputerTool: SessionToolsOptions["createComputerTool"];
190
+ #createThinkTool: SessionToolsOptions["createThinkTool"];
187
191
  #createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
188
192
  #installedVibeToolNames = new Set<string>();
189
193
  #builtInToolNames: Set<string>;
@@ -241,6 +245,7 @@ export class SessionTools {
241
245
  this.#toolRegistry = options.toolRegistry ?? new Map();
242
246
  this.#createVibeTools = options.createVibeTools;
243
247
  this.#createComputerTool = options.createComputerTool;
248
+ this.#createThinkTool = options.createThinkTool;
244
249
  this.#createInspectImageTool = options.createInspectImageTool;
245
250
  this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
246
251
  this.#mcpManagerToolNames = new Set(options.mcpManagerToolNames ?? []);
@@ -1158,6 +1163,48 @@ export class SessionTools {
1158
1163
  });
1159
1164
  }
1160
1165
 
1166
+ /**
1167
+ * Session-scoped enable/disable for the private `think` scratchpad tool.
1168
+ *
1169
+ * Enabling constructs the tool once and refreshes the model's tool contract;
1170
+ * disabling removes it from the active set while preserving its registry entry.
1171
+ *
1172
+ * @returns false when enabling was requested but this session cannot build the tool.
1173
+ */
1174
+ setThinkToolEnabled(enabled: boolean): Promise<boolean> {
1175
+ return this.#setThinkToolActive(enabled && supportsExternalThinking(this.#host.model()));
1176
+ }
1177
+
1178
+ /** Reconciles the external scratchpad after the active model changes. */
1179
+ reconcileThinkTool(): Promise<boolean> {
1180
+ return this.#setThinkToolActive(
1181
+ this.#host.settings.get("externalThinking") && supportsExternalThinking(this.#host.model()),
1182
+ );
1183
+ }
1184
+
1185
+ #setThinkToolActive(enabled: boolean): Promise<boolean> {
1186
+ return this.runToolRegistryMutation(async () => {
1187
+ const active = this.getEnabledToolNames();
1188
+ if (!enabled) {
1189
+ if (active.includes("think")) {
1190
+ await this.#applyActiveToolsByName(active.filter(name => name !== "think"));
1191
+ }
1192
+ return true;
1193
+ }
1194
+ if (!this.#toolRegistry.has("think")) {
1195
+ const tool = await this.#createThinkTool?.();
1196
+ if (tool?.name !== "think") return false;
1197
+ const wrapped = this.#wrapRuntimeTool(tool);
1198
+ this.#toolRegistry.set(wrapped.name, wrapped);
1199
+ this.#builtInToolNames.add(wrapped.name);
1200
+ }
1201
+ if (!active.includes("think")) {
1202
+ await this.#applyActiveToolsByName([...active, "think"]);
1203
+ }
1204
+ return true;
1205
+ });
1206
+ }
1207
+
1161
1208
  /** Current effective inspect_image state for `/vision status`. */
1162
1209
  inspectImageState(): { mode: InspectImageMode; active: boolean; model: string | undefined } {
1163
1210
  const model = this.#host.model();
@@ -32,8 +32,7 @@ export const BUILTIN_TOOL_NAMES = [
32
32
 
33
33
  export type BuiltinToolName = (typeof BUILTIN_TOOL_NAMES)[number];
34
34
 
35
- /** Hidden built-ins: constructible and `--tools`-addressable, but never part of the default active set. */
36
- export const HIDDEN_TOOL_NAMES = ["yield", "goal"] as const;
35
+ export const HIDDEN_TOOL_NAMES = ["yield", "goal", "think"] as const;
37
36
 
38
37
  export type HiddenToolName = (typeof HIDDEN_TOOL_NAMES)[number];
39
38
 
@@ -62,6 +62,7 @@ import { wrapToolWithMetaNotice } from "./output-meta";
62
62
  import { ReadTool } from "./read";
63
63
  import type { PlanProposalHandler } from "./resolve";
64
64
  import { SecurityScanTool } from "./security-scan";
65
+ import { supportsExternalThinking, ThinkTool } from "./think";
65
66
  import { type TodoPhase, TodoTool } from "./todo";
66
67
  import { WriteTool } from "./write";
67
68
  import { isMountableUnderXdev, type XdevState } from "./xdev";
@@ -102,6 +103,7 @@ export * from "./report-tool-issue";
102
103
  export * from "./resolve";
103
104
  export * from "./review";
104
105
  export * from "./security-scan";
106
+ export * from "./think";
105
107
  export * from "./todo";
106
108
  export * from "./tts";
107
109
  export * from "./vibe";
@@ -444,6 +446,7 @@ export const BUILTIN_TOOLS: Record<BuiltinToolName, ToolFactory> = {
444
446
  };
445
447
 
446
448
  export const HIDDEN_TOOLS: Record<HiddenToolName, ToolFactory> = {
449
+ think: () => new ThinkTool(),
447
450
  yield: s => new YieldTool(s),
448
451
  goal: s => new GoalTool(s),
449
452
  };
@@ -464,6 +467,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
464
467
  : undefined;
465
468
  const goalEnabled = session.settings.get("goal.enabled");
466
469
  const goalModeActive = !restrictToolNames && goalEnabled && session.getGoalModeState?.()?.enabled === true;
470
+ const externalThinkingActive =
471
+ session.settings.get("externalThinking") && supportsExternalThinking(session.getActiveModel?.());
467
472
  if (goalModeActive && requestedTools && !requestedTools.includes("goal")) {
468
473
  requestedTools.push("goal");
469
474
  }
@@ -562,6 +567,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
562
567
  if (session.settings.get("memory.backend") === "mnemopi" && !requestedTools.includes("memory_edit")) {
563
568
  requestedTools.push("memory_edit");
564
569
  }
570
+ if (externalThinkingActive && !requestedTools.includes("think")) {
571
+ requestedTools.push("think");
572
+ }
565
573
  // Auto-learn tools are gated by `autolearn.enabled` but, like the memory
566
574
  // tools above, must also be force-included into an explicit requestedTools
567
575
  // list so a restricted top-level session whose controller/guidance is
@@ -604,6 +612,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
604
612
  if (name === "inspect_image") return isInspectImageToolActive(session);
605
613
  if (name === "web_search") return session.settings.get("web_search.enabled");
606
614
  if (name === "security_scan") return session.settings.get("security.enabled");
615
+ if (name === "think") return externalThinkingActive;
607
616
  if (name === "ask") return session.settings.get("ask.enabled");
608
617
  if (name === "browser") return session.settings.get("browser.enabled");
609
618
  if (name === "computer") return session.settings.get("computer.enabled");
@@ -650,6 +659,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
650
659
  ...Object.entries(BUILTIN_TOOLS)
651
660
  .filter(([name]) => isToolAllowed(name))
652
661
  .map(([name, factory]) => [name, factory] as const),
662
+ ...(externalThinkingActive ? ([["think", HIDDEN_TOOLS.think]] as const) : []),
653
663
  ...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
654
664
  ...(goalModeActive ? ([["goal", HIDDEN_TOOLS.goal]] as const) : []),
655
665
  ];
@@ -1,5 +1,10 @@
1
1
  import * as path from "node:path";
2
- import { formatHashlineHeader, formatNumberedLine, formatNumberedLines } from "@oh-my-pi/hashline";
2
+ import {
3
+ formatHashlineHeader,
4
+ formatNumberedLine,
5
+ formatNumberedLines,
6
+ splitAddressableFileLines,
7
+ } from "@oh-my-pi/hashline";
3
8
  import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
4
9
  import { canonicalSnapshotKey, getFileSnapshotStore, recordSeenLines } from "../edit/file-snapshot-store";
5
10
  import { normalizeToLF } from "../edit/normalize";
@@ -290,7 +295,7 @@ export function buildInMemoryTextResult(
290
295
  ): AgentToolResult<ReadToolDetails> {
291
296
  const displayMode = resolveFileDisplayMode(session, { raw: options.raw, immutable: options.immutable });
292
297
  const details = options.details ?? {};
293
- const allLines = text.split("\n");
298
+ const allLines = options.raw === true ? text.split("\n") : splitAddressableFileLines(text);
294
299
  const totalLines = allLines.length;
295
300
  details.totalLines = totalLines;
296
301
  // User-requested 0-indexed range start. Lines BEFORE this are leading
@@ -487,7 +492,7 @@ export function buildInMemoryMultiRangeResult(
487
492
  ): AgentToolResult<ReadToolDetails> {
488
493
  const displayMode = resolveFileDisplayMode(session, { raw: options.raw, immutable: options.immutable });
489
494
  const details = options.details ?? {};
490
- const allLines = text.split("\n");
495
+ const allLines = options.raw === true ? text.split("\n") : splitAddressableFileLines(text);
491
496
  const totalLines = allLines.length;
492
497
  details.totalLines = totalLines;
493
498
  const shouldAddHashLines = displayMode.hashLines;
package/src/tools/read.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { splitAddressableFileLines } from "@oh-my-pi/hashline";
3
4
  import { type } from "@oh-my-pi/omptype";
4
5
  import type {
5
6
  AgentTool,
@@ -119,12 +120,17 @@ const MAX_ARTIFACT_RAW_INLINE_BYTES = DEFAULT_MAX_BYTES;
119
120
  async function readBracketContextFullLines(absolutePath: string, fileSize: number): Promise<string[] | undefined> {
120
121
  if (fileSize > SNAPSHOT_MAX_BYTES) return undefined;
121
122
  try {
122
- return normalizeToLF(await Bun.file(absolutePath).text()).split("\n");
123
+ return splitAddressableFileLines(normalizeToLF(await Bun.file(absolutePath).text()));
123
124
  } catch {
124
125
  return undefined;
125
126
  }
126
127
  }
127
128
 
129
+ interface StreamFileLinesOptions {
130
+ includeTerminalNewline?: boolean;
131
+ stopScanAfterCollect?: boolean;
132
+ }
133
+
128
134
  async function streamLinesFromFile(
129
135
  filePath: string,
130
136
  startLine: number,
@@ -132,7 +138,7 @@ async function streamLinesFromFile(
132
138
  maxBytes: number,
133
139
  selectedLineLimit: number | null,
134
140
  signal?: AbortSignal,
135
- stopScanAfterCollect = false,
141
+ options: StreamFileLinesOptions = {},
136
142
  ): Promise<{
137
143
  lines: string[];
138
144
  totalFileLines: number;
@@ -141,9 +147,12 @@ async function streamLinesFromFile(
141
147
  firstLinePreview?: { text: string; bytes: number };
142
148
  firstLineByteLength?: number;
143
149
  selectedBytesTotal: number;
150
+ /** Whether the fully scanned source ended in a newline. */
151
+ hasTrailingNewline: boolean;
144
152
  /** False when `stopScanAfterCollect` cut the scan short — `totalFileLines` is then a lower bound. */
145
153
  reachedEof: boolean;
146
154
  }> {
155
+ const { includeTerminalNewline = false, stopScanAfterCollect = false } = options;
147
156
  const bufferChunk = Buffer.allocUnsafe(READ_CHUNK_SIZE);
148
157
  const collectedLines: string[] = [];
149
158
  let lineIndex = 0;
@@ -311,7 +320,7 @@ async function streamLinesFromFile(
311
320
  }
312
321
  }
313
322
 
314
- if (reachedEof && (endedWithNewline || currentLineLength > 0 || !sawAnyByte)) {
323
+ if (reachedEof && (currentLineLength > 0 || !sawAnyByte || (endedWithNewline && includeTerminalNewline))) {
315
324
  finalizeLine();
316
325
  }
317
326
 
@@ -330,6 +339,7 @@ async function streamLinesFromFile(
330
339
  firstLineByteLength,
331
340
  selectedBytesTotal,
332
341
  reachedEof,
342
+ hasTrailingNewline: reachedEof && endedWithNewline,
333
343
  };
334
344
  }
335
345
 
@@ -692,7 +702,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
692
702
  maxBytesForRead,
693
703
  maxLines,
694
704
  signal,
695
- fileSize > SNAPSHOT_MAX_BYTES, // giant file: collected ranges don't need an exact EOF line count
705
+ { includeTerminalNewline: rawSelector, stopScanAfterCollect: fileSize > SNAPSHOT_MAX_BYTES },
696
706
  );
697
707
  totalFileLines = streamResult.totalFileLines;
698
708
  collectedLines = streamResult.lines;
@@ -1256,7 +1266,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1256
1266
  maxBytesForRead,
1257
1267
  selectedLineLimit,
1258
1268
  undefined, // plain-file read: deterministic and fast, never abort mid-read
1259
- fileSize > SNAPSHOT_MAX_BYTES, // giant file: don't scan to EOF just for an exact line count
1269
+ { includeTerminalNewline: rawSelector, stopScanAfterCollect: fileSize > SNAPSHOT_MAX_BYTES },
1260
1270
  );
1261
1271
 
1262
1272
  const {
@@ -1267,6 +1277,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1267
1277
  firstLinePreview,
1268
1278
  firstLineByteLength,
1269
1279
  reachedEof,
1280
+ hasTrailingNewline,
1270
1281
  } = streamResult;
1271
1282
 
1272
1283
  // Check if offset is out of bounds - return graceful message instead of throwing
@@ -1347,7 +1358,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1347
1358
  const tag = isWholeFile
1348
1359
  ? getFileSnapshotStore(this.session).record(
1349
1360
  canonicalSnapshotKey(absolutePath),
1350
- normalizeToLF(collectedLines.join("\n")),
1361
+ normalizeToLF(`${collectedLines.join("\n")}${hasTrailingNewline ? "\n" : ""}`),
1351
1362
  )
1352
1363
  : await recordFileSnapshot(this.session, absolutePath);
1353
1364
  if (tag) {
@@ -1694,7 +1705,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1694
1705
  maxBytesForRead,
1695
1706
  selectedLineLimit,
1696
1707
  signal,
1697
- artifact.size > SNAPSHOT_MAX_BYTES,
1708
+ { includeTerminalNewline: rawSelector, stopScanAfterCollect: artifact.size > SNAPSHOT_MAX_BYTES },
1698
1709
  );
1699
1710
  const {
1700
1711
  lines: collectedLines,
@@ -27,6 +27,7 @@ import { inspectImageToolRenderer } from "./inspect-image-renderer";
27
27
  import { recallToolRenderer, reflectToolRenderer, retainToolRenderer } from "./memory-render";
28
28
  import { readToolRenderer } from "./read";
29
29
  import { resolveRenderer } from "./resolve";
30
+ import { thinkToolRenderer } from "./think";
30
31
  import { todoToolRenderer } from "./todo";
31
32
  import { createVibeToolRenderer } from "./vibe";
32
33
  import { writeToolRenderer } from "./write";
@@ -115,6 +116,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
115
116
  get task(): ToolRenderer {
116
117
  return taskToolRenderer as ToolRenderer;
117
118
  },
119
+ think: thinkToolRenderer as ToolRenderer,
118
120
  todo: todoToolRenderer as ToolRenderer,
119
121
  github: githubToolRenderer as ToolRenderer,
120
122
  goal: goalToolRenderer as ToolRenderer,
@@ -0,0 +1,84 @@
1
+ import { type } from "@oh-my-pi/omptype";
2
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
3
+ import type { Model } from "@oh-my-pi/pi-ai";
4
+ import { type Component, Markdown } from "@oh-my-pi/pi-tui";
5
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
6
+ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
7
+
8
+ /** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
9
+ export function supportsExternalThinking(model: Model | null | undefined): boolean {
10
+ if (!model) return false;
11
+ const requiresThinking =
12
+ model.api === "anthropic-messages" &&
13
+ model.compat !== undefined &&
14
+ "requiresThinkingEnabled" in model.compat &&
15
+ model.compat.requiresThinkingEnabled === true;
16
+ if (model.reasoning && (requiresThinking || (model.thinking?.requiresEffort && !model.thinking.suppressWhenOff))) {
17
+ return false;
18
+ }
19
+ if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
20
+ return !model.reasoning || model.thinking?.mode === "budget" || model.thinking?.suppressWhenOff === true;
21
+ }
22
+ return (
23
+ model.api === "openai-responses" ||
24
+ model.api === "azure-openai-responses" ||
25
+ model.api === "openai-codex-responses" ||
26
+ model.api === "anthropic-messages"
27
+ );
28
+ }
29
+
30
+ const thinkSchema = type({
31
+ thoughts: type("string").describe("private scratchpad; not shown to user"),
32
+ "+": "reject",
33
+ }).describe("private scratchpad; not shown to user");
34
+
35
+ type ThinkParams = typeof thinkSchema.infer;
36
+
37
+ export type ThinkRenderArgs = {
38
+ thoughts?: string;
39
+ };
40
+
41
+ export const thinkToolRenderer = {
42
+ inline: true,
43
+ renderCall(args: ThinkRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
44
+ const thoughts =
45
+ typeof args === "object" && args !== null && "thoughts" in args && typeof args.thoughts === "string"
46
+ ? args.thoughts
47
+ : "";
48
+ return new Markdown(thoughts, 1, 0, getMarkdownTheme(), {
49
+ color: (text: string) => uiTheme.fg("thinkingText", text),
50
+ italic: true,
51
+ });
52
+ },
53
+ renderResult(): Component {
54
+ return undefined as unknown as Component;
55
+ },
56
+ };
57
+
58
+ interface ThinkToolDetails {
59
+ recorded: true;
60
+ }
61
+
62
+ /** Records private scratchpad thoughts while native model reasoning is disabled. */
63
+ export class ThinkTool implements AgentTool<typeof thinkSchema, ThinkToolDetails> {
64
+ readonly name = "think";
65
+ readonly approval = "read" as const;
66
+ readonly label = "Think";
67
+ readonly summary = "Record private scratchpad thoughts";
68
+ readonly description = "private scratchpad; not shown to user";
69
+ readonly parameters = thinkSchema;
70
+ readonly strict = true;
71
+ readonly intent = "omit" as const;
72
+
73
+ async execute(_toolCallId: string, _params: ThinkParams): Promise<AgentToolResult<ThinkToolDetails>> {
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: "------",
79
+ },
80
+ ],
81
+ details: { recorded: true },
82
+ };
83
+ }
84
+ }
@@ -659,7 +659,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
659
659
  const entries = new Map<string, ArchiveMemberContent>();
660
660
  if (resolvedArchivePath.exists) {
661
661
  try {
662
- const existing = await readArchiveEntries({ bytes: await Bun.file(finalPath).bytes(), format });
662
+ const existing = await readArchiveEntries({ path: finalPath, format });
663
663
  for (const [entryPath, data] of existing) {
664
664
  entries.set(entryPath, data);
665
665
  }
@@ -7,7 +7,12 @@
7
7
  */
8
8
  import * as fs from "node:fs/promises";
9
9
  import path from "node:path";
10
- import { formatHashlineHeader, formatNumberedLines, type SnapshotStore } from "@oh-my-pi/hashline";
10
+ import {
11
+ formatHashlineHeader,
12
+ formatNumberedLines,
13
+ type SnapshotStore,
14
+ splitAddressableFileLines,
15
+ } from "@oh-my-pi/hashline";
11
16
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
12
17
  import type { ImageContent } from "@oh-my-pi/pi-ai";
13
18
  import { formatAge, formatBytes, isProbablyBinary, readImageMetadata } from "@oh-my-pi/pi-utils";
@@ -270,7 +275,8 @@ export async function generateFileMentionMessages(
270
275
  const content = await Bun.file(absolutePath).text();
271
276
  const snapshotStore = options?.useHashLines ? options.snapshotStore : undefined;
272
277
  const normalized = snapshotStore ? normalizeToLF(content) : content;
273
- let { output, lineCount } = buildTextOutput(normalized);
278
+ const displayText = snapshotStore ? splitAddressableFileLines(normalized).join("\n") : normalized;
279
+ let { output, lineCount } = buildTextOutput(displayText);
274
280
  if (snapshotStore) {
275
281
  const tag = snapshotStore.record(canonicalSnapshotKey(absolutePath), normalized);
276
282
  output = `${formatHashlineHeader(resolvedPath, tag)}\n${formatNumberedLines(output)}`;