@oh-my-pi/pi-coding-agent 6.1.0 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/docs/sdk.md +1 -1
  3. package/package.json +5 -5
  4. package/scripts/generate-template.ts +6 -6
  5. package/src/cli/args.ts +3 -0
  6. package/src/core/agent-session.ts +39 -0
  7. package/src/core/bash-executor.ts +3 -3
  8. package/src/core/cursor/exec-bridge.ts +95 -88
  9. package/src/core/custom-commands/bundled/review/index.ts +142 -145
  10. package/src/core/custom-commands/bundled/wt/index.ts +68 -66
  11. package/src/core/custom-commands/loader.ts +4 -6
  12. package/src/core/custom-tools/index.ts +2 -2
  13. package/src/core/custom-tools/loader.ts +66 -61
  14. package/src/core/custom-tools/types.ts +4 -4
  15. package/src/core/custom-tools/wrapper.ts +61 -25
  16. package/src/core/event-bus.ts +19 -47
  17. package/src/core/extensions/index.ts +8 -4
  18. package/src/core/extensions/loader.ts +160 -120
  19. package/src/core/extensions/types.ts +4 -4
  20. package/src/core/extensions/wrapper.ts +149 -100
  21. package/src/core/hooks/index.ts +1 -1
  22. package/src/core/hooks/tool-wrapper.ts +96 -70
  23. package/src/core/hooks/types.ts +1 -2
  24. package/src/core/index.ts +1 -0
  25. package/src/core/mcp/index.ts +6 -2
  26. package/src/core/mcp/json-rpc.ts +88 -0
  27. package/src/core/mcp/loader.ts +22 -4
  28. package/src/core/mcp/manager.ts +202 -48
  29. package/src/core/mcp/tool-bridge.ts +143 -55
  30. package/src/core/mcp/tool-cache.ts +122 -0
  31. package/src/core/python-executor.ts +3 -9
  32. package/src/core/sdk.ts +33 -32
  33. package/src/core/session-manager.ts +30 -0
  34. package/src/core/settings-manager.ts +34 -1
  35. package/src/core/ssh/ssh-executor.ts +6 -84
  36. package/src/core/streaming-output.ts +107 -53
  37. package/src/core/tools/ask.ts +92 -93
  38. package/src/core/tools/bash.ts +103 -94
  39. package/src/core/tools/calculator.ts +41 -26
  40. package/src/core/tools/complete.ts +76 -66
  41. package/src/core/tools/context.ts +25 -25
  42. package/src/core/tools/exa/index.ts +1 -1
  43. package/src/core/tools/exa/mcp-client.ts +56 -101
  44. package/src/core/tools/find.ts +250 -253
  45. package/src/core/tools/git.ts +39 -33
  46. package/src/core/tools/grep.ts +440 -427
  47. package/src/core/tools/index.ts +62 -61
  48. package/src/core/tools/ls.ts +119 -114
  49. package/src/core/tools/lsp/clients/biome-client.ts +5 -7
  50. package/src/core/tools/lsp/clients/index.ts +4 -4
  51. package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
  52. package/src/core/tools/lsp/config.ts +2 -2
  53. package/src/core/tools/lsp/index.ts +824 -639
  54. package/src/core/tools/notebook.ts +121 -119
  55. package/src/core/tools/output.ts +163 -147
  56. package/src/core/tools/patch/applicator.ts +1100 -0
  57. package/src/core/tools/patch/diff.ts +362 -0
  58. package/src/core/tools/patch/fuzzy.ts +647 -0
  59. package/src/core/tools/patch/index.ts +430 -0
  60. package/src/core/tools/patch/normalize.ts +220 -0
  61. package/src/core/tools/patch/normative.ts +49 -0
  62. package/src/core/tools/patch/parser.ts +528 -0
  63. package/src/core/tools/patch/shared.ts +228 -0
  64. package/src/core/tools/patch/types.ts +244 -0
  65. package/src/core/tools/python.ts +139 -136
  66. package/src/core/tools/read.ts +237 -216
  67. package/src/core/tools/render-utils.ts +196 -77
  68. package/src/core/tools/renderers.ts +1 -1
  69. package/src/core/tools/ssh.ts +99 -80
  70. package/src/core/tools/task/executor.ts +11 -7
  71. package/src/core/tools/task/index.ts +352 -343
  72. package/src/core/tools/task/worker.ts +13 -23
  73. package/src/core/tools/todo-write.ts +74 -59
  74. package/src/core/tools/web-fetch.ts +54 -47
  75. package/src/core/tools/web-search/index.ts +27 -16
  76. package/src/core/tools/write.ts +89 -41
  77. package/src/core/ttsr.ts +106 -152
  78. package/src/core/voice.ts +49 -39
  79. package/src/index.ts +16 -12
  80. package/src/lib/worktree/index.ts +1 -9
  81. package/src/modes/interactive/components/diff.ts +15 -8
  82. package/src/modes/interactive/components/settings-defs.ts +24 -0
  83. package/src/modes/interactive/components/tool-execution.ts +34 -6
  84. package/src/modes/interactive/controllers/event-controller.ts +6 -19
  85. package/src/modes/interactive/controllers/input-controller.ts +1 -1
  86. package/src/modes/interactive/utils/ui-helpers.ts +5 -1
  87. package/src/modes/rpc/rpc-mode.ts +99 -81
  88. package/src/prompts/tools/patch.md +76 -0
  89. package/src/prompts/tools/read.md +1 -1
  90. package/src/prompts/tools/{edit.md → replace.md} +1 -0
  91. package/src/utils/shell.ts +0 -40
  92. package/src/core/tools/edit-diff.ts +0 -574
  93. package/src/core/tools/edit.ts +0 -326
@@ -134,6 +134,8 @@ export class EventController {
134
134
  content.arguments,
135
135
  {
136
136
  showImages: this.ctx.settingsManager.getShowImages(),
137
+ editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
138
+ editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
137
139
  },
138
140
  tool,
139
141
  this.ctx.ui,
@@ -174,26 +176,9 @@ export class EventController {
174
176
  }
175
177
 
176
178
  if (
177
- this.ctx.streamingMessage.stopReason === "aborted" ||
178
- this.ctx.streamingMessage.stopReason === "error"
179
+ this.ctx.streamingMessage.stopReason !== "aborted" &&
180
+ this.ctx.streamingMessage.stopReason !== "error"
179
181
  ) {
180
- if (!this.ctx.session.isTtsrAbortPending) {
181
- if (!errorMessage) {
182
- errorMessage = this.ctx.streamingMessage.errorMessage || "Error";
183
- }
184
- for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
185
- component.updateResult(
186
- {
187
- content: [{ type: "text", text: errorMessage }],
188
- isError: true,
189
- },
190
- false,
191
- toolCallId,
192
- );
193
- }
194
- }
195
- this.ctx.pendingTools.clear();
196
- } else {
197
182
  for (const [toolCallId, component] of this.ctx.pendingTools.entries()) {
198
183
  component.setArgsComplete(toolCallId);
199
184
  }
@@ -223,6 +208,8 @@ export class EventController {
223
208
  event.args,
224
209
  {
225
210
  showImages: this.ctx.settingsManager.getShowImages(),
211
+ editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
212
+ editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
226
213
  },
227
214
  tool,
228
215
  this.ctx.ui,
@@ -338,7 +338,7 @@ export class InputController {
338
338
 
339
339
  // Generate session title on first message
340
340
  const hasUserMessages = this.ctx.agent.state.messages.some((m: AgentMessage) => m.role === "user");
341
- if (!hasUserMessages && !this.ctx.sessionManager.getSessionTitle()) {
341
+ if (!hasUserMessages && !this.ctx.sessionManager.getSessionTitle() && !process.env.OMP_NO_TITLE) {
342
342
  const registry = this.ctx.session.modelRegistry;
343
343
  const smolModel = this.ctx.settingsManager.getModelRole("smol");
344
344
  generateSessionTitle(text, registry, smolModel, this.ctx.session.sessionId)
@@ -206,7 +206,11 @@ export class UiHelpers {
206
206
  const component = new ToolExecutionComponent(
207
207
  content.name,
208
208
  content.arguments,
209
- { showImages: this.ctx.settingsManager.getShowImages() },
209
+ {
210
+ showImages: this.ctx.settingsManager.getShowImages(),
211
+ editFuzzyThreshold: this.ctx.settingsManager.getEditFuzzyThreshold(),
212
+ editAllowFuzzy: this.ctx.settingsManager.getEditFuzzyMatch(),
213
+ },
210
214
  tool,
211
215
  this.ctx.ui,
212
216
  this.ctx.sessionManager.getCwd(),
@@ -67,55 +67,60 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
67
67
  // Shutdown request flag (wrapped in object to allow mutation with const)
68
68
  const shutdownState = { requested: false };
69
69
 
70
- /** Helper for dialog methods with signal/timeout support */
71
- function createDialogPromise<T>(
72
- opts: ExtensionUIDialogOptions | undefined,
73
- defaultValue: T,
74
- request: Record<string, unknown>,
75
- parseResponse: (response: RpcExtensionUIResponse) => T,
76
- ): Promise<T> {
77
- if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
78
-
79
- const id = nanoid();
80
- return new Promise((resolve, reject) => {
81
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
82
-
83
- const cleanup = () => {
84
- if (timeoutId) clearTimeout(timeoutId);
85
- opts?.signal?.removeEventListener("abort", onAbort);
86
- pendingExtensionRequests.delete(id);
87
- };
88
-
89
- const onAbort = () => {
90
- cleanup();
91
- resolve(defaultValue);
92
- };
93
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
94
-
95
- if (opts?.timeout !== undefined) {
96
- timeoutId = setTimeout(() => {
70
+ /**
71
+ * Extension UI context that uses the RPC protocol.
72
+ */
73
+ class RpcExtensionUIContext implements ExtensionUIContext {
74
+ constructor(
75
+ private pendingRequests: Map<string, PendingExtensionRequest>,
76
+ private output: (obj: RpcResponse | RpcExtensionUIRequest | object) => void,
77
+ ) {}
78
+
79
+ /** Helper for dialog methods with signal/timeout support */
80
+ private createDialogPromise<T>(
81
+ opts: ExtensionUIDialogOptions | undefined,
82
+ defaultValue: T,
83
+ request: Record<string, unknown>,
84
+ parseResponse: (response: RpcExtensionUIResponse) => T,
85
+ ): Promise<T> {
86
+ if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
87
+
88
+ const id = nanoid();
89
+ return new Promise((resolve, reject) => {
90
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
91
+
92
+ const cleanup = () => {
93
+ if (timeoutId) clearTimeout(timeoutId);
94
+ opts?.signal?.removeEventListener("abort", onAbort);
95
+ this.pendingRequests.delete(id);
96
+ };
97
+
98
+ const onAbort = () => {
97
99
  cleanup();
98
100
  resolve(defaultValue);
99
- }, opts.timeout);
100
- }
101
+ };
102
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
101
103
 
102
- pendingExtensionRequests.set(id, {
103
- resolve: (response: RpcExtensionUIResponse) => {
104
- cleanup();
105
- resolve(parseResponse(response));
106
- },
107
- reject,
104
+ if (opts?.timeout !== undefined) {
105
+ timeoutId = setTimeout(() => {
106
+ cleanup();
107
+ resolve(defaultValue);
108
+ }, opts.timeout);
109
+ }
110
+
111
+ this.pendingRequests.set(id, {
112
+ resolve: (response: RpcExtensionUIResponse) => {
113
+ cleanup();
114
+ resolve(parseResponse(response));
115
+ },
116
+ reject,
117
+ });
118
+ this.output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
108
119
  });
109
- output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
110
- });
111
- }
120
+ }
112
121
 
113
- /**
114
- * Create an extension UI context that uses the RPC protocol.
115
- */
116
- const createExtensionUIContext = (): ExtensionUIContext => ({
117
- select: (title, options, dialogOptions) =>
118
- createDialogPromise(
122
+ select(title: string, options: string[], dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined> {
123
+ return this.createDialogPromise(
119
124
  dialogOptions,
120
125
  undefined,
121
126
  { method: "select", title, options, timeout: dialogOptions?.timeout },
@@ -125,10 +130,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
125
130
  : "value" in response
126
131
  ? response.value
127
132
  : undefined,
128
- ),
133
+ );
134
+ }
129
135
 
130
- confirm: (title, message, dialogOptions) =>
131
- createDialogPromise(
136
+ confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
137
+ return this.createDialogPromise(
132
138
  dialogOptions,
133
139
  false,
134
140
  { method: "confirm", title, message, timeout: dialogOptions?.timeout },
@@ -138,10 +144,15 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
138
144
  : "confirmed" in response
139
145
  ? response.confirmed
140
146
  : false,
141
- ),
147
+ );
148
+ }
142
149
 
143
- input: (title, placeholder, dialogOptions) =>
144
- createDialogPromise(
150
+ input(
151
+ title: string,
152
+ placeholder?: string,
153
+ dialogOptions?: ExtensionUIDialogOptions,
154
+ ): Promise<string | undefined> {
155
+ return this.createDialogPromise(
145
156
  dialogOptions,
146
157
  undefined,
147
158
  { method: "input", title, placeholder, timeout: dialogOptions?.timeout },
@@ -151,34 +162,35 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
151
162
  : "value" in response
152
163
  ? response.value
153
164
  : undefined,
154
- ),
165
+ );
166
+ }
155
167
 
156
168
  notify(message: string, type?: "info" | "warning" | "error"): void {
157
169
  // Fire and forget - no response needed
158
- output({
170
+ this.output({
159
171
  type: "extension_ui_request",
160
172
  id: nanoid(),
161
173
  method: "notify",
162
174
  message,
163
175
  notifyType: type,
164
176
  } as RpcExtensionUIRequest);
165
- },
177
+ }
166
178
 
167
179
  setStatus(key: string, text: string | undefined): void {
168
180
  // Fire and forget - no response needed
169
- output({
181
+ this.output({
170
182
  type: "extension_ui_request",
171
183
  id: nanoid(),
172
184
  method: "setStatus",
173
185
  statusKey: key,
174
186
  statusText: text,
175
187
  } as RpcExtensionUIRequest);
176
- },
188
+ }
177
189
 
178
190
  setWidget(key: string, content: unknown): void {
179
191
  // Only support string arrays in RPC mode - factory functions are ignored
180
192
  if (content === undefined || Array.isArray(content)) {
181
- output({
193
+ this.output({
182
194
  type: "extension_ui_request",
183
195
  id: nanoid(),
184
196
  method: "setWidget",
@@ -187,53 +199,53 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
187
199
  } as RpcExtensionUIRequest);
188
200
  }
189
201
  // Component factories are not supported in RPC mode - would need TUI access
190
- },
202
+ }
191
203
 
192
204
  setFooter(_factory: unknown): void {
193
205
  // Custom footer not supported in RPC mode - requires TUI access
194
- },
206
+ }
195
207
 
196
208
  setHeader(_factory: unknown): void {
197
209
  // Custom header not supported in RPC mode - requires TUI access
198
- },
210
+ }
199
211
 
200
212
  setTitle(title: string): void {
201
213
  // Fire and forget - host can implement terminal title control
202
- output({
214
+ this.output({
203
215
  type: "extension_ui_request",
204
216
  id: nanoid(),
205
217
  method: "setTitle",
206
218
  title,
207
219
  } as RpcExtensionUIRequest);
208
- },
220
+ }
209
221
 
210
- async custom() {
222
+ async custom(): Promise<never> {
211
223
  // Custom UI not supported in RPC mode
212
224
  return undefined as never;
213
- },
225
+ }
214
226
 
215
227
  setEditorText(text: string): void {
216
228
  // Fire and forget - host can implement editor control
217
- output({
229
+ this.output({
218
230
  type: "extension_ui_request",
219
231
  id: nanoid(),
220
232
  method: "set_editor_text",
221
233
  text,
222
234
  } as RpcExtensionUIRequest);
223
- },
235
+ }
224
236
 
225
237
  getEditorText(): string {
226
238
  // Synchronous method can't wait for RPC response
227
239
  // Host should track editor state locally if needed
228
240
  return "";
229
- },
241
+ }
230
242
 
231
243
  async editor(title: string, prefill?: string): Promise<string | undefined> {
232
244
  const id = nanoid();
233
245
  return new Promise((resolve, reject) => {
234
- pendingExtensionRequests.set(id, {
246
+ this.pendingRequests.set(id, {
235
247
  resolve: (response: RpcExtensionUIResponse) => {
236
- pendingExtensionRequests.delete(id);
248
+ this.pendingRequests.delete(id);
237
249
  if ("cancelled" in response && response.cancelled) {
238
250
  resolve(undefined);
239
251
  } else if ("value" in response) {
@@ -244,31 +256,37 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
244
256
  },
245
257
  reject,
246
258
  });
247
- output({ type: "extension_ui_request", id, method: "editor", title, prefill } as RpcExtensionUIRequest);
259
+ this.output({
260
+ type: "extension_ui_request",
261
+ id,
262
+ method: "editor",
263
+ title,
264
+ prefill,
265
+ } as RpcExtensionUIRequest);
248
266
  });
249
- },
267
+ }
250
268
 
251
- get theme() {
269
+ get theme(): Theme {
252
270
  return theme;
253
- },
271
+ }
254
272
 
255
- getAllThemes() {
273
+ getAllThemes(): { name: string; path: string | undefined }[] {
256
274
  return [];
257
- },
275
+ }
258
276
 
259
- getTheme(_name: string) {
277
+ getTheme(_name: string): Theme | undefined {
260
278
  return undefined;
261
- },
279
+ }
262
280
 
263
- setTheme(_theme: string | Theme) {
281
+ setTheme(_theme: string | Theme): { success: boolean; error?: string } {
264
282
  // Theme switching not supported in RPC mode
265
283
  return { success: false, error: "Theme switching not supported in RPC mode" };
266
- },
284
+ }
267
285
 
268
286
  setEditorComponent(): void {
269
287
  // Custom editor components not supported in RPC mode
270
- },
271
- });
288
+ }
289
+ }
272
290
 
273
291
  // Set up extensions with RPC-based UI context
274
292
  const extensionRunner = session.extensionRunner;
@@ -352,7 +370,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
352
370
  await session.compact(instructions, options);
353
371
  },
354
372
  },
355
- createExtensionUIContext(),
373
+ new RpcExtensionUIContext(pendingExtensionRequests, output),
356
374
  );
357
375
  extensionRunner.onError((err) => {
358
376
  output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
@@ -0,0 +1,76 @@
1
+ Performs patch operations on a file given a diff.
2
+ This is your primary tool for making changes to existing files.
3
+
4
+ <critical>
5
+ - Always read the target file before editing.
6
+ - Copy anchors + context lines verbatim (including whitespace).
7
+ - Output the clean patch format below.
8
+ </critical>
9
+
10
+ <parameters>
11
+ ```ts
12
+ type T =
13
+ // Diff is one or more hunks, within the same file.
14
+ // - Each hunk begins with "@@" (optionally with an anchor).
15
+ // - Each hunk body contains only lines starting with: ' ' | '+' | '-'.
16
+ // - Each hunk must include at least one real change (+ or -). No no-op hunks.
17
+ | { path: string, op: "update", diff: string }
18
+ // Diff is the full file content, no prefixes.
19
+ | { path: string, op: "create", diff: string }
20
+ // Omit diff for delete operation.
21
+ | { path: string, op: "delete" }
22
+ // New path for update-and-move operation.
23
+ | { path: string, op: "update", rename: string, diff: string }
24
+ ```
25
+ </parameters>
26
+
27
+ <hunk_header>
28
+ Allowed:
29
+ - `@@`
30
+ - `@@ $ANCHOR`
31
+
32
+ ANCHOR RULES:
33
+ - `$ANCHOR` MUST be copied verbatim from the file as either:
34
+ - a full existing line, OR
35
+ - a unique substring of a single existing line.
36
+ - NEVER use it as a comment:
37
+ - line numbers / ranges: `line 207`, `lines 26-37`
38
+ - location labels: `top of file`, `start`, `near imports`
39
+ - placeholders: `@@ @@`, `...`
40
+ </hunk_header>
41
+
42
+ <anchor_selection>
43
+ ANCHOR SELECTION ALGORITHM (use in this order):
44
+ 1) If the surrounding context lines are already unique in the file, use bare `@@`.
45
+ 2) Else choose an anchor that is highly specific and stable, copied from the file, e.g.:
46
+ - full function signature line
47
+ - class declaration line
48
+ - a unique string literal / error message
49
+ - a config key with uncommon name
50
+ 3) If you get "Found multiple matches", escalate by:
51
+ - adding more context lines, OR
52
+ - using multiple hunks with separate nearby anchors, OR
53
+ - using a more specific anchor substring (longer, includes identifiers).
54
+ NEVER use generic anchors like `import`, `export`, `describe`, `function`, `const`.
55
+ </anchor_selection>
56
+
57
+ <context_rules>
58
+ - Include enough context lines (' ' prefixed) to make the match unique (usually 2–8 total).
59
+ - Context lines must exist in the file exactly as written; preserve indentation/trailing spaces.
60
+ </context_rules>
61
+
62
+ <example name="create">
63
+ edit {"path":"hello.txt","op":"create","diff":"Hello\n"}
64
+ </example>
65
+
66
+ <example name="update">
67
+ edit {"path":"src/app.py","op":"update","diff":"@@ def greet():\n def greet():\n-print('Hi')\n+print('Hello')\n"}
68
+ </example>
69
+
70
+ <example name="rename">
71
+ edit {"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n ...\n"}
72
+ </example>
73
+
74
+ <example name="delete">
75
+ edit {"path":"obsolete.txt","op":"delete"}
76
+ </example>
@@ -5,7 +5,7 @@ Usage:
5
5
  - By default, it reads up to {{DEFAULT_MAX_LINES}} lines starting from the beginning of the file
6
6
  - You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters
7
7
  - Any lines longer than 500 characters will be truncated
8
- - By default, results include line numbers (cat -n format). Use `lines: false` to omit them
8
+ - By default, results omit line numbers. Use `lines: true` to include them
9
9
  - This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.
10
10
  - This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.
11
11
  - This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.
@@ -2,6 +2,7 @@ Performs string replacements in files with fuzzy whitespace matching.
2
2
 
3
3
  Usage:
4
4
  - Use the smallest edit that uniquely identifies the change. To toggle a checkbox: `- [ ] Task` → `- [x] Task`, not the entire line.
5
+ - If the old text is not unique, expand the replacement to a larger block (function/class/section) so it is unique.
5
6
  - You must use your read tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
6
7
  - Fuzzy matching handles minor whitespace/indentation differences automatically - you don't need to match indentation exactly.
7
8
  - ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
@@ -181,46 +181,6 @@ export async function getShellConfig(): Promise<ShellConfig> {
181
181
  return cachedShellConfig;
182
182
  }
183
183
 
184
- /**
185
- * Sanitize binary output for display/storage.
186
- * Removes characters that crash string-width or cause display issues:
187
- * - Control characters (except tab, newline, carriage return)
188
- * - Lone surrogates
189
- * - Unicode Format characters (crash string-width due to a bug)
190
- * - Characters with undefined code points
191
- */
192
- export function sanitizeBinaryOutput(str: string): string {
193
- // Use Array.from to properly iterate over code points (not code units)
194
- // This handles surrogate pairs correctly and catches edge cases where
195
- // codePointAt() might return undefined
196
- return Array.from(str)
197
- .filter((char) => {
198
- // Filter out characters that cause string-width to crash
199
- // This includes:
200
- // - Unicode format characters
201
- // - Lone surrogates (already filtered by Array.from)
202
- // - Control chars except \t \n \r
203
- // - Characters with undefined code points
204
-
205
- const code = char.codePointAt(0);
206
-
207
- // Skip if code point is undefined (edge case with invalid strings)
208
- if (code === undefined) return false;
209
-
210
- // Allow tab, newline, carriage return
211
- if (code === 0x09 || code === 0x0a || code === 0x0d) return true;
212
-
213
- // Filter out control characters (0x00-0x1F, except 0x09, 0x0a, 0x0x0d)
214
- if (code <= 0x1f) return false;
215
-
216
- // Filter out Unicode format characters
217
- if (code >= 0xfff9 && code <= 0xfffb) return false;
218
-
219
- return true;
220
- })
221
- .join("");
222
- }
223
-
224
184
  let pgrepAvailable: boolean | null = null;
225
185
 
226
186
  /**