@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.1

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 (191) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/dist/types/cli/file-processor.d.ts +1 -1
  3. package/dist/types/config/settings-schema.d.ts +45 -3
  4. package/dist/types/config/settings.d.ts +1 -1
  5. package/dist/types/debug/raw-sse.d.ts +2 -0
  6. package/dist/types/edit/file-read-cache.d.ts +15 -4
  7. package/dist/types/edit/index.d.ts +3 -8
  8. package/dist/types/edit/renderer.d.ts +1 -2
  9. package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
  10. package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
  11. package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
  12. package/dist/types/eval/js/shared/runtime.d.ts +14 -8
  13. package/dist/types/eval/py/executor.d.ts +1 -2
  14. package/dist/types/eval/py/kernel.d.ts +6 -0
  15. package/dist/types/eval/py/tool-bridge.d.ts +1 -5
  16. package/dist/types/eval/session-id.d.ts +3 -0
  17. package/dist/types/extensibility/extensions/types.d.ts +1 -3
  18. package/dist/types/hashline/anchors.d.ts +15 -9
  19. package/dist/types/hashline/constants.d.ts +0 -2
  20. package/dist/types/hashline/diff.d.ts +1 -2
  21. package/dist/types/hashline/executor.d.ts +52 -0
  22. package/dist/types/hashline/hash.d.ts +44 -93
  23. package/dist/types/hashline/index.d.ts +2 -1
  24. package/dist/types/hashline/input.d.ts +2 -9
  25. package/dist/types/hashline/recovery.d.ts +3 -9
  26. package/dist/types/hashline/tokenizer.d.ts +91 -0
  27. package/dist/types/hashline/types.d.ts +5 -7
  28. package/dist/types/modes/components/extensions/types.d.ts +0 -4
  29. package/dist/types/modes/types.d.ts +1 -0
  30. package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
  31. package/dist/types/sdk.d.ts +2 -0
  32. package/dist/types/session/agent-session.d.ts +11 -15
  33. package/dist/types/session/agent-storage.d.ts +11 -10
  34. package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
  35. package/dist/types/slash-commands/types.d.ts +0 -5
  36. package/dist/types/task/executor.d.ts +2 -0
  37. package/dist/types/tool-discovery/tool-index.d.ts +0 -50
  38. package/dist/types/tools/index.d.ts +2 -8
  39. package/dist/types/tools/match-line-format.d.ts +4 -4
  40. package/dist/types/tools/output-schema-validator.d.ts +64 -0
  41. package/dist/types/tools/review.d.ts +13 -0
  42. package/dist/types/tools/search-tool-bm25.d.ts +1 -1
  43. package/dist/types/tools/search.d.ts +4 -3
  44. package/dist/types/utils/edit-mode.d.ts +1 -1
  45. package/dist/types/web/kagi.d.ts +4 -2
  46. package/dist/types/web/parallel.d.ts +4 -3
  47. package/dist/types/web/scrapers/types.d.ts +2 -1
  48. package/dist/types/web/search/index.d.ts +12 -4
  49. package/dist/types/web/search/provider.d.ts +2 -1
  50. package/dist/types/web/search/providers/anthropic.d.ts +9 -4
  51. package/dist/types/web/search/providers/base.d.ts +34 -2
  52. package/dist/types/web/search/providers/brave.d.ts +8 -1
  53. package/dist/types/web/search/providers/codex.d.ts +13 -9
  54. package/dist/types/web/search/providers/exa.d.ts +10 -1
  55. package/dist/types/web/search/providers/gemini.d.ts +20 -23
  56. package/dist/types/web/search/providers/jina.d.ts +2 -1
  57. package/dist/types/web/search/providers/kagi.d.ts +4 -1
  58. package/dist/types/web/search/providers/kimi.d.ts +10 -1
  59. package/dist/types/web/search/providers/parallel.d.ts +3 -2
  60. package/dist/types/web/search/providers/perplexity.d.ts +5 -2
  61. package/dist/types/web/search/providers/searxng.d.ts +2 -1
  62. package/dist/types/web/search/providers/synthetic.d.ts +5 -8
  63. package/dist/types/web/search/providers/tavily.d.ts +11 -4
  64. package/dist/types/web/search/providers/utils.d.ts +8 -6
  65. package/dist/types/web/search/providers/zai.d.ts +12 -3
  66. package/package.json +7 -7
  67. package/src/cli/file-processor.ts +12 -2
  68. package/src/cli.ts +0 -8
  69. package/src/commands/commit.ts +8 -8
  70. package/src/config/prompt-templates.ts +6 -6
  71. package/src/config/settings-schema.ts +47 -3
  72. package/src/config/settings.ts +5 -5
  73. package/src/debug/raw-sse.ts +68 -3
  74. package/src/edit/file-read-cache.ts +68 -25
  75. package/src/edit/index.ts +6 -37
  76. package/src/edit/renderer.ts +9 -47
  77. package/src/edit/streaming.ts +43 -56
  78. package/src/eval/__tests__/shared-executors.test.ts +520 -0
  79. package/src/eval/js/context-manager.ts +64 -53
  80. package/src/eval/js/shared/local-module-loader.ts +265 -0
  81. package/src/eval/js/shared/prelude.txt +4 -0
  82. package/src/eval/js/shared/rewrite-imports.ts +85 -0
  83. package/src/eval/js/shared/runtime.ts +129 -86
  84. package/src/eval/js/worker-core.ts +23 -38
  85. package/src/eval/py/executor.ts +155 -84
  86. package/src/eval/py/kernel.ts +10 -1
  87. package/src/eval/py/prelude.py +22 -24
  88. package/src/eval/py/runner.py +203 -85
  89. package/src/eval/py/tool-bridge.ts +17 -10
  90. package/src/eval/session-id.ts +8 -0
  91. package/src/exec/bash-executor.ts +27 -16
  92. package/src/extensibility/extensions/runner.ts +0 -1
  93. package/src/extensibility/extensions/types.ts +1 -3
  94. package/src/hashline/anchors.ts +56 -65
  95. package/src/hashline/apply.ts +29 -31
  96. package/src/hashline/constants.ts +0 -3
  97. package/src/hashline/diff-preview.ts +4 -5
  98. package/src/hashline/diff.ts +30 -4
  99. package/src/hashline/execute.ts +91 -26
  100. package/src/hashline/executor.ts +239 -0
  101. package/src/hashline/grammar.lark +12 -10
  102. package/src/hashline/hash.ts +69 -114
  103. package/src/hashline/index.ts +2 -1
  104. package/src/hashline/input.ts +48 -41
  105. package/src/hashline/prefixes.ts +21 -11
  106. package/src/hashline/recovery.ts +63 -71
  107. package/src/hashline/stream.ts +2 -2
  108. package/src/hashline/tokenizer.ts +467 -0
  109. package/src/hashline/types.ts +6 -8
  110. package/src/internal-urls/docs-index.generated.ts +7 -7
  111. package/src/modes/components/extensions/types.ts +0 -5
  112. package/src/modes/components/session-observer-overlay.ts +11 -2
  113. package/src/modes/components/tree-selector.ts +10 -2
  114. package/src/modes/controllers/command-controller.ts +1 -3
  115. package/src/modes/controllers/extension-ui-controller.ts +10 -11
  116. package/src/modes/controllers/selector-controller.ts +5 -5
  117. package/src/modes/types.ts +4 -1
  118. package/src/modes/utils/ui-helpers.ts +4 -0
  119. package/src/prompts/agents/explore.md +1 -1
  120. package/src/prompts/tools/ast-edit.md +1 -1
  121. package/src/prompts/tools/ast-grep.md +1 -1
  122. package/src/prompts/tools/eval.md +1 -1
  123. package/src/prompts/tools/hashline.md +73 -94
  124. package/src/prompts/tools/read.md +4 -4
  125. package/src/prompts/tools/search.md +3 -3
  126. package/src/sdk.ts +17 -23
  127. package/src/session/agent-session.ts +59 -66
  128. package/src/session/agent-storage.ts +13 -14
  129. package/src/slash-commands/acp-builtins.ts +3 -3
  130. package/src/slash-commands/types.ts +0 -6
  131. package/src/task/executor.ts +26 -57
  132. package/src/task/index.ts +8 -4
  133. package/src/tool-discovery/tool-index.ts +0 -134
  134. package/src/tools/ast-edit.ts +36 -13
  135. package/src/tools/ast-grep.ts +45 -4
  136. package/src/tools/browser/tab-worker.ts +3 -2
  137. package/src/tools/eval.ts +2 -1
  138. package/src/tools/fetch.ts +23 -14
  139. package/src/tools/index.ts +2 -8
  140. package/src/tools/irc.ts +59 -5
  141. package/src/tools/match-line-format.ts +5 -7
  142. package/src/tools/output-schema-validator.ts +132 -0
  143. package/src/tools/read.ts +142 -31
  144. package/src/tools/review.ts +23 -0
  145. package/src/tools/search-tool-bm25.ts +3 -30
  146. package/src/tools/search.ts +48 -16
  147. package/src/tools/write.ts +3 -3
  148. package/src/tools/yield.ts +32 -41
  149. package/src/utils/edit-mode.ts +1 -2
  150. package/src/utils/file-mentions.ts +2 -2
  151. package/src/web/kagi.ts +15 -6
  152. package/src/web/parallel.ts +9 -6
  153. package/src/web/scrapers/types.ts +7 -1
  154. package/src/web/scrapers/youtube.ts +13 -7
  155. package/src/web/search/index.ts +37 -11
  156. package/src/web/search/provider.ts +5 -3
  157. package/src/web/search/providers/anthropic.ts +30 -21
  158. package/src/web/search/providers/base.ts +35 -2
  159. package/src/web/search/providers/brave.ts +4 -4
  160. package/src/web/search/providers/codex.ts +118 -89
  161. package/src/web/search/providers/exa.ts +3 -2
  162. package/src/web/search/providers/gemini.ts +58 -155
  163. package/src/web/search/providers/jina.ts +4 -4
  164. package/src/web/search/providers/kagi.ts +17 -11
  165. package/src/web/search/providers/kimi.ts +29 -13
  166. package/src/web/search/providers/parallel.ts +171 -23
  167. package/src/web/search/providers/perplexity.ts +38 -37
  168. package/src/web/search/providers/searxng.ts +3 -1
  169. package/src/web/search/providers/synthetic.ts +16 -19
  170. package/src/web/search/providers/tavily.ts +23 -18
  171. package/src/web/search/providers/utils.ts +11 -17
  172. package/src/web/search/providers/zai.ts +16 -8
  173. package/dist/types/hashline/parser.d.ts +0 -7
  174. package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
  175. package/dist/types/tools/vim.d.ts +0 -58
  176. package/dist/types/vim/buffer.d.ts +0 -41
  177. package/dist/types/vim/commands.d.ts +0 -6
  178. package/dist/types/vim/engine.d.ts +0 -47
  179. package/dist/types/vim/parser.d.ts +0 -3
  180. package/dist/types/vim/render.d.ts +0 -25
  181. package/dist/types/vim/types.d.ts +0 -182
  182. package/src/hashline/parser.ts +0 -246
  183. package/src/mcp/discoverable-tool-metadata.ts +0 -24
  184. package/src/prompts/tools/vim.md +0 -98
  185. package/src/tools/vim.ts +0 -949
  186. package/src/vim/buffer.ts +0 -309
  187. package/src/vim/commands.ts +0 -382
  188. package/src/vim/engine.ts +0 -2409
  189. package/src/vim/parser.ts +0 -134
  190. package/src/vim/render.ts +0 -252
  191. package/src/vim/types.ts +0 -197
package/src/task/index.ts CHANGED
@@ -558,6 +558,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
558
558
  const commitStyle = this.session.settings.get("task.isolation.commits");
559
559
  const maxConcurrency = this.session.settings.get("task.maxConcurrency");
560
560
  const taskDepth = this.session.taskDepth ?? 0;
561
+ const subagentLspEnabled = (this.session.enableLsp ?? true) && this.session.settings.get("task.enableLsp");
561
562
 
562
563
  if (isolationMode === "none" && "isolated" in params) {
563
564
  return {
@@ -843,6 +844,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
843
844
  file => path.basename(file.path).toLowerCase() !== "agents.md",
844
845
  );
845
846
  const promptTemplates = this.session.promptTemplates;
847
+ const parentEvalSessionId = this.session.getEvalSessionId?.() ?? undefined;
846
848
 
847
849
  // Initialize progress for all tasks
848
850
  for (let i = 0; i < tasksWithUniqueIds.length; i++) {
@@ -872,7 +874,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
872
874
  if (!isIsolated) {
873
875
  return runSubprocess({
874
876
  cwd: this.session.cwd,
875
- agent,
877
+ agent: effectiveAgent,
876
878
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
877
879
  assignment: task.assignment.trim(),
878
880
  context: sharedContext,
@@ -888,7 +890,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
888
890
  persistArtifacts: !!artifactsDir,
889
891
  artifactsDir: effectiveArtifactsDir,
890
892
  contextFile: contextFilePath,
891
- enableLsp: false,
893
+ enableLsp: subagentLspEnabled,
892
894
  signal,
893
895
  eventBus: this.session.eventBus,
894
896
  onProgress: progress => {
@@ -910,6 +912,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
910
912
  parentArtifactManager,
911
913
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
912
914
  parentTelemetry: this.session.getTelemetry?.(),
915
+ parentEvalSessionId,
913
916
  });
914
917
  }
915
918
 
@@ -927,7 +930,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
927
930
  const result = await runSubprocess({
928
931
  cwd: this.session.cwd,
929
932
  worktree: isolationDir,
930
- agent,
933
+ agent: effectiveAgent,
931
934
  task: renderSubagentUserPrompt(task.assignment, simpleMode),
932
935
  assignment: task.assignment.trim(),
933
936
  context: sharedContext,
@@ -943,7 +946,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
943
946
  persistArtifacts: !!artifactsDir,
944
947
  artifactsDir: effectiveArtifactsDir,
945
948
  contextFile: contextFilePath,
946
- enableLsp: false,
949
+ enableLsp: subagentLspEnabled,
947
950
  signal,
948
951
  eventBus: this.session.eventBus,
949
952
  onProgress: progress => {
@@ -965,6 +968,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
965
968
  parentArtifactManager,
966
969
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
967
970
  parentTelemetry: this.session.getTelemetry?.(),
971
+ parentEvalSessionId,
968
972
  });
969
973
  if (mergeMode === "branch" && result.exitCode === 0) {
970
974
  try {
@@ -44,47 +44,6 @@ export interface DiscoverableToolSearchResult {
44
44
  score: number;
45
45
  }
46
46
 
47
- // ─── Legacy MCP-typed aliases (back-compat) ──────────────────────────────────
48
-
49
- /** @deprecated Use DiscoverableTool with source === "mcp" */
50
- export type DiscoverableMCPTool = Pick<
51
- DiscoverableTool,
52
- "name" | "label" | "schemaKeys" | "serverName" | "mcpToolName"
53
- > & { description: string };
54
-
55
- /** @deprecated Use DiscoverableToolServerSummary */
56
- export type DiscoverableMCPToolServerSummary = DiscoverableToolServerSummary;
57
-
58
- /** @deprecated Use DiscoverableToolSummary */
59
- export type DiscoverableMCPToolSummary = DiscoverableToolSummary;
60
-
61
- /** Tool object stored on legacy MCP index documents. Carries both legacy `description` and the
62
- * generic `summary`/`source` so the legacy index is structurally assignable to
63
- * DiscoverableToolSearchIndex (search functions read termFrequencies, not the tool fields). */
64
- export type DiscoverableMCPSearchTool = DiscoverableTool & { description: string };
65
-
66
- /** @deprecated Use DiscoverableToolSearchDocument */
67
- export interface DiscoverableMCPSearchDocument {
68
- tool: DiscoverableMCPSearchTool;
69
- termFrequencies: Map<string, number>;
70
- length: number;
71
- }
72
-
73
- /** @deprecated Use DiscoverableToolSearchIndex.
74
- * Documents on this index expose `tool.description` (legacy MCP shape) while still being
75
- * searchable via `searchDiscoverableTools`. */
76
- export interface DiscoverableMCPSearchIndex {
77
- documents: DiscoverableMCPSearchDocument[];
78
- averageLength: number;
79
- documentFrequencies: Map<string, number>;
80
- }
81
-
82
- /** @deprecated Use DiscoverableToolSearchResult */
83
- export interface DiscoverableMCPSearchResult {
84
- tool: DiscoverableMCPSearchTool;
85
- score: number;
86
- }
87
-
88
47
  // ─── BM25 Constants ───────────────────────────────────────────────────────────
89
48
 
90
49
  const BM25_K1 = 1.2;
@@ -295,96 +254,3 @@ export function searchDiscoverableTools(
295
254
  .sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
296
255
  .slice(0, limit);
297
256
  }
298
-
299
- // ─── Legacy MCP-specific shims (back-compat wrappers) ────────────────────────
300
-
301
- /** @deprecated Use getDiscoverableTool */
302
- export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | null {
303
- if (!isMCPToolName(tool.name)) return null;
304
- const toolRecord = tool as AgentTool & {
305
- label?: string;
306
- description?: string;
307
- mcpServerName?: string;
308
- mcpToolName?: string;
309
- parameters?: unknown;
310
- };
311
- return {
312
- name: tool.name,
313
- label: typeof toolRecord.label === "string" ? toolRecord.label : tool.name,
314
- description: typeof toolRecord.description === "string" ? toolRecord.description : "",
315
- serverName: typeof toolRecord.mcpServerName === "string" ? toolRecord.mcpServerName : undefined,
316
- mcpToolName: typeof toolRecord.mcpToolName === "string" ? toolRecord.mcpToolName : undefined,
317
- schemaKeys: getSchemaPropertyKeys(toolRecord.parameters),
318
- };
319
- }
320
-
321
- /** @deprecated Use collectDiscoverableTools with source filter */
322
- export function collectDiscoverableMCPTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
323
- const discoverable: DiscoverableMCPTool[] = [];
324
- for (const tool of tools) {
325
- const metadata = getDiscoverableMCPTool(tool);
326
- if (metadata) {
327
- discoverable.push(metadata);
328
- }
329
- }
330
- return discoverable;
331
- }
332
-
333
- /** @deprecated Use selectDiscoverableToolNamesByServer */
334
- export function selectDiscoverableMCPToolNamesByServer(
335
- tools: Iterable<DiscoverableMCPTool>,
336
- serverNames: ReadonlySet<string>,
337
- ): string[] {
338
- if (serverNames.size === 0) return [];
339
- return Array.from(tools)
340
- .filter(tool => tool.serverName !== undefined && serverNames.has(tool.serverName))
341
- .map(tool => tool.name);
342
- }
343
-
344
- /** @deprecated Use summarizeDiscoverableTools */
345
- export function summarizeDiscoverableMCPTools(tools: DiscoverableMCPTool[]): DiscoverableMCPToolSummary {
346
- const serverToolCounts = new Map<string, number>();
347
- for (const tool of tools) {
348
- if (!tool.serverName) continue;
349
- serverToolCounts.set(tool.serverName, (serverToolCounts.get(tool.serverName) ?? 0) + 1);
350
- }
351
- const servers = Array.from(serverToolCounts.entries())
352
- .sort(([left], [right]) => left.localeCompare(right))
353
- .map(([name, toolCount]) => ({ name, toolCount }));
354
- return {
355
- servers,
356
- toolCount: tools.length,
357
- };
358
- }
359
-
360
- /** @deprecated Use buildDiscoverableToolSearchIndex.
361
- * Builds an index whose documents preserve the legacy `description` field on each tool while
362
- * also carrying the generic `summary` (set from `description`) so the index remains usable
363
- * with `searchDiscoverableTools`. */
364
- export function buildDiscoverableMCPSearchIndex(tools: Iterable<DiscoverableMCPTool>): DiscoverableMCPSearchIndex {
365
- const adapted: DiscoverableMCPSearchTool[] = Array.from(tools).map(t => ({
366
- name: t.name,
367
- label: t.label,
368
- description: t.description,
369
- summary: t.description,
370
- source: "mcp" as DiscoverableToolSource,
371
- serverName: t.serverName,
372
- mcpToolName: t.mcpToolName,
373
- schemaKeys: t.schemaKeys,
374
- }));
375
- const generic = buildDiscoverableToolSearchIndex(adapted);
376
- // Documents reference `adapted` tools (with `description`), so the cast is sound.
377
- return generic as unknown as DiscoverableMCPSearchIndex;
378
- }
379
-
380
- /** @deprecated Use searchDiscoverableTools */
381
- export function searchDiscoverableMCPTools(
382
- index: DiscoverableMCPSearchIndex | DiscoverableToolSearchIndex,
383
- query: string,
384
- limit: number,
385
- ): DiscoverableMCPSearchResult[] {
386
- return searchDiscoverableTools(index as DiscoverableToolSearchIndex, query, limit) as DiscoverableMCPSearchResult[];
387
- }
388
-
389
- /** @deprecated Use formatDiscoverableToolServerSummary */
390
- export const formatDiscoverableMCPToolServerSummary = formatDiscoverableToolServerSummary;
@@ -6,7 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import * as z from "zod/v4";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
- import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
9
+ import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
10
10
  import type { Theme } from "../modes/theme/theme";
11
11
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
12
12
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -257,12 +257,26 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
257
257
  }
258
258
 
259
259
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
260
+ const hashContexts = new Map<string, { fileHash: string }>();
261
+ if (useHashLines) {
262
+ for (const relativePath of fileList) {
263
+ const absolutePath = path.resolve(this.session.cwd, relativePath);
264
+ try {
265
+ const fullText = await Bun.file(absolutePath).text();
266
+ const fileHash = computeFileHash(fullText);
267
+ hashContexts.set(relativePath, { fileHash });
268
+ } catch {
269
+ // Best-effort: if a file disappears between ast-edit and rendering, emit plain line output.
270
+ }
271
+ }
272
+ }
260
273
  const outputLines: string[] = [];
261
274
  const displayLines: string[] = [];
262
275
  const renderChangesForFile = (relativePath: string): { model: string[]; display: string[] } => {
263
276
  const modelOut: string[] = [];
264
277
  const displayOut: string[] = [];
265
278
  const fileChanges = changesByFile.get(relativePath) ?? [];
279
+ const hashContext = hashContexts.get(relativePath);
266
280
  const lineNumberWidth = fileChanges.reduce(
267
281
  (width, change) => Math.max(width, String(change.startLine).length),
268
282
  0,
@@ -272,13 +286,9 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
272
286
  const afterFirstLine = change.after.split("\n", 1)[0] ?? "";
273
287
  const beforeLine = beforeFirstLine.slice(0, 120);
274
288
  const afterLine = afterFirstLine.slice(0, 120);
275
- const beforeRef = useHashLines
276
- ? `${change.startLine}${computeLineHash(change.startLine, beforeFirstLine)}`
277
- : `${change.startLine}:${change.startColumn}`;
278
- const afterRef = useHashLines
279
- ? `${change.startLine}${computeLineHash(change.startLine, afterFirstLine)}`
280
- : `${change.startLine}:${change.startColumn}`;
281
- const lineSeparator = useHashLines ? HL_BODY_SEP : " ";
289
+ const beforeRef = hashContext ? `${change.startLine}` : `${change.startLine}:${change.startColumn}`;
290
+ const afterRef = hashContext ? `${change.startLine}` : `${change.startLine}:${change.startColumn}`;
291
+ const lineSeparator = hashContext ? ":" : " ";
282
292
  modelOut.push(`-${beforeRef}${lineSeparator}${beforeLine}`);
283
293
  modelOut.push(`+${afterRef}${lineSeparator}${afterLine}`);
284
294
  displayOut.push(formatCodeFrameLine("-", change.startLine, beforeLine, lineNumberWidth));
@@ -291,10 +301,13 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
291
301
  const grouped = formatGroupedFiles(fileList, relativePath => {
292
302
  const rendered = renderChangesForFile(relativePath);
293
303
  const count = fileReplacementCounts.get(relativePath) ?? 0;
304
+ const hashContext = hashContexts.get(relativePath);
305
+ const hashSuffix = hashContext ? `#${hashContext.fileHash}` : "";
294
306
  return {
295
- headerSuffix: ` (${formatCount("replacement", count)})`,
307
+ headerSuffix: `${hashSuffix} (${formatCount("replacement", count)})`,
296
308
  modelLines: rendered.model,
297
309
  displayLines: rendered.display,
310
+ skip: rendered.model.length === 0,
298
311
  };
299
312
  });
300
313
  outputLines.push(...grouped.model);
@@ -302,6 +315,15 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
302
315
  } else {
303
316
  for (const relativePath of fileList) {
304
317
  const rendered = renderChangesForFile(relativePath);
318
+ if (rendered.model.length === 0) continue;
319
+ if (outputLines.length > 0) {
320
+ outputLines.push("");
321
+ displayLines.push("");
322
+ }
323
+ const hashContext = hashContexts.get(relativePath);
324
+ if (hashContext) {
325
+ outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
326
+ }
305
327
  outputLines.push(...rendered.model);
306
328
  displayLines.push(...rendered.display);
307
329
  }
@@ -499,11 +521,12 @@ export const astEditToolRenderer = {
499
521
  let contextDir = searchBase ?? "";
500
522
  return group.map(line => {
501
523
  if (line.startsWith("## ")) {
502
- // Strip ` (3 replacements)` suffix attached by formatGroupedFiles.
524
+ // Strip ` (3 replacements)` and `#hash` suffixes from formatGroupedFiles.
503
525
  const fileName = line
504
526
  .slice(3)
505
527
  .trimEnd()
506
- .replace(/\s+\([^)]*\)\s*$/, "");
528
+ .replace(/\s+\([^)]*\)\s*$/, "")
529
+ .replace(/#[0-9a-f]+$/, "");
507
530
  const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
508
531
  const styled = uiTheme.fg("dim", line);
509
532
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -514,14 +537,14 @@ export const astEditToolRenderer = {
514
537
  .trimEnd()
515
538
  .replace(/\s+\([^)]*\)\s*$/, "");
516
539
  const isDirectory = raw.endsWith("/");
517
- const name = raw.replace(/\/$/, "");
540
+ const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
518
541
  if (isDirectory) {
519
542
  if (searchBase) {
520
543
  contextDir = name === "." ? searchBase : path.join(searchBase, name);
521
544
  }
522
545
  return uiTheme.fg("accent", line);
523
546
  }
524
- // Root-level file with optional suffix, e.g. `# foo.ts (3 replacements)`.
547
+ // Root-level file with optional `#hash` and ` (3 replacements)` suffixes.
525
548
  const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
526
549
  const styled = uiTheme.fg("accent", line);
527
550
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -5,7 +5,9 @@ import type { Component } from "@oh-my-pi/pi-tui";
5
5
  import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import * as z from "zod/v4";
8
+ import { getFileReadCache } from "../edit/file-read-cache";
8
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import { computeFileHash, formatHashlineHeader } from "../hashline/hash";
9
11
  import type { Theme } from "../modes/theme/theme";
10
12
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
11
13
  import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -216,25 +218,43 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
216
218
  }
217
219
 
218
220
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
221
+ const hashContexts = new Map<string, { absolutePath: string; fileHash: string }>();
222
+ if (useHashLines) {
223
+ for (const relativePath of fileList) {
224
+ const absolutePath = path.resolve(this.session.cwd, relativePath);
225
+ try {
226
+ const fullText = await Bun.file(absolutePath).text();
227
+ const fileHash = computeFileHash(fullText);
228
+ hashContexts.set(relativePath, { absolutePath, fileHash });
229
+ } catch {
230
+ // Best-effort: if a file disappears between ast-grep and rendering, emit plain line output.
231
+ }
232
+ }
233
+ }
219
234
  const outputLines: string[] = [];
220
235
  const displayLines: string[] = [];
221
236
  const renderMatchesForFile = (relativePath: string): { model: string[]; display: string[] } => {
222
237
  const modelOut: string[] = [];
223
238
  const displayOut: string[] = [];
224
239
  const fileMatches = matchesByFile.get(relativePath) ?? [];
240
+ const hashContext = hashContexts.get(relativePath);
225
241
  const lineNumberWidth = fileMatches.reduce((width, match) => {
226
242
  const lineCount = match.text.split("\n").length;
227
243
  const endLine = match.startLine + lineCount - 1;
228
244
  return Math.max(width, String(match.startLine).length, String(endLine).length);
229
245
  }, 0);
246
+ const cacheEntries: Array<readonly [number, string]> = [];
230
247
  for (const match of fileMatches) {
231
248
  const matchLines = match.text.split("\n");
232
249
  for (let index = 0; index < matchLines.length; index++) {
233
250
  const lineNumber = match.startLine + index;
234
251
  const isMatch = index === 0;
235
252
  const line = matchLines[index] ?? "";
236
- modelOut.push(formatMatchLine(lineNumber, line, isMatch, { useHashLines }));
253
+ modelOut.push(
254
+ formatMatchLine(lineNumber, line, isMatch, { useHashLines: hashContext !== undefined }),
255
+ );
237
256
  displayOut.push(formatCodeFrameLine(isMatch ? "*" : " ", lineNumber, line, lineNumberWidth));
257
+ cacheEntries.push([lineNumber, line] as const);
238
258
  }
239
259
  if (match.metaVariables && Object.keys(match.metaVariables).length > 0) {
240
260
  const serializedMeta = Object.entries(match.metaVariables)
@@ -246,19 +266,39 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
246
266
  }
247
267
  fileMatchCounts.set(relativePath, (fileMatchCounts.get(relativePath) ?? 0) + 1);
248
268
  }
269
+ if (hashContext && cacheEntries.length > 0) {
270
+ getFileReadCache(this.session).recordSparse(hashContext.absolutePath, cacheEntries, {
271
+ fileHash: hashContext.fileHash,
272
+ });
273
+ }
249
274
  return { model: modelOut, display: displayOut };
250
275
  };
251
276
 
252
277
  if (isDirectory) {
253
278
  const grouped = formatGroupedFiles(fileList, relativePath => {
254
279
  const rendered = renderMatchesForFile(relativePath);
255
- return { modelLines: rendered.model, displayLines: rendered.display };
280
+ const hashContext = hashContexts.get(relativePath);
281
+ return {
282
+ modelLines: rendered.model,
283
+ displayLines: rendered.display,
284
+ headerSuffix: hashContext ? `#${hashContext.fileHash}` : "",
285
+ skip: rendered.model.length === 0,
286
+ };
256
287
  });
257
288
  outputLines.push(...grouped.model);
258
289
  displayLines.push(...grouped.display);
259
290
  } else {
260
291
  for (const relativePath of fileList) {
261
292
  const rendered = renderMatchesForFile(relativePath);
293
+ if (rendered.model.length === 0) continue;
294
+ if (outputLines.length > 0) {
295
+ outputLines.push("");
296
+ displayLines.push("");
297
+ }
298
+ const hashContext = hashContexts.get(relativePath);
299
+ if (hashContext) {
300
+ outputLines.push(formatHashlineHeader(relativePath, hashContext.fileHash));
301
+ }
262
302
  outputLines.push(...rendered.model);
263
303
  displayLines.push(...rendered.display);
264
304
  }
@@ -385,7 +425,8 @@ export const astGrepToolRenderer = {
385
425
  const fileName = line
386
426
  .slice(3)
387
427
  .trimEnd()
388
- .replace(/\s+\([^)]*\)\s*$/, "");
428
+ .replace(/\s+\([^)]*\)\s*$/, "")
429
+ .replace(/#[0-9a-f]+$/, "");
389
430
  const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
390
431
  const styled = uiTheme.fg("dim", line);
391
432
  return absPath ? fileHyperlink(absPath, styled) : styled;
@@ -396,7 +437,7 @@ export const astGrepToolRenderer = {
396
437
  .trimEnd()
397
438
  .replace(/\s+\([^)]*\)\s*$/, "");
398
439
  const isDirectory = raw.endsWith("/");
399
- const name = raw.replace(/\/$/, "");
440
+ const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
400
441
  if (isDirectory) {
401
442
  if (searchBase) {
402
443
  contextDir = name === "." ? searchBase : path.join(searchBase, name);
@@ -575,8 +575,10 @@ export class WorkerCore {
575
575
  if (signal.aborted) onCancel();
576
576
  else signal.addEventListener("abort", onCancel, { once: true });
577
577
  try {
578
+ const hooks = this.#hooksForActiveRun();
579
+ if (!hooks) throw new ToolError("Browser runtime started without an active run");
578
580
  const returnValue = await Promise.race([
579
- runtime.run(msg.code, `browser-run-${msg.id}.js`),
581
+ runtime.run(msg.code, `browser-run-${msg.id}.js`, hooks, { runId: msg.id, cwd: msg.session.cwd }),
580
582
  cancelRejection,
581
583
  ]);
582
584
  await this.#postReadyInfo();
@@ -601,7 +603,6 @@ export class WorkerCore {
601
603
  this.#runtime = new JsRuntime({
602
604
  initialCwd: session.cwd,
603
605
  sessionId: `browser-tab-${this.#targetId ?? "unknown"}`,
604
- getHooks: () => this.#hooksForActiveRun(),
605
606
  });
606
607
  return this.#runtime;
607
608
  }
package/src/tools/eval.ts CHANGED
@@ -6,6 +6,7 @@ import { prompt } from "@oh-my-pi/pi-utils";
6
6
  import * as z from "zod/v4";
7
7
  import { jsBackend, pythonBackend } from "../eval";
8
8
  import type { ExecutorBackend } from "../eval/backend";
9
+ import { defaultEvalSessionId } from "../eval/session-id";
9
10
  import type { EvalCellResult, EvalDisplayOutput, EvalLanguage, EvalStatusEvent, EvalToolDetails } from "../eval/types";
10
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
12
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
@@ -347,7 +348,7 @@ export class EvalTool implements AgentTool<typeof evalSchema> {
347
348
  pushUpdate();
348
349
  },
349
350
  });
350
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
351
+ const sessionId = session.getEvalSessionId?.() ?? defaultEvalSessionId(session);
351
352
 
352
353
  for (let i = 0; i < cells.length; i++) {
353
354
  const cell = cells[i];
@@ -10,6 +10,7 @@ import type { Settings } from "../config/settings";
10
10
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
11
  import { type Theme, theme } from "../modes/theme/theme";
12
12
  import type { ToolSession } from "../sdk";
13
+ import type { AgentStorage } from "../session/agent-storage";
13
14
  import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
14
15
  import { renderStatusLine } from "../tui";
15
16
  import { CachedOutputBlock } from "../tui/output-block";
@@ -542,7 +543,8 @@ async function renderHtmlToText(
542
543
  html: string,
543
544
  timeout: number,
544
545
  settings: Settings,
545
- userSignal?: AbortSignal,
546
+ userSignal: AbortSignal | undefined,
547
+ storage: AgentStorage | null,
546
548
  ): Promise<{ content: string; ok: boolean; method: string }> {
547
549
  const signal = ptree.combineSignals(userSignal, timeout * 1000);
548
550
  const execOptions = {
@@ -554,14 +556,18 @@ async function renderHtmlToText(
554
556
  };
555
557
 
556
558
  // Try Parallel extract first when credentials are configured
557
- if (settings.get("providers.parallelFetch") && (await findParallelApiKey())) {
559
+ if (settings.get("providers.parallelFetch") && findParallelApiKey(storage)) {
558
560
  try {
559
- const parallelResult = await extractWithParallel([url], {
560
- objective: "Extract the main content",
561
- excerpts: true,
562
- fullContent: false,
563
- signal,
564
- });
561
+ const parallelResult = await extractWithParallel(
562
+ [url],
563
+ {
564
+ objective: "Extract the main content",
565
+ excerpts: true,
566
+ fullContent: false,
567
+ signal,
568
+ },
569
+ storage,
570
+ );
565
571
  const firstDocument = parallelResult.results[0];
566
572
  if (firstDocument) {
567
573
  const content = getParallelExtractContent(firstDocument);
@@ -682,13 +688,14 @@ type FetchRenderResult = RenderResult & {
682
688
  async function handleSpecialUrls(
683
689
  url: string,
684
690
  timeout: number,
685
- signal?: AbortSignal,
691
+ signal: AbortSignal | undefined,
692
+ storage: AgentStorage | null,
686
693
  ): Promise<FetchRenderResult | null> {
687
694
  for (const handler of specialHandlers) {
688
695
  if (signal?.aborted) {
689
696
  throw new ToolAbortError();
690
697
  }
691
- const result = await handler(url, timeout, signal);
698
+ const result = await handler(url, timeout, signal, storage);
692
699
  if (result) return result;
693
700
  }
694
701
  return null;
@@ -706,7 +713,8 @@ async function renderUrl(
706
713
  timeout: number,
707
714
  raw: boolean,
708
715
  settings: Settings,
709
- signal?: AbortSignal,
716
+ signal: AbortSignal | undefined,
717
+ storage: AgentStorage | null,
710
718
  ): Promise<FetchRenderResult> {
711
719
  const notes: string[] = [];
712
720
  const fetchedAt = new Date().toISOString();
@@ -733,7 +741,7 @@ async function renderUrl(
733
741
 
734
742
  // Step 1: Try special handlers for known sites (unless raw mode)
735
743
  if (!raw) {
736
- const specialResult = await handleSpecialUrls(url, timeout, signal);
744
+ const specialResult = await handleSpecialUrls(url, timeout, signal, storage);
737
745
  if (specialResult) return specialResult;
738
746
  }
739
747
 
@@ -1051,7 +1059,7 @@ async function renderUrl(
1051
1059
  }
1052
1060
 
1053
1061
  // 5E: Render HTML with lynx or html2text
1054
- const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal);
1062
+ const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
1055
1063
  if (!htmlResult.ok) {
1056
1064
  notes.push("html rendering failed (lynx/html2text unavailable)");
1057
1065
  const output = finalizeOutput(rawContent);
@@ -1233,7 +1241,8 @@ async function buildReadUrlCacheEntry(
1233
1241
  throw new ToolAbortError();
1234
1242
  }
1235
1243
 
1236
- const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal);
1244
+ const storage = session.settings.getStorage();
1245
+ const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
1237
1246
  const output = buildUrlReadOutput(result, result.content);
1238
1247
  const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
1239
1248
 
@@ -91,7 +91,6 @@ export * from "./search";
91
91
  export * from "./search-tool-bm25";
92
92
  export * from "./ssh";
93
93
  export * from "./todo-write";
94
- export * from "./vim";
95
94
  export * from "./write";
96
95
  export * from "./yield";
97
96
 
@@ -104,7 +103,6 @@ export type ContextFileEntry = {
104
103
  depth?: number;
105
104
  };
106
105
 
107
- export type { DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
108
106
  export type {
109
107
  DiscoverableTool,
110
108
  DiscoverableToolSearchIndex,
@@ -140,6 +138,8 @@ export interface ToolSession {
140
138
  requireYieldTool?: boolean;
141
139
  /** Task recursion depth (0 = top-level, 1 = first child, etc.) */
142
140
  taskDepth?: number;
141
+ /** Get shared eval executor session ID. Subagents inherit this to share JS/Python state. */
142
+ getEvalSessionId?: () => string | null;
143
143
  /** Get session file */
144
144
  getSessionFile: () => string | null;
145
145
  /** Get eval kernel owner ID for session-scoped retained-kernel cleanup. */
@@ -194,12 +194,6 @@ export interface ToolSession {
194
194
  setTodoPhases?: (phases: TodoPhase[]) => void;
195
195
  /** Whether MCP tool discovery is active for this session. */
196
196
  isMCPDiscoveryEnabled?: () => boolean;
197
- /** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks.
198
- * @deprecated Use getDiscoverableTools with source filter instead. */
199
- getDiscoverableMCPTools?: () => import("../mcp/discoverable-tool-metadata").DiscoverableMCPTool[];
200
- /** Get the cached discoverable MCP search index for search_tool_bm25 execution.
201
- * @deprecated Use getDiscoverableToolSearchIndex instead. */
202
- getDiscoverableMCPSearchIndex?: () => import("../tool-discovery/tool-index").DiscoverableMCPSearchIndex;
203
197
  /** Get MCP tools activated by prior search_tool_bm25 calls. */
204
198
  getSelectedMCPToolNames?: () => string[];
205
199
  /** Merge MCP tool selections into the active session tool set. */