@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.2

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 (202) hide show
  1. package/CHANGELOG.md +266 -1
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/prompt-templates.ts +44 -226
  38. package/src/config/resolve-config-value.ts +4 -2
  39. package/src/config/settings-schema.ts +54 -2
  40. package/src/config/settings.ts +25 -26
  41. package/src/dap/client.ts +674 -0
  42. package/src/dap/config.ts +150 -0
  43. package/src/dap/defaults.json +211 -0
  44. package/src/dap/index.ts +4 -0
  45. package/src/dap/session.ts +1255 -0
  46. package/src/dap/types.ts +600 -0
  47. package/src/debug/log-viewer.ts +3 -2
  48. package/src/discovery/builtin.ts +1 -2
  49. package/src/discovery/codex.ts +2 -2
  50. package/src/discovery/github.ts +2 -1
  51. package/src/discovery/helpers.ts +2 -2
  52. package/src/discovery/opencode.ts +2 -2
  53. package/src/edit/diff.ts +818 -0
  54. package/src/edit/index.ts +309 -0
  55. package/src/edit/line-hash.ts +67 -0
  56. package/src/edit/modes/chunk.ts +454 -0
  57. package/src/{patch → edit/modes}/hashline.ts +741 -361
  58. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  59. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  60. package/src/{patch → edit}/normalize.ts +97 -76
  61. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  62. package/src/exec/bash-executor.ts +4 -2
  63. package/src/exec/idle-timeout-watchdog.ts +126 -0
  64. package/src/exec/non-interactive-env.ts +5 -0
  65. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  66. package/src/extensibility/custom-commands/bundled/review/index.ts +2 -2
  67. package/src/extensibility/custom-commands/loader.ts +1 -2
  68. package/src/extensibility/custom-tools/loader.ts +34 -11
  69. package/src/extensibility/extensions/loader.ts +9 -4
  70. package/src/extensibility/extensions/runner.ts +24 -1
  71. package/src/extensibility/extensions/types.ts +1 -1
  72. package/src/extensibility/hooks/loader.ts +5 -6
  73. package/src/extensibility/hooks/types.ts +1 -1
  74. package/src/extensibility/plugins/doctor.ts +2 -1
  75. package/src/extensibility/slash-commands.ts +3 -7
  76. package/src/index.ts +2 -1
  77. package/src/internal-urls/docs-index.generated.ts +11 -11
  78. package/src/ipy/executor.ts +58 -17
  79. package/src/ipy/gateway-coordinator.ts +6 -4
  80. package/src/ipy/kernel.ts +45 -22
  81. package/src/ipy/runtime.ts +2 -2
  82. package/src/lsp/client.ts +7 -4
  83. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  84. package/src/lsp/config.ts +2 -2
  85. package/src/lsp/defaults.json +688 -154
  86. package/src/lsp/index.ts +234 -45
  87. package/src/lsp/lspmux.ts +2 -2
  88. package/src/lsp/startup-events.ts +13 -0
  89. package/src/lsp/types.ts +12 -1
  90. package/src/lsp/utils.ts +8 -1
  91. package/src/main.ts +102 -46
  92. package/src/memories/index.ts +4 -5
  93. package/src/modes/acp/acp-agent.ts +563 -163
  94. package/src/modes/acp/acp-event-mapper.ts +9 -1
  95. package/src/modes/acp/acp-mode.ts +4 -2
  96. package/src/modes/components/agent-dashboard.ts +3 -4
  97. package/src/modes/components/diff.ts +6 -7
  98. package/src/modes/components/read-tool-group.ts +6 -12
  99. package/src/modes/components/settings-defs.ts +5 -0
  100. package/src/modes/components/tool-execution.ts +1 -1
  101. package/src/modes/components/welcome.ts +1 -1
  102. package/src/modes/controllers/btw-controller.ts +2 -2
  103. package/src/modes/controllers/command-controller.ts +3 -2
  104. package/src/modes/controllers/input-controller.ts +12 -8
  105. package/src/modes/index.ts +20 -2
  106. package/src/modes/interactive-mode.ts +94 -37
  107. package/src/modes/rpc/host-tools.ts +186 -0
  108. package/src/modes/rpc/rpc-client.ts +178 -13
  109. package/src/modes/rpc/rpc-mode.ts +73 -3
  110. package/src/modes/rpc/rpc-types.ts +53 -1
  111. package/src/modes/theme/theme.ts +80 -8
  112. package/src/modes/types.ts +2 -2
  113. package/src/prompts/system/system-prompt.md +2 -1
  114. package/src/prompts/tools/chunk-edit.md +219 -0
  115. package/src/prompts/tools/debug.md +43 -0
  116. package/src/prompts/tools/grep.md +3 -0
  117. package/src/prompts/tools/lsp.md +5 -5
  118. package/src/prompts/tools/read-chunk.md +17 -0
  119. package/src/prompts/tools/read.md +19 -5
  120. package/src/sdk.ts +190 -154
  121. package/src/secrets/obfuscator.ts +1 -1
  122. package/src/session/agent-session.ts +306 -256
  123. package/src/session/agent-storage.ts +12 -12
  124. package/src/session/compaction/branch-summarization.ts +3 -3
  125. package/src/session/compaction/compaction.ts +5 -6
  126. package/src/session/compaction/utils.ts +3 -3
  127. package/src/session/history-storage.ts +62 -19
  128. package/src/session/messages.ts +3 -3
  129. package/src/session/session-dump-format.ts +203 -0
  130. package/src/session/session-storage.ts +4 -2
  131. package/src/session/streaming-output.ts +1 -1
  132. package/src/session/tool-choice-queue.ts +213 -0
  133. package/src/slash-commands/builtin-registry.ts +56 -8
  134. package/src/ssh/connection-manager.ts +2 -2
  135. package/src/ssh/sshfs-mount.ts +5 -5
  136. package/src/stt/downloader.ts +4 -4
  137. package/src/stt/recorder.ts +4 -4
  138. package/src/stt/transcriber.ts +2 -2
  139. package/src/system-prompt.ts +21 -13
  140. package/src/task/agents.ts +5 -6
  141. package/src/task/commands.ts +2 -5
  142. package/src/task/executor.ts +4 -4
  143. package/src/task/index.ts +3 -4
  144. package/src/task/template.ts +2 -2
  145. package/src/task/worktree.ts +4 -4
  146. package/src/tools/ask.ts +2 -3
  147. package/src/tools/ast-edit.ts +7 -7
  148. package/src/tools/ast-grep.ts +7 -7
  149. package/src/tools/auto-generated-guard.ts +36 -41
  150. package/src/tools/await-tool.ts +2 -2
  151. package/src/tools/bash.ts +5 -23
  152. package/src/tools/browser.ts +4 -5
  153. package/src/tools/calculator.ts +2 -3
  154. package/src/tools/cancel-job.ts +2 -2
  155. package/src/tools/checkpoint.ts +3 -3
  156. package/src/tools/debug.ts +1007 -0
  157. package/src/tools/exit-plan-mode.ts +2 -3
  158. package/src/tools/fetch.ts +67 -3
  159. package/src/tools/find.ts +4 -5
  160. package/src/tools/fs-cache-invalidation.ts +5 -0
  161. package/src/tools/gemini-image.ts +13 -5
  162. package/src/tools/gh.ts +10 -11
  163. package/src/tools/grep.ts +57 -9
  164. package/src/tools/index.ts +44 -22
  165. package/src/tools/inspect-image.ts +4 -4
  166. package/src/tools/output-meta.ts +1 -1
  167. package/src/tools/python.ts +19 -6
  168. package/src/tools/read.ts +198 -67
  169. package/src/tools/render-mermaid.ts +2 -3
  170. package/src/tools/render-utils.ts +20 -6
  171. package/src/tools/renderers.ts +3 -1
  172. package/src/tools/report-tool-issue.ts +80 -0
  173. package/src/tools/resolve.ts +70 -39
  174. package/src/tools/search-tool-bm25.ts +2 -2
  175. package/src/tools/ssh.ts +2 -2
  176. package/src/tools/todo-write.ts +2 -2
  177. package/src/tools/tool-timeouts.ts +1 -0
  178. package/src/tools/write.ts +5 -6
  179. package/src/tui/tree-list.ts +3 -1
  180. package/src/utils/clipboard.ts +80 -0
  181. package/src/utils/commit-message-generator.ts +2 -3
  182. package/src/utils/edit-mode.ts +49 -0
  183. package/src/utils/file-display-mode.ts +6 -5
  184. package/src/utils/file-mentions.ts +8 -7
  185. package/src/utils/git.ts +4 -4
  186. package/src/utils/image-loading.ts +98 -0
  187. package/src/utils/title-generator.ts +2 -3
  188. package/src/utils/tools-manager.ts +6 -6
  189. package/src/web/scrapers/choosealicense.ts +1 -1
  190. package/src/web/search/index.ts +3 -3
  191. package/src/autoresearch/command-initialize.md +0 -34
  192. package/src/patch/diff.ts +0 -433
  193. package/src/patch/index.ts +0 -888
  194. package/src/patch/parser.ts +0 -532
  195. package/src/patch/types.ts +0 -292
  196. package/src/prompts/agents/oracle.md +0 -77
  197. package/src/tools/pending-action.ts +0 -49
  198. package/src/utils/child-process.ts +0 -88
  199. package/src/utils/frontmatter.ts +0 -117
  200. package/src/utils/image-input.ts +0 -274
  201. package/src/utils/mime.ts +0 -53
  202. package/src/utils/prompt-format.ts +0 -170
@@ -337,7 +337,7 @@ export function formatTruncationMetaNotice(truncation: TruncationMeta): string {
337
337
  }
338
338
 
339
339
  if (truncation.nextOffset != null) {
340
- notice += `. Use offset=${truncation.nextOffset} to continue`;
340
+ notice += `. Use sel=L${truncation.nextOffset} to continue`;
341
341
  }
342
342
 
343
343
  if (truncation.artifactId != null) {
@@ -4,11 +4,10 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
4
4
  import type { ImageContent } from "@oh-my-pi/pi-ai";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Markdown, Text } from "@oh-my-pi/pi-tui";
7
- import { getProjectDir } from "@oh-my-pi/pi-utils";
7
+ import { getProjectDir, prompt } from "@oh-my-pi/pi-utils";
8
8
  import { type Static, Type } from "@sinclair/typebox";
9
- import { renderPromptTemplate } from "../config/prompt-templates";
10
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
11
- import { executePython, getPreludeDocs, type PythonExecutorOptions } from "../ipy/executor";
10
+ import { executePython, getPreludeDocs, type PythonExecutorOptions, warmPythonEnvironment } from "../ipy/executor";
12
11
  import type { PreludeHelper, PythonStatusEvent } from "../ipy/kernel";
13
12
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
14
13
  import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
@@ -136,7 +135,7 @@ function renderJsonTree(value: unknown, theme: Theme, expanded: boolean, maxDept
136
135
  export function getPythonToolDescription(): string {
137
136
  const helpers = getPreludeDocs();
138
137
  const categories = groupPreludeHelpers(helpers);
139
- return renderPromptTemplate(pythonDescription, { categories });
138
+ return prompt.render(pythonDescription, { categories });
140
139
  }
141
140
 
142
141
  export interface PythonToolOptions {
@@ -146,7 +145,9 @@ export interface PythonToolOptions {
146
145
  export class PythonTool implements AgentTool<typeof pythonSchema> {
147
146
  readonly name = "python";
148
147
  readonly label = "Python";
149
- readonly description: string;
148
+ get description(): string {
149
+ return getPythonToolDescription();
150
+ }
150
151
  readonly parameters = pythonSchema;
151
152
  readonly concurrency = "exclusive";
152
153
  readonly strict = true;
@@ -158,7 +159,6 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
158
159
  options?: PythonToolOptions,
159
160
  ) {
160
161
  this.#proxyExecutor = options?.proxyExecutor;
161
- this.description = getPythonToolDescription();
162
162
  }
163
163
 
164
164
  async execute(
@@ -266,6 +266,19 @@ export class PythonTool implements AgentTool<typeof pythonSchema> {
266
266
  },
267
267
  });
268
268
  const sessionId = sessionFile ? `session:${sessionFile}:cwd:${commandCwd}` : `cwd:${commandCwd}`;
269
+
270
+ if (getPreludeDocs().length === 0) {
271
+ const warmup = await warmPythonEnvironment(
272
+ commandCwd,
273
+ sessionId,
274
+ this.session.settings.get("python.sharedGateway"),
275
+ sessionFile ?? undefined,
276
+ );
277
+ if (!warmup.ok) {
278
+ throw new ToolError(warmup.reason ?? "Python prelude helpers unavailable");
279
+ }
280
+ }
281
+
269
282
  const baseExecutorOptions: Omit<PythonExecutorOptions, "reset"> = {
270
283
  cwd: commandCwd,
271
284
  deadlineMs,
package/src/tools/read.ts CHANGED
@@ -5,15 +5,22 @@ import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
5
5
  import { glob } from "@oh-my-pi/pi-natives";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
7
  import { Text } from "@oh-my-pi/pi-tui";
8
- import { getRemoteDir, untilAborted } from "@oh-my-pi/pi-utils";
8
+ import { getRemoteDir, prompt, readImageMetadata, untilAborted } from "@oh-my-pi/pi-utils";
9
9
  import { type Static, Type } from "@sinclair/typebox";
10
- import { renderPromptTemplate } from "../config/prompt-templates";
10
+ import { computeLineHash } from "../edit/line-hash";
11
+ import {
12
+ type ChunkReadTarget,
13
+ formatChunkedRead,
14
+ parseChunkReadPath,
15
+ parseChunkSelector,
16
+ resolveAnchorStyle,
17
+ } from "../edit/modes/chunk";
11
18
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
19
  import { parseInternalUrl } from "../internal-urls/parse";
13
20
  import type { InternalUrl } from "../internal-urls/types";
14
21
  import { getLanguageFromPath, type Theme } from "../modes/theme/theme";
15
- import { computeLineHash } from "../patch/hashline";
16
22
  import readDescription from "../prompts/tools/read.md" with { type: "text" };
23
+ import readChunkDescription from "../prompts/tools/read-chunk.md" with { type: "text" };
17
24
  import type { ToolSession } from "../sdk";
18
25
  import {
19
26
  DEFAULT_MAX_BYTES,
@@ -25,20 +32,17 @@ import {
25
32
  } from "../session/streaming-output";
26
33
  import { renderCodeCell, renderStatusLine } from "../tui";
27
34
  import { CachedOutputBlock } from "../tui/output-block";
35
+ import { resolveEditMode } from "../utils/edit-mode";
28
36
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
29
- import {
30
- ImageInputTooLargeError,
31
- loadImageInput,
32
- MAX_IMAGE_INPUT_BYTES,
33
- readImageMetadata,
34
- } from "../utils/image-input";
37
+ import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "../utils/image-loading";
35
38
  import { convertFileWithMarkit } from "../utils/markit";
36
- import { detectSupportedImageMimeTypeFromFile } from "../utils/mime";
37
39
  import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
40
+
38
41
  import {
39
42
  executeReadUrl,
40
43
  isReadableUrlPath,
41
44
  loadReadUrlCacheEntry,
45
+ parseReadUrlTarget,
42
46
  type ReadUrlToolDetails,
43
47
  renderReadUrlCall,
44
48
  renderReadUrlResult,
@@ -50,19 +54,14 @@ import { formatAge, formatBytes, shortenPath, wrapBrackets } from "./render-util
50
54
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
51
55
  import { toolResult } from "./tool-result";
52
56
 
57
+ const PROSE_LANGUAGES = new Set(["markdown", "text", "log", "asciidoc", "restructuredtext"]);
58
+
59
+ function isProseLanguage(language: string | undefined): boolean {
60
+ return language !== undefined && PROSE_LANGUAGES.has(language);
61
+ }
62
+
53
63
  // Document types converted to markdown via markit.
54
- const CONVERTIBLE_EXTENSIONS = new Set([
55
- ".pdf",
56
- ".doc",
57
- ".docx",
58
- ".ppt",
59
- ".pptx",
60
- ".xls",
61
- ".xlsx",
62
- ".rtf",
63
- ".epub",
64
- ".ipynb",
65
- ]);
64
+ const CONVERTIBLE_EXTENSIONS = new Set([".pdf", ".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx", ".rtf", ".epub"]);
66
65
 
67
66
  // Remote mount path prefix (sshfs mounts) - skip fuzzy matching to avoid hangs
68
67
  const REMOTE_MOUNT_PREFIX = getRemoteDir() + path.sep;
@@ -354,10 +353,8 @@ function prependSuffixResolutionNotice(text: string, suffixResolution?: { from:
354
353
 
355
354
  const readSchema = Type.Object({
356
355
  path: Type.String({ description: "Path or URL to read" }),
357
- offset: Type.Optional(Type.Number({ description: "Line number to start from (1-indexed)" })),
358
- limit: Type.Optional(Type.Number({ description: "Maximum number of lines" })),
356
+ sel: Type.Optional(Type.String({ description: "Selector: chunk path, L10-L50, or raw" })),
359
357
  timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (default: 20)" })),
360
- raw: Type.Optional(Type.Boolean({ description: "If set, returns raw content without transformations" })),
361
358
  });
362
359
 
363
360
  export type ReadToolInput = Static<typeof readSchema>;
@@ -368,6 +365,7 @@ export interface ReadToolDetails {
368
365
  isDirectory?: boolean;
369
366
  resolvedPath?: string;
370
367
  suffixResolution?: { from: string; to: string };
368
+ chunk?: ChunkReadTarget;
371
369
  url?: string;
372
370
  finalUrl?: string;
373
371
  contentType?: string;
@@ -378,6 +376,43 @@ export interface ReadToolDetails {
378
376
 
379
377
  type ReadParams = ReadToolInput;
380
378
 
379
+ /** Parsed representation of the `sel` parameter. */
380
+ type ParsedSelector =
381
+ | { kind: "none" }
382
+ | { kind: "raw" }
383
+ | { kind: "lines"; startLine: number; endLine: number | undefined }
384
+ | { kind: "chunk"; selector: string };
385
+
386
+ const LINE_RANGE_RE = /^L(\d+)(?:-L?(\d+))?$/i;
387
+
388
+ function parseSel(sel: string | undefined): ParsedSelector {
389
+ if (!sel || sel.length === 0) return { kind: "none" };
390
+ const normalizedSelector = parseChunkSelector(sel).selector ?? sel;
391
+ if (normalizedSelector === "raw") return { kind: "raw" };
392
+ const lineMatch = LINE_RANGE_RE.exec(normalizedSelector);
393
+ if (lineMatch) {
394
+ const rawStart = Number.parseInt(lineMatch[1]!, 10);
395
+ if (rawStart < 1) {
396
+ throw new ToolError("L0 is invalid; lines are 1-indexed. Use sel=L1.");
397
+ }
398
+ const rawEnd = lineMatch[2] ? Number.parseInt(lineMatch[2], 10) : undefined;
399
+ if (rawEnd !== undefined && rawEnd < rawStart) {
400
+ throw new ToolError(`Invalid range L${rawStart}-L${rawEnd}: end must be >= start.`);
401
+ }
402
+ return { kind: "lines", startLine: rawStart, endLine: rawEnd };
403
+ }
404
+ return { kind: "chunk", selector: normalizedSelector };
405
+ }
406
+
407
+ /** Convert a line-range selector to the offset/limit pair used by internal pagination. */
408
+ function selToOffsetLimit(parsed: ParsedSelector): { offset?: number; limit?: number } {
409
+ if (parsed.kind === "lines") {
410
+ const limit = parsed.endLine !== undefined ? parsed.endLine - parsed.startLine + 1 : undefined;
411
+ return { offset: parsed.startLine, limit };
412
+ }
413
+ return {};
414
+ }
415
+
381
416
  interface ResolvedArchiveReadPath {
382
417
  absolutePath: string;
383
418
  archiveSubPath: string;
@@ -410,12 +445,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
410
445
  Math.min(session.settings.get("read.defaultLimit") ?? DEFAULT_MAX_LINES, DEFAULT_MAX_LINES),
411
446
  );
412
447
  this.#inspectImageEnabled = session.settings.get("inspect_image.enabled");
413
- this.description = renderPromptTemplate(readDescription, {
414
- DEFAULT_LIMIT: String(this.#defaultLimit),
415
- DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
416
- IS_HASHLINE_MODE: displayMode.hashLines,
417
- IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
418
- });
448
+ this.description =
449
+ resolveEditMode(session) === "chunk"
450
+ ? prompt.render(readChunkDescription, {
451
+ anchorStyle: resolveAnchorStyle(session.settings),
452
+ })
453
+ : prompt.render(readDescription, {
454
+ DEFAULT_LIMIT: String(this.#defaultLimit),
455
+ DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
456
+ IS_HASHLINE_MODE: displayMode.hashLines,
457
+ IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
458
+ });
419
459
  }
420
460
 
421
461
  async #resolveArchiveReadPath(readPath: string, signal?: AbortSignal): Promise<ResolvedArchiveReadPath | null> {
@@ -496,10 +536,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
496
536
  const suggestion =
497
537
  allLines.length === 0
498
538
  ? `The ${options.entityLabel} is empty.`
499
- : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`;
539
+ : `Use sel=L1 to read from the start, or sel=L${allLines.length} to read the last line.`;
500
540
  return resultBuilder
501
541
  .text(
502
- `Offset ${offset} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
542
+ `Line ${startLineDisplay} is beyond end of ${options.entityLabel} (${allLines.length} lines total). ${suggestion}`,
503
543
  )
504
544
  .done();
505
545
  }
@@ -556,7 +596,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
556
596
  const nextOffset = startLine + userLimitedLines + 1;
557
597
 
558
598
  outputText = formatText(selectedContent, startLineDisplay);
559
- outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use offset=${nextOffset} to continue]`;
599
+ outputText += `\n\n[${remaining} more lines in ${options.entityLabel}. Use sel=L${nextOffset} to continue]`;
560
600
  } else {
561
601
  outputText = formatText(truncation.content, startLineDisplay);
562
602
  }
@@ -674,42 +714,60 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
674
714
  _onUpdate?: AgentToolUpdateCallback<ReadToolDetails>,
675
715
  _toolContext?: AgentToolContext,
676
716
  ): Promise<AgentToolResult<ReadToolDetails>> {
677
- let { path: readPath, offset, limit, timeout, raw } = params;
678
- const displayMode = resolveFileDisplayMode(this.session);
717
+ let { path: readPath, sel, timeout } = params;
679
718
  if (readPath.startsWith("file://")) {
680
719
  readPath = expandPath(readPath);
681
720
  }
721
+ const displayMode = resolveFileDisplayMode(this.session);
722
+ const chunkMode = resolveEditMode(this.session) === "chunk";
682
723
 
683
724
  // Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://)
684
725
  const internalRouter = this.session.internalRouter;
685
726
  if (internalRouter?.canHandle(readPath)) {
727
+ const parsed = parseSel(sel);
728
+ const { offset, limit } = selToOffsetLimit(parsed);
686
729
  return this.#handleInternalUrl(readPath, offset, limit);
687
730
  }
688
731
 
689
- if (isReadableUrlPath(readPath)) {
732
+ const parsedUrlTarget = parseReadUrlTarget(readPath, sel);
733
+ if (parsedUrlTarget) {
690
734
  if (!this.session.settings.get("fetch.enabled")) {
691
735
  throw new ToolError("URL reads are disabled by settings.");
692
736
  }
693
- if (offset !== undefined || limit !== undefined) {
694
- const cached = await loadReadUrlCacheEntry(this.session, { path: readPath, timeout, raw }, signal, {
695
- ensureArtifact: true,
696
- preferCached: true,
697
- });
698
- return this.#buildInMemoryTextResult(cached.output, offset, limit, {
737
+ if (parsedUrlTarget.offset !== undefined || parsedUrlTarget.limit !== undefined) {
738
+ const cached = await loadReadUrlCacheEntry(
739
+ this.session,
740
+ { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw },
741
+ signal,
742
+ {
743
+ ensureArtifact: true,
744
+ preferCached: true,
745
+ },
746
+ );
747
+ return this.#buildInMemoryTextResult(cached.output, parsedUrlTarget.offset, parsedUrlTarget.limit, {
699
748
  details: { ...cached.details },
700
749
  sourceUrl: cached.details.finalUrl,
701
750
  entityLabel: "URL output",
702
751
  });
703
752
  }
704
- return executeReadUrl(this.session, { path: readPath, timeout, raw }, signal);
753
+ return executeReadUrl(this.session, { path: parsedUrlTarget.path, timeout, raw: parsedUrlTarget.raw }, signal);
705
754
  }
706
755
 
707
- const archivePath = await this.#resolveArchiveReadPath(readPath, signal);
756
+ const parsedReadPath = chunkMode ? parseChunkReadPath(readPath) : { filePath: readPath };
757
+ const localReadPath = parsedReadPath.filePath;
758
+ const pathSelectorParsed = chunkMode ? parseSel(parsedReadPath.selector) : { kind: "none" as const };
759
+ const pathChunkSelector = pathSelectorParsed.kind === "chunk" ? pathSelectorParsed.selector : undefined;
760
+ const selectorInput = sel ?? parsedReadPath.selector;
761
+ const rawSelectorInput = sel ?? parsedReadPath.selector;
762
+ const parsed = parseSel(selectorInput);
763
+
764
+ const archivePath = await this.#resolveArchiveReadPath(localReadPath, signal);
708
765
  if (archivePath) {
766
+ const { offset, limit } = selToOffsetLimit(parsed);
709
767
  return this.#readArchive(readPath, offset, limit, archivePath, signal);
710
768
  }
711
769
 
712
- let absolutePath = resolveReadPath(readPath, this.session.cwd);
770
+ let absolutePath = resolveReadPath(localReadPath, this.session.cwd);
713
771
  let suffixResolution: { from: string; to: string } | undefined;
714
772
 
715
773
  let isDirectory = false;
@@ -722,14 +780,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
722
780
  if (isNotFoundError(error)) {
723
781
  // Attempt unique suffix resolution before falling back to fuzzy suggestions
724
782
  if (!isRemoteMountPath(absolutePath)) {
725
- const suffixMatch = await findUniqueSuffixMatch(readPath, this.session.cwd, signal);
783
+ const suffixMatch = await findUniqueSuffixMatch(localReadPath, this.session.cwd, signal);
726
784
  if (suffixMatch) {
727
785
  try {
728
786
  const retryStat = await Bun.file(suffixMatch.absolutePath).stat();
729
787
  absolutePath = suffixMatch.absolutePath;
730
788
  fileSize = retryStat.size;
731
789
  isDirectory = retryStat.isDirectory();
732
- suffixResolution = { from: readPath, to: suffixMatch.displayPath };
790
+ suffixResolution = { from: localReadPath, to: suffixMatch.displayPath };
733
791
  } catch {
734
792
  // Suffix match candidate no longer stats — fall through to error path
735
793
  }
@@ -737,7 +795,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
737
795
  }
738
796
 
739
797
  if (!suffixResolution) {
740
- throw new ToolError(`Path '${readPath}' not found`);
798
+ throw new ToolError(`Path '${localReadPath}' not found`);
741
799
  }
742
800
  } else {
743
801
  throw error;
@@ -745,7 +803,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
745
803
  }
746
804
 
747
805
  if (isDirectory) {
748
- const dirResult = await this.#readDirectory(absolutePath, limit, signal);
806
+ const dirResult = await this.#readDirectory(absolutePath, selToOffsetLimit(parsed).limit, signal);
749
807
  if (suffixResolution) {
750
808
  dirResult.details ??= {};
751
809
  dirResult.details.suffixResolution = suffixResolution;
@@ -753,11 +811,59 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
753
811
  return dirResult;
754
812
  }
755
813
 
756
- const mimeType = await detectSupportedImageMimeTypeFromFile(absolutePath);
814
+ const imageMetadata = await readImageMetadata(absolutePath);
815
+ const mimeType = imageMetadata?.mimeType;
757
816
  const ext = path.extname(absolutePath).toLowerCase();
817
+ const hasEditTool = this.session.hasEditTool ?? true;
818
+ const language = getLanguageFromPath(absolutePath);
819
+ const skipChunksForExplore = !hasEditTool && !this.session.settings.get("read.explorechunks");
820
+ const skipChunksForProse = isProseLanguage(language) && !this.session.settings.get("read.prosechunks");
821
+ const shouldConvertWithMarkit =
822
+ CONVERTIBLE_EXTENSIONS.has(ext) || (ext === ".ipynb" && (parsed.kind === "raw" || !chunkMode));
823
+
824
+ if (chunkMode && parsed.kind !== "raw" && !skipChunksForExplore && !skipChunksForProse) {
825
+ const absoluteLineRange =
826
+ pathChunkSelector && parsed.kind === "lines"
827
+ ? { startLine: parsed.startLine, endLine: parsed.endLine }
828
+ : undefined;
829
+ // sel= wins over path:chunk when both are provided (explicit param > embedded path).
830
+ const effectiveSelector = sel ? selectorInput : (pathChunkSelector ?? selectorInput);
831
+ const rawEffectiveSelector = sel ? selectorInput : (rawSelectorInput ?? effectiveSelector);
832
+ const chunkReadPath =
833
+ parsed.kind === "chunk" || (pathChunkSelector && !sel)
834
+ ? rawEffectiveSelector
835
+ ? `${localReadPath}:${rawEffectiveSelector}`
836
+ : localReadPath
837
+ : parsed.kind === "lines"
838
+ ? parsed.endLine !== undefined
839
+ ? `${localReadPath}:L${parsed.startLine}-L${parsed.endLine}`
840
+ : `${localReadPath}:L${parsed.startLine}`
841
+ : localReadPath;
842
+ const chunkResult = await formatChunkedRead({
843
+ filePath: absolutePath,
844
+ readPath: chunkReadPath,
845
+ cwd: this.session.cwd,
846
+ language,
847
+ omitChecksum: !hasEditTool,
848
+ anchorStyle: resolveAnchorStyle(this.session.settings),
849
+ absoluteLineRange,
850
+ });
851
+ let text = chunkResult.text;
852
+ if (suffixResolution) {
853
+ text = prependSuffixResolutionNotice(text, suffixResolution);
854
+ }
855
+ return toolResult<ReadToolDetails>({
856
+ resolvedPath: absolutePath,
857
+ suffixResolution,
858
+ chunk: chunkResult.chunk,
859
+ })
860
+ .text(text)
861
+ .sourcePath(absolutePath)
862
+ .done();
863
+ }
758
864
 
759
865
  // Read the file based on type
760
- let content: (TextContent | ImageContent)[];
866
+ let content: Array<TextContent | ImageContent>;
761
867
  let details: ReadToolDetails = {};
762
868
  let sourcePath: string | undefined;
763
869
  let truncationInfo:
@@ -766,14 +872,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
766
872
 
767
873
  if (mimeType) {
768
874
  if (this.#inspectImageEnabled) {
769
- const metadata = await readImageMetadata({
770
- path: readPath,
771
- cwd: this.session.cwd,
772
- resolvedPath: absolutePath,
773
- detectedMimeType: mimeType,
774
- });
875
+ const metadata = imageMetadata;
775
876
  const outputMime = metadata?.mimeType ?? mimeType;
776
- const outputBytes = metadata?.bytes ?? fileSize;
877
+ const outputBytes = fileSize;
777
878
  const metadataLines = [
778
879
  "Image metadata:",
779
880
  `- MIME: ${outputMime}`,
@@ -824,7 +925,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
824
925
  throw error;
825
926
  }
826
927
  }
827
- } else if (CONVERTIBLE_EXTENSIONS.has(ext)) {
928
+ } else if (shouldConvertWithMarkit) {
828
929
  // Convert document or notebook via markit.
829
930
  const result = await convertFileWithMarkit(absolutePath, signal);
830
931
  if (result.ok) {
@@ -843,13 +944,41 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
843
944
  content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }];
844
945
  }
845
946
  } else {
846
- // Read as text using streaming to avoid loading huge files into memory
947
+ // Chunk mode: dispatch to chunk tree unless raw or line range requested
948
+ if (chunkMode && parsed.kind !== "raw" && parsed.kind !== "lines") {
949
+ const chunkSel = parsed.kind === "chunk" ? parsed.selector : undefined;
950
+ const chunkResult = await formatChunkedRead({
951
+ filePath: absolutePath,
952
+ readPath: chunkSel ? `${localReadPath}:${chunkSel}` : localReadPath,
953
+ cwd: this.session.cwd,
954
+ language: getLanguageFromPath(absolutePath),
955
+ omitChecksum: !(this.session.hasEditTool ?? true),
956
+ anchorStyle: resolveAnchorStyle(this.session.settings),
957
+ });
958
+ let text = chunkResult.text;
959
+ if (suffixResolution) {
960
+ text = prependSuffixResolutionNotice(text, suffixResolution);
961
+ }
962
+ return toolResult<ReadToolDetails>({
963
+ resolvedPath: absolutePath,
964
+ suffixResolution,
965
+ chunk: chunkResult.chunk,
966
+ })
967
+ .text(text)
968
+ .sourcePath(absolutePath)
969
+ .done();
970
+ }
971
+
972
+ // Raw text or line-range mode
973
+ const { offset, limit } = selToOffsetLimit(parsed);
847
974
  const startLine = offset ? Math.max(0, offset - 1) : 0;
848
- const startLineDisplay = startLine + 1; // For display (1-indexed)
975
+ const startLineDisplay = startLine + 1;
849
976
 
850
- const effectiveLimit = limit ?? this.#defaultLimit;
977
+ const DEFAULT_LIMIT = this.#defaultLimit;
978
+ const effectiveLimit = limit ?? DEFAULT_LIMIT;
851
979
  const maxLinesToCollect = Math.min(effectiveLimit, DEFAULT_MAX_LINES);
852
980
  const selectedLineLimit = effectiveLimit;
981
+
853
982
  const streamResult = await streamLinesFromFile(
854
983
  absolutePath,
855
984
  startLine,
@@ -873,9 +1002,9 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
873
1002
  const suggestion =
874
1003
  totalFileLines === 0
875
1004
  ? "The file is empty."
876
- : `Use offset=1 to read from the start, or offset=${totalFileLines} to read the last line.`;
1005
+ : `Use sel=L1 to read from the start, or sel=L${totalFileLines} to read the last line.`;
877
1006
  return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
878
- .text(`Offset ${offset} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
1007
+ .text(`Line ${startLineDisplay} is beyond end of file (${totalFileLines} lines total). ${suggestion}`)
879
1008
  .done();
880
1009
  }
881
1010
 
@@ -942,7 +1071,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
942
1071
  const nextOffset = startLine + userLimitedLines + 1;
943
1072
 
944
1073
  outputText = formatText(truncation.content, startLineDisplay);
945
- outputText += `\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue]`;
1074
+ outputText += `\n\n[${remaining} more lines in file. Use sel=L${nextOffset} to continue]`;
946
1075
  details = {};
947
1076
  sourcePath = absolutePath;
948
1077
  } else {
@@ -1100,9 +1229,11 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1100
1229
  interface ReadRenderArgs {
1101
1230
  path?: string;
1102
1231
  file_path?: string;
1232
+ sel?: string;
1233
+ timeout?: number;
1234
+ // Legacy fields from old schema — tolerated for in-flight tool calls during transition
1103
1235
  offset?: number;
1104
1236
  limit?: number;
1105
- timeout?: number;
1106
1237
  raw?: boolean;
1107
1238
  }
1108
1239
 
@@ -1,7 +1,6 @@
1
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { type MermaidAsciiRenderOptions, renderMermaidAscii } from "@oh-my-pi/pi-utils";
2
+ import { type MermaidAsciiRenderOptions, prompt, renderMermaidAscii } from "@oh-my-pi/pi-utils";
3
3
  import { type Static, Type } from "@sinclair/typebox";
4
- import { renderPromptTemplate } from "../config/prompt-templates";
5
4
  import renderMermaidDescription from "../prompts/tools/render-mermaid.md" with { type: "text" };
6
5
  import type { ToolSession } from "./index";
7
6
 
@@ -41,7 +40,7 @@ export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema,
41
40
  readonly strict = true;
42
41
 
43
42
  constructor(private readonly session: ToolSession) {
44
- this.description = renderPromptTemplate(renderMermaidDescription);
43
+ this.description = prompt.render(renderMermaidDescription);
45
44
  }
46
45
 
47
46
  async execute(
@@ -5,17 +5,17 @@
5
5
  * tool renderers to ensure a unified TUI experience.
6
6
  */
7
7
  import * as os from "node:os";
8
- import { type Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
9
- import { getIndentation, pluralize } from "@oh-my-pi/pi-utils";
8
+ import * as path from "node:path";
9
+ import type { Ellipsis } from "@oh-my-pi/pi-natives";
10
+ import { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
11
+ import { pluralize } from "@oh-my-pi/pi-utils";
10
12
  import { settings } from "../config/settings";
11
13
  import type { Theme } from "../modes/theme/theme";
12
14
  import { formatDimensionNote, type ResizedImage } from "../utils/image-resize";
13
15
 
14
- export { Ellipsis, truncateToWidth } from "@oh-my-pi/pi-tui";
16
+ export { Ellipsis } from "@oh-my-pi/pi-natives";
17
+ export { replaceTabs, truncateToWidth } from "@oh-my-pi/pi-tui";
15
18
 
16
- export function replaceTabs(text: string, file?: string): string {
17
- return text.replaceAll("\t", getIndentation(file));
18
- }
19
19
  // =============================================================================
20
20
  // Standardized Display Constants
21
21
  // =============================================================================
@@ -548,6 +548,20 @@ export function shortenPath(filePath: string, homeDir?: string): string {
548
548
  return filePath;
549
549
  }
550
550
 
551
+ export function formatToolWorkingDirectory(workdir: string | undefined, projectDir: string): string | undefined {
552
+ if (!workdir) return undefined;
553
+ const resolvedProjectDir = path.resolve(projectDir);
554
+ const resolvedWorkdir = path.resolve(projectDir, workdir);
555
+ if (resolvedWorkdir === resolvedProjectDir) {
556
+ return undefined;
557
+ }
558
+ const relativePath = path.relative(resolvedProjectDir, resolvedWorkdir);
559
+ const isWithinProject =
560
+ relativePath.length > 0 && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
561
+ const displayWorkdir = isWithinProject ? relativePath : shortenPath(resolvedWorkdir);
562
+ return replaceTabs(displayWorkdir);
563
+ }
564
+
551
565
  export function formatScreenshot(opts: {
552
566
  saveFullRes: boolean;
553
567
  savedMimeType: string;
@@ -4,10 +4,10 @@
4
4
  * These provide rich visualization for tool calls and results in the TUI.
5
5
  */
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
+ import { editToolRenderer } from "../edit/renderer";
7
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
9
  import { lspToolRenderer } from "../lsp/render";
9
10
  import type { Theme } from "../modes/theme/theme";
10
- import { editToolRenderer } from "../patch";
11
11
  import { taskToolRenderer } from "../task/render";
12
12
  import { webSearchToolRenderer } from "../web/search/render";
13
13
  import { askToolRenderer } from "./ask";
@@ -15,6 +15,7 @@ import { astEditToolRenderer } from "./ast-edit";
15
15
  import { astGrepToolRenderer } from "./ast-grep";
16
16
  import { bashToolRenderer } from "./bash";
17
17
  import { calculatorToolRenderer } from "./calculator";
18
+ import { debugToolRenderer } from "./debug";
18
19
  import { findToolRenderer } from "./find";
19
20
  import { ghRunWatchToolRenderer } from "./gh-renderer";
20
21
  import { grepToolRenderer } from "./grep";
@@ -46,6 +47,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
46
47
  ast_grep: astGrepToolRenderer as ToolRenderer,
47
48
  ast_edit: astEditToolRenderer as ToolRenderer,
48
49
  bash: bashToolRenderer as ToolRenderer,
50
+ debug: debugToolRenderer as ToolRenderer,
49
51
  python: pythonToolRenderer as ToolRenderer,
50
52
  calc: calculatorToolRenderer as ToolRenderer,
51
53
  edit: editToolRenderer as ToolRenderer,