@oh-my-pi/pi-coding-agent 12.13.0 → 12.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
8
8
  - `skill://` URIs are auto-resolved to filesystem paths before execution
9
9
  - `python skill://my-skill/scripts/init.py` runs the script from the skill directory
10
10
  - `skill://<name>/<relative-path>` resolves within the skill's base directory
11
- - `agent://`, `artifact://`, `plan://`, `memory://`, and `rule://` URIs are also auto-resolved to filesystem paths before execution
11
+ - `agent://`, `artifact://`, `plan://`, `memory://`, `rule://`, and `docs://` URIs are also auto-resolved to filesystem paths before execution
12
12
  </instruction>
13
13
 
14
14
  <output>
@@ -13,7 +13,7 @@ Powerful search tool built on ripgrep.
13
13
  <output>
14
14
  - Results are always content mode.
15
15
  {{#if IS_HASHLINE_MODE}}
16
- - Text output is CID prefixed: `LINE#ID|content`
16
+ - Text output is CID prefixed: `LINE#ID:content`
17
17
  {{else}}
18
18
  {{#if IS_LINE_NUMBER_MODE}}
19
19
  - Text output is line-number-prefixed
@@ -1,31 +1,34 @@
1
- # Edit (Hash Anchored)
1
+ # Edit
2
2
 
3
- Apply precise file edits using `LINE#ID` anchors from `read` output.
4
- **CRITICAL:** anchors are `LINE#ID` only. Copy verbatim from the prefix (example: `{{hlineref 42 "const x = 1"}}`). Never include `|content`.
3
+ Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
5
4
 
6
5
  <workflow>
7
- 1. `read` the target range to capture current `LINE#ID` anchors.
8
- 2. Pick the smallest operation per change site (`set`/`set_range`/`insert`/`replace`).
9
- 3. Direction-lock every edit: exact current text -> intended text.
6
+ 1. `read` the target range to capture current `LINE#ID` tags.
7
+ 2. Pick the smallest operation per change site (line/range/insert/content-replace).
8
+ 3. Direction-lock every edit: exact current text intended text.
10
9
  4. Submit one `edit` call per file containing all operations.
11
10
  5. If another edit is needed in that file, re-read first (hashes changed).
12
11
  6. Output tool calls only; no prose.
13
12
  </workflow>
14
13
 
15
14
  <operations>
16
- - **`set`** (single line replace/delete)
17
- - `{ set: { ref: "LINE#ID", body: ["..."] } }`
18
- - `body: []` deletes the line; `body: [""]` keeps a blank line.
19
- - **`set_range`** (contiguous multi-line replace/delete)
20
- - `{ set_range: { beg: "LINE#ID", end: "LINE#ID", body: ["..."] } }`
21
- - Use for swaps, block rewrites, or deleting a full span (`body: []`).
22
- - **`insert`** (new content)
23
- - `{ insert: { before: "LINE#ID", body: ["..."] } }`
24
- - `{ insert: { after: "LINE#ID", body: ["..."] } }`
25
- - `{ insert: { after: "LINE#ID", before: "LINE#ID", body: ["..."] } }` (between adjacent anchors; safest for blocks)
26
- - `{ insert: { body: ["..."] } }` (append EOF only when intentional)
27
- - **`replace`** (fuzzy text fallback when anchors unavailable)
28
- - `{ replace: { old_text: "...", new_text: "...", all?: boolean } }`
15
+ - **Single line replace/delete**
16
+ - `{ op: "set", tag: "N#ID", content: [] }`
17
+ - `content: null` deletes the line; `content: [""]` keeps a blank line.
18
+ - **Range replace/delete**
19
+ - `{ op: "replace", first: "N#ID", last: "N#ID", content: [] }`
20
+ - Use for swaps, block rewrites, or deleting a full span (`content: null`).
21
+ - **Insert** (new content)
22
+ - `{ op: "prepend", before: "N#ID", content: […] }` or `{ op: "prepend", content: […] }` (no `before` = insert at beginning of file)
23
+ - `{ op: "append", after: "N#ID", content: […] }` or `{ op: "append", content: […] }` (no `after` = insert at end of file)
24
+ - `{ op: "insert", after: "N#ID", before: "N#ID", content: [] }` (between adjacent anchors; safest for blocks)
25
+ {{#if allowReplaceText}}
26
+ - **Content replace**
27
+ - `{ op: "replaceText", old_text: "", new_text: "", all?: boolean }`
28
+ {{/if}}
29
+ - **File-level controls**
30
+ - `{ delete: true, edits: [] }` deletes the file (cannot be combined with `rename`).
31
+ - `{ rename: "new/path.ts", edits: […] }` writes result to new path and removes old path.
29
32
  **Atomicity:** all ops validate against the same pre-edit file snapshot; refs are interpreted against last `read`; applicator applies bottom-up.
30
33
  </operations>
31
34
 
@@ -33,98 +36,197 @@ Apply precise file edits using `LINE#ID` anchors from `read` output.
33
36
  1. **Minimize scope:** one logical mutation site per operation.
34
37
  2. **Preserve formatting:** keep indentation, punctuation, line breaks, trailing commas, brace style.
35
38
  3. **Prefer insertion over neighbor rewrites:** anchor on structural boundaries (`}`, `]`, `},`) not interior property lines.
36
- 4. **No no-ops:** replacement body must differ from current content.
39
+ 4. **No no-ops:** replacement content must differ from current content.
37
40
  5. **Touch only requested code:** avoid incidental edits.
38
- 6. **Use exact current tokens:** never "rewrite approximately"; mutate the token that exists now.
39
- 7. **For swaps/moves:** prefer one `set_range` over multiple conflicting `set`s.
41
+ 6. **Use exact current tokens:** never rewrite approximately; mutate the token that exists now.
42
+ 7. **For swaps/moves:** prefer one range operation over multiple single-line operations.
40
43
  </rules>
41
44
 
42
- <selection_heuristics>
43
- - One wrong line -> `set`
44
- - Adjacent block changed -> `set_range`
45
- - Missing line/block -> `insert`
46
- - Cannot trust line anchors (generated/unknown offsets) -> `replace` (last resort)
47
- </selection_heuristics>
48
-
49
- <anchor_hygiene>
50
- - Copy anchor IDs exactly from `read` or error output.
51
- - Never handcraft hashes.
52
- - For inserts, prefer `after+before` dual anchors when both boundaries are known.
45
+ <op_choice>
46
+ - One wrong line `set`
47
+ - Adjacent block changed `insert`
48
+ - Missing line/block insert with `append`/`prepend`
49
+ </op_choice>
50
+
51
+ <tag_choice>
52
+ - Copy tags exactly from the prefix of the `read` or error output.
53
+ - Never guess tags.
54
+ - For inserts, prefer `insert` > `append`/`prepend` when both boundaries are known.
53
55
  - Re-read after each successful edit call before issuing another on same file.
54
- </anchor_hygiene>
56
+ </tag_choice>
55
57
 
56
58
  <recovery>
57
- **Hash mismatch (`>>>`)**
58
- - Retry with the updated anchors shown in error output.
59
- - Re-read only if required anchors are missing from error snippet.
59
+ **Tag mismatch (`>>>`)**
60
+ - Retry with the updated tags shown in error output.
61
+ - Re-read only if required tags are missing from error snippet.
60
62
  - If mismatch repeats, stop and re-read the exact block.
61
- **No-op / identical content**
62
- - Re-read immediately; target is stale or replacement equals current text.
63
- - After two no-ops on same area, re-read the full function/block before retry.
64
63
  </recovery>
65
64
 
66
- <examples>
67
- <example name="single-line token fix (set)">
68
- Read:
69
- {{hlinefull 41 " return record != null && record.status === 'fulfilled';"}}
70
- Edit:
71
- set: { ref: "{{hlineref 41 " return record != null && record.status === 'fulfilled';"}}", body: [" return record != null && record?.status === 'fulfilled';"] }
65
+ <example name="fix a value or type">
66
+ ```ts
67
+ {{hlinefull 23 " const timeout: number = 5000;"}}
68
+ ```
69
+ ```
70
+ op: "set"
71
+ tag: "{{hlineref 23 " const timeout: number = 5000;"}}"
72
+ content: [" const timeout: number = 30_000;"]
73
+ ```
74
+ </example>
75
+
76
+ <example name="remove a line entirely">
77
+ ```ts
78
+ {{hlinefull 7 "// @ts-ignore"}}
79
+ {{hlinefull 8 "const data = fetchSync(url);"}}
80
+ ```
81
+ ```
82
+ op: "set"
83
+ tag: "{{hlineref 7 "// @ts-ignore"}}"
84
+ content: null
85
+ ```
86
+ </example>
87
+
88
+ <example name="clear content but keep the line break">
89
+ ```ts
90
+ {{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
91
+ ```
92
+ ```
93
+ op: "set"
94
+ tag: "{{hlineref 14 " placeholder: \"DO NOT SHIP\","}}"
95
+ content: [""]
96
+ ```
97
+ </example>
98
+
99
+ <example name="rewrite a block of logic">
100
+ ```ts
101
+ {{hlinefull 60 " } catch (err) {"}}
102
+ {{hlinefull 61 " console.error(err);"}}
103
+ {{hlinefull 62 " return null;"}}
104
+ {{hlinefull 63 " }"}}
105
+ ```
106
+ ```
107
+ op: "replace"
108
+ first: "{{hlineref 60 " } catch (err) {"}}"
109
+ last: "{{hlineref 63 " }"}}"
110
+ content: [" } catch (err) {", " if (isEnoent(err)) return null;", " throw err;", " }"]
111
+ ```
72
112
  </example>
73
113
 
74
- <example name="restore missing declaration (insert before)">
75
- Read:
76
- {{hlinefull 15 "export function useX(...): boolean {"}}
77
- {{hlinefull 16 " useEffect(() => {"}}
78
- Edit:
79
- insert: { before: "{{hlineref 16 " useEffect(() => {"}}", body: [" const [isVisible, setIsVisible] = useState(true);"] }
114
+ <example name="remove a full block">
115
+ ```ts
116
+ {{hlinefull 80 " // TODO: remove after migration"}}
117
+ {{hlinefull 81 " if (legacy) {"}}
118
+ {{hlinefull 82 " legacyHandler(req);"}}
119
+ {{hlinefull 83 " }"}}
120
+ ```
121
+ ```
122
+ op: "replace"
123
+ first: "{{hlineref 80 " // TODO: remove after migration"}}"
124
+ last: "{{hlineref 83 " }"}}"
125
+ content: null
126
+ ```
80
127
  </example>
81
128
 
82
- <example name="insert between siblings (after+before)">
83
- Read:
84
- {{hlinefull 120 " doFirst();"}}
85
- {{hlinefull 121 " doThird();"}}
86
- Edit:
87
- insert: { after: "{{hlineref 120 " doFirst();"}}", before: "{{hlineref 121 " doThird();"}}", body: [" doSecond();"] }
129
+ <example name="add an import above the first import">
130
+ ```ts
131
+ {{hlinefull 1 "import * as fs from \"node:fs/promises\";"}}
132
+ {{hlinefull 2 "import * as path from \"node:path\";"}}
133
+ ```
134
+ ```
135
+ op: "prepend"
136
+ before: "{{hlineref 1 "import * as fs from \"node:fs/promises\";"}}"
137
+ content: ["import * as os from \"node:os\";"]
138
+ ```
139
+ Use `before` for anchored insertion before a specific line. Omit `before` to prepend at BOF.
88
140
  </example>
89
141
 
90
- <example name="swap adjacent lines atomically (set_range)">
91
- Read:
92
- {{hlinefull 190 " thenable.then(resolve, ignoreReject);"}}
93
- {{hlinefull 191 " chunkCache.set(chunkId, thenable);"}}
94
- Edit:
95
- set_range: { beg: "{{hlineref 190 " thenable.then(resolve, ignoreReject);"}}", end: "{{hlineref 191 " chunkCache.set(chunkId, thenable);"}}", body: [" chunkCache.set(chunkId, thenable);", " thenable.then(resolve, ignoreReject);"] }
142
+ <example name="append at end of file">
143
+ ```ts
144
+ {{hlinefull 260 "export { serialize, deserialize };"}}
145
+ ```
146
+ ```
147
+ op: "append"
148
+ after: "{{hlineref 260 "export { serialize, deserialize };"}}"
149
+ content: ["export { validate };"]
150
+ ```
151
+ Use `after` for anchored insertion after a specific line. Omit `after` to append at EOF.
96
152
  </example>
97
153
 
98
- <example name="insert guard before comment">
99
- Read:
100
- {{hlinefull 188 ""}}
101
- {{hlinefull 189 " // If we don't find a Fiber on the comment..."}}
102
- Edit:
103
- insert: { after: "{{hlineref 188 ""}}", body: [" if (targetFiber) {", " targetInst = targetFiber;", " }"] }
154
+ <example name="add an entry between known siblings">
155
+ ```ts
156
+ {{hlinefull 44 " \"build\": \"bun run compile\","}}
157
+ {{hlinefull 45 " \"test\": \"bun test\""}}
158
+ ```
159
+ ```
160
+ op: "insert"
161
+ after: "{{hlineref 44 " \"build\": \"bun run compile\","}}"
162
+ before: "{{hlineref 45 " \"test\": \"bun test\""}}"
163
+ content: [" \"lint\": \"biome check\","]
164
+ ```
165
+ Dual anchors pin the insert to exactly one gap, preventing drift from edits elsewhere in the file. **Always prefer dual anchors when both boundaries are content lines.**
104
166
  </example>
105
167
 
106
- <example name="anti-pattern: interior anchor vs boundary anchor">
107
- Bad:
108
- insert: { after: "195#d3", body: [" { id: \"nanogpt\", available: true },"] }
109
- Good:
110
- insert: { after: "196#f6", before: "197#fc", body: [" { id: \"nanogpt\", available: true },"] }
168
+ <example name="insert a function before another function">
169
+ ```ts
170
+ {{hlinefull 100 " return buf.toString(\"hex\");"}}
171
+ {{hlinefull 101 "}"}}
172
+ {{hlinefull 102 ""}}
173
+ {{hlinefull 103 "export function serialize(data: unknown): string {"}}
174
+ ```
175
+ ```
176
+ op: "insert"
177
+ before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
178
+ content: ["function validate(data: unknown): boolean {", " return data != null && typeof data === \"object\";", "}", ""]
179
+ ```
180
+ The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function ...`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
111
181
  </example>
112
182
 
113
- <example name="explicit EOF append">
114
- insert: { body: ["// end marker"] }
183
+ {{#if allowReplaceText}}
184
+ <example name="content replace (rare)">
185
+ ```
186
+ op: "replaceText"
187
+ old_text: "x = 42"
188
+ new_text: "x = 99"
189
+ ```
190
+
191
+ Use only when line anchors aren't available. `old_text` must match exactly one location in the file (or set `"all": true` for all occurrences).
115
192
  </example>
193
+ {{/if}}
116
194
 
117
- <example name="replace fallback only">
118
- replace: { old_text: "x = 42", new_text: "x = 99" }
195
+ <example name="file delete">
196
+ ```
197
+ path: "src/deprecated/legacy.ts"
198
+ delete: true
199
+ ```
119
200
  </example>
120
- </examples>
121
-
122
- <validation>
123
- - [ ] Payload shape is `{ "path": string, "edits": [operation, ...] }` and `edits` is non-empty
124
- - [ ] Every operation has exactly one variant key: `set` | `set_range` | `insert` | `replace`
125
- - [ ] Every anchor is copied exactly as `LINE#ID` (no spaces, no `|content`)
126
- - [ ] `body` lines are raw content only (no diff markers, no anchor prefixes)
127
- - [ ] Every replacement is meaningfully different from current content
128
- - [ ] Scope is minimal and formatting is preserved except targeted token changes
129
- </validation>
130
- **Final reminder:** anchors are immutable references to the last read snapshot. Re-read when state changes, then edit.
201
+
202
+ <example name="file rename with edits">
203
+ ```
204
+ path: "src/utils.ts"
205
+ rename: "src/helpers/utils.ts"
206
+ edits: []
207
+ ```
208
+ </example>
209
+
210
+ <example name="anti-pattern: anchoring to whitespace">
211
+ Bad tags to a blank line; fragile if blank lines shift:
212
+ ```
213
+ after: "{{hlineref 102 ""}}"
214
+ content: ["function validate() {", …, "}"]
215
+ ```
216
+
217
+ Good — anchors to the structural target:
218
+
219
+ ```
220
+ before: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
221
+ content: ["function validate() {", …, "}"]
222
+ ```
223
+ </example>
224
+
225
+ <critical>
226
+ Ensure:
227
+ - Payload shape is `{ "path": string, "edits": [operation, …], "delete"?: boolean, "rename"?: string }`
228
+ - Every edit matches exactly one variant
229
+ - Every tag has been copied EXACTLY from a tool result as `N#ID`
230
+ - Scope is minimal and formatting is preserved except targeted token changes
231
+ </critical>
232
+ **Final reminder:** tags are immutable references to the last read snapshot. Re-read when state changes, then edit.
@@ -6,7 +6,7 @@ Reads files from local filesystem or internal URLs.
6
6
  - Reads up to {{DEFAULT_MAX_LINES}} lines default
7
7
  - Use `offset` and `limit` for large files
8
8
  {{#if IS_HASHLINE_MODE}}
9
- - Text output is CID prefixed: `LINE#ID|content`
9
+ - Text output is CID prefixed: `LINE#ID:content`
10
10
  {{else}}
11
11
  {{#if IS_LINE_NUMBER_MODE}}
12
12
  - Text output is line-number-prefixed
@@ -23,6 +23,8 @@ Reads files from local filesystem or internal URLs.
23
23
  - `memory://root/<path>` - read relative path within project memory root
24
24
  - `agent://<id>` - read agent output artifact
25
25
  - `agent://<id>/<path>` or `agent://<id>?q=<query>` - extract JSON from agent output
26
+ - `docs://` - list available pi documentation files
27
+ - `docs://<file>.md` - read a specific pi documentation file
26
28
  </instruction>
27
29
 
28
30
  <output>
package/src/sdk.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type CustomCommandsLoadResult,
20
20
  loadCustomCommands as loadCustomCommandsInternal,
21
21
  } from "./extensibility/custom-commands";
22
+ import { discoverAndLoadCustomTools } from "./extensibility/custom-tools";
22
23
  import type { CustomTool, CustomToolContext, CustomToolSessionEvent } from "./extensibility/custom-tools/types";
23
24
  import { CustomToolAdapter } from "./extensibility/custom-tools/wrapper";
24
25
  import {
@@ -39,6 +40,7 @@ import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal }
39
40
  import {
40
41
  AgentProtocolHandler,
41
42
  ArtifactProtocolHandler,
43
+ DocsProtocolHandler,
42
44
  InternalUrlRouter,
43
45
  MemoryProtocolHandler,
44
46
  PlanProtocolHandler,
@@ -770,6 +772,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
770
772
  getRules: () => rulebookRules,
771
773
  }),
772
774
  );
775
+ internalRouter.register(new DocsProtocolHandler());
773
776
  toolSession.internalRouter = internalRouter;
774
777
  toolSession.getArtifactsDir = getArtifactsDir;
775
778
  toolSession.agentOutputManager = new AgentOutputManager(
@@ -848,6 +851,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
848
851
  time("getSearchTools");
849
852
  }
850
853
 
854
+ debugStartup("sdk:discoverCustomTools:start");
855
+ // Discover and load custom tools from .omp/tools/, .claude/tools/, etc.
856
+ const builtInToolNames = builtinTools.map(t => t.name);
857
+ const discoveredCustomTools = await discoverAndLoadCustomTools([], cwd, builtInToolNames);
858
+ for (const { path, error } of discoveredCustomTools.errors) {
859
+ logger.error("Custom tool load failed", { path, error });
860
+ }
861
+ if (discoveredCustomTools.tools.length > 0) {
862
+ customTools.push(...discoveredCustomTools.tools.map(loaded => loaded.tool));
863
+ }
864
+ time("discoverAndLoadCustomTools");
865
+ debugStartup("sdk:discoverCustomTools:done");
866
+
851
867
  const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
852
868
  if (customTools.length > 0) {
853
869
  inlineExtensions.push(createCustomToolsExtension(customTools));
@@ -1207,6 +1223,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1207
1223
  },
1208
1224
  cursorExecHandlers,
1209
1225
  transformToolCallArguments: obfuscator?.hasSecrets() ? args => obfuscator!.deobfuscateObject(args) : undefined,
1226
+ intentTracing: settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1",
1210
1227
  });
1211
1228
  cursorEventEmitter = event => agent.emitExternalEvent(event);
1212
1229
  debugStartup("sdk:createAgent");
@@ -1140,6 +1140,7 @@ export class AgentSession {
1140
1140
  toolCallId: event.toolCallId,
1141
1141
  toolName: event.toolName,
1142
1142
  args: event.args,
1143
+ intent: event.intent,
1143
1144
  };
1144
1145
  await this.#extensionRunner.emit(extensionEvent);
1145
1146
  } else if (event.type === "tool_execution_update") {
@@ -966,11 +966,12 @@ function countNonEmptyLines(text: string): number {
966
966
 
967
967
  /** Render fetch call (URL preview) */
968
968
  export function renderFetchCall(
969
- args: { url: string; timeout?: number; raw?: boolean },
969
+ args: { url?: string; timeout?: number; raw?: boolean },
970
970
  uiTheme: Theme = theme,
971
971
  ): Component {
972
- const domain = getDomain(args.url);
973
- const path = truncate(args.url.replace(/^https?:\/\/[^/]+/, ""), 50, "…");
972
+ const url = args.url ?? "";
973
+ const domain = getDomain(url);
974
+ const path = truncate(url.replace(/^https?:\/\/[^/]+/, ""), 50, "\u2026");
974
975
  const description = `${domain}${path ? ` ${path}` : ""}`.trim();
975
976
  const meta: string[] = [];
976
977
  if (args.raw) meta.push("raw");
package/src/tools/grep.ts CHANGED
@@ -104,7 +104,17 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
104
104
  const effectiveMultiline = multiline ?? patternHasNewline;
105
105
 
106
106
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
107
- const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
107
+ let searchPath: string;
108
+ const internalRouter = this.session.internalRouter;
109
+ if (searchDir && internalRouter?.canHandle(searchDir)) {
110
+ const resource = await internalRouter.resolve(searchDir);
111
+ if (!resource.sourcePath) {
112
+ throw new ToolError(`Cannot grep internal URL without a backing file: ${searchDir}`);
113
+ }
114
+ searchPath = resource.sourcePath;
115
+ } else {
116
+ searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
117
+ }
108
118
  const scopePath = (() => {
109
119
  const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
110
120
  return relative.length === 0 ? "." : relative;
@@ -210,10 +220,10 @@ export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
210
220
  const formatLine = (lineNumber: number, line: string, isMatch: boolean): string => {
211
221
  if (useHashLines) {
212
222
  const ref = `${lineNumber}#${computeLineHash(lineNumber, line)}`;
213
- return isMatch ? `>>${ref}|${line}` : ` ${ref}|${line}`;
223
+ return isMatch ? `>>${ref}:${line}` : ` ${ref}:${line}`;
214
224
  }
215
225
  const padded = lineNumber.toString().padStart(lineWidth, " ");
216
- return isMatch ? `>>${padded}|${line}` : ` ${padded}|${line}`;
226
+ return isMatch ? `>>${padded}:${line}` : ` ${padded}:${line}`;
217
227
  };
218
228
 
219
229
  // Add context before
package/src/tools/read.ts CHANGED
@@ -772,7 +772,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
772
772
  const prependHashLines = (text: string, startNum: number): string => {
773
773
  const textLines = text.split("\n");
774
774
  return textLines
775
- .map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}|${line}`)
775
+ .map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}:${line}`)
776
776
  .join("\n");
777
777
  };
778
778
  const formatText = (text: string, startNum: number): string => {
@@ -929,7 +929,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
929
929
  };
930
930
  const prependHashLines = (text: string, startNum: number): string => {
931
931
  const textLines = text.split("\n");
932
- return textLines.map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}|${line}`).join("\n");
932
+ return textLines.map((line, i) => `${startNum + i}#${computeLineHash(startNum + i, line)}:${line}`).join("\n");
933
933
  };
934
934
  const formatText = (text: string, startNum: number): string => {
935
935
  if (shouldAddHashLines) return prependHashLines(text, startNum);
@@ -282,11 +282,11 @@ export function renderSearchResult(
282
282
 
283
283
  /** Render web search call (query preview) */
284
284
  export function renderSearchCall(
285
- args: { query: string; provider?: string; [key: string]: unknown },
285
+ args: { query?: string; provider?: string; [key: string]: unknown },
286
286
  theme: Theme,
287
287
  ): Component {
288
288
  const provider = args.provider ?? "auto";
289
- const query = truncateToWidth(args.query, 80);
289
+ const query = truncateToWidth(args.query ?? "", 80);
290
290
  const text = renderStatusLine({ icon: "pending", title: "Web Search", description: query, meta: [provider] }, theme);
291
291
  return new Text(text, 0, 0);
292
292
  }