@oh-my-pi/pi-coding-agent 14.2.0 → 14.3.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 (54) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/package.json +19 -19
  3. package/src/cli/args.ts +10 -1
  4. package/src/cli/shell-cli.ts +15 -3
  5. package/src/config/settings-schema.ts +60 -1
  6. package/src/dap/session.ts +8 -2
  7. package/src/debug/system-info.ts +6 -2
  8. package/src/discovery/claude.ts +58 -36
  9. package/src/discovery/opencode.ts +20 -2
  10. package/src/edit/index.ts +3 -1
  11. package/src/edit/modes/chunk.ts +133 -53
  12. package/src/edit/modes/hashline.ts +36 -11
  13. package/src/edit/renderer.ts +98 -133
  14. package/src/edit/streaming.ts +351 -0
  15. package/src/exec/bash-executor.ts +60 -5
  16. package/src/internal-urls/docs-index.generated.ts +5 -5
  17. package/src/internal-urls/pi-protocol.ts +0 -2
  18. package/src/lsp/client.ts +22 -6
  19. package/src/lsp/defaults.json +2 -1
  20. package/src/lsp/index.ts +53 -10
  21. package/src/lsp/types.ts +2 -0
  22. package/src/modes/acp/acp-agent.ts +76 -2
  23. package/src/modes/components/assistant-message.ts +1 -34
  24. package/src/modes/components/hook-editor.ts +1 -1
  25. package/src/modes/components/tool-execution.ts +111 -101
  26. package/src/modes/controllers/input-controller.ts +1 -1
  27. package/src/modes/interactive-mode.ts +0 -2
  28. package/src/modes/theme/mermaid-cache.ts +13 -52
  29. package/src/modes/theme/theme.ts +2 -2
  30. package/src/prompts/system/system-prompt.md +1 -1
  31. package/src/prompts/tools/ast-grep.md +1 -0
  32. package/src/prompts/tools/browser.md +1 -0
  33. package/src/prompts/tools/chunk-edit.md +25 -22
  34. package/src/prompts/tools/gh-pr-push.md +2 -1
  35. package/src/prompts/tools/grep.md +4 -3
  36. package/src/prompts/tools/lsp.md +6 -0
  37. package/src/prompts/tools/read-chunk.md +46 -7
  38. package/src/prompts/tools/read.md +7 -4
  39. package/src/sdk.ts +8 -5
  40. package/src/session/agent-session.ts +36 -20
  41. package/src/session/session-manager.ts +228 -57
  42. package/src/session/streaming-output.ts +11 -0
  43. package/src/system-prompt.ts +7 -2
  44. package/src/task/executor.ts +1 -0
  45. package/src/tools/ast-edit.ts +37 -2
  46. package/src/tools/bash.ts +75 -12
  47. package/src/tools/find.ts +19 -26
  48. package/src/tools/gh.ts +6 -16
  49. package/src/tools/grep.ts +94 -37
  50. package/src/tools/path-utils.ts +31 -3
  51. package/src/tools/resolve.ts +12 -3
  52. package/src/tools/sqlite-reader.ts +116 -3
  53. package/src/tools/vim.ts +1 -1
  54. package/src/web/search/providers/codex.ts +129 -6
@@ -14,14 +14,7 @@ import {
14
14
  type TUI,
15
15
  } from "@oh-my-pi/pi-tui";
16
16
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
17
- import {
18
- computeEditDiff,
19
- computeHashlineDiff,
20
- computePatchDiff,
21
- type DiffError,
22
- type DiffResult,
23
- expandApplyPatchToEntries,
24
- } from "../../edit";
17
+ import { EDIT_MODE_STRATEGIES, type EditMode, type PerFileDiffPreview } from "../../edit";
25
18
  import type { Theme } from "../../modes/theme/theme";
26
19
  import { theme } from "../../modes/theme/theme";
27
20
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
@@ -65,6 +58,12 @@ function isEditLikeToolName(toolName: string): boolean {
65
58
  return toolName === "edit" || toolName === "apply_patch";
66
59
  }
67
60
 
61
+ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined): EditMode | undefined {
62
+ if (toolName === "apply_patch") return "apply_patch";
63
+ if (toolName !== "edit") return undefined;
64
+ return (tool as { mode?: EditMode } | undefined)?.mode;
65
+ }
66
+
68
67
  export interface ToolExecutionOptions {
69
68
  showImages?: boolean; // default: true (only used if terminal supports images)
70
69
  editFuzzyThreshold?: number;
@@ -111,9 +110,12 @@ export class ToolExecutionComponent extends Container {
111
110
  isError?: boolean;
112
111
  details?: any;
113
112
  };
114
- // Cached edit diff preview (computed when args arrive, before tool executes)
115
- #editDiffPreview?: DiffResult | DiffError;
116
- #editDiffArgsKey?: string; // Track which args the preview is for
113
+ // Edit preview state (single-file for legacy modes, multi-file for chunk)
114
+ #editMode?: EditMode;
115
+ #editDiffPreview?: PerFileDiffPreview[];
116
+ #editDiffScheduleTimer?: NodeJS.Timeout;
117
+ #editDiffAbort?: AbortController;
118
+ #editDiffLastArgsKey?: string;
117
119
  // Cached converted images for Kitty protocol (which requires PNG), keyed by index
118
120
  #convertedImages: Map<number, { data: string; mimeType: string }> = new Map();
119
121
  // Spinner animation for partial task results
@@ -166,116 +168,98 @@ export class ToolExecutionComponent extends Container {
166
168
  this.addChild(this.#contentText);
167
169
  }
168
170
 
171
+ this.#editMode = resolveEditModeForTool(toolName, tool);
172
+
169
173
  this.#updateDisplay();
174
+ this.#schedulePreviewDiff(0);
170
175
  }
171
176
 
172
177
  updateArgs(args: any, _toolCallId?: string): void {
173
178
  this.#args = cloneToolArgs(args);
174
179
  this.#updateSpinnerAnimation();
180
+ this.#schedulePreviewDiff();
175
181
  this.#updateDisplay();
176
182
  }
177
183
 
178
184
  /**
179
185
  * Signal that args are complete (tool is about to execute).
180
- * This triggers diff computation for edit-like tools.
186
+ * This triggers an immediate final diff computation for edit-like tools.
181
187
  */
182
188
  setArgsComplete(_toolCallId?: string): void {
183
189
  this.#argsComplete = true;
184
190
  this.#updateSpinnerAnimation();
185
- this.#maybeComputeEditDiff();
191
+ this.#schedulePreviewDiff(0);
186
192
  }
187
193
 
188
194
  /**
189
- * Compute edit diff preview when we have complete args.
190
- * This runs async and updates display when done.
195
+ * Schedule a debounced compute of the streaming edit-diff preview.
196
+ * `delayMs === 0` runs immediately (used on construction and on
197
+ * `setArgsComplete`). All other calls coalesce to a trailing-edge timer.
191
198
  */
192
- #maybeComputeEditDiff(): void {
193
- if (!isEditLikeToolName(this.#toolName)) return;
194
-
195
- const edits = this.#args?.edits;
196
- if (!Array.isArray(edits) || edits.length === 0) {
197
- if (this.#toolName !== "apply_patch" || typeof this.#args?.input !== "string") {
198
- return;
199
- }
200
-
201
- const input = this.#args.input;
202
- const argsKey = JSON.stringify({ input });
203
- if (this.#editDiffArgsKey === argsKey) return;
204
- this.#editDiffArgsKey = argsKey;
205
-
206
- try {
207
- const first = expandApplyPatchToEntries({ input })[0];
208
- if (!first?.path) return;
209
- computePatchDiff({ ...first, op: first.op ?? "update" }, this.#cwd, {
210
- fuzzyThreshold: this.#editFuzzyThreshold,
211
- allowFuzzy: this.#editAllowFuzzy,
212
- }).then(result => {
213
- if (this.#editDiffArgsKey === argsKey) {
214
- this.#editDiffPreview = result;
215
- this.#updateDisplay();
216
- this.#ui.requestRender();
217
- }
218
- });
219
- } catch (err) {
220
- this.#editDiffPreview = { error: err instanceof Error ? err.message : String(err) };
221
- this.#updateDisplay();
222
- this.#ui.requestRender();
223
- }
199
+ #schedulePreviewDiff(delayMs = 80): void {
200
+ if (!this.#editMode) return;
201
+ if (this.#editDiffScheduleTimer) {
202
+ clearTimeout(this.#editDiffScheduleTimer);
203
+ this.#editDiffScheduleTimer = undefined;
204
+ }
205
+ if (delayMs === 0) {
206
+ void this.#runPreviewDiff();
224
207
  return;
225
208
  }
209
+ this.#editDiffScheduleTimer = setTimeout(() => {
210
+ this.#editDiffScheduleTimer = undefined;
211
+ void this.#runPreviewDiff();
212
+ }, delayMs);
213
+ }
226
214
 
227
- const first = edits[0];
228
- if (!first || typeof first !== "object") return;
229
-
230
- // Detect mode from first edit entry shape and compute preview for first file
231
- if ("old_text" in first && "new_text" in first) {
232
- // Replace mode
233
- const { path, old_text: oldText, new_text: newText, all } = first;
234
- if (!path || oldText === undefined || newText === undefined) return;
235
-
236
- const argsKey = JSON.stringify({ path, oldText, newText, all });
237
- if (this.#editDiffArgsKey === argsKey) return;
238
- this.#editDiffArgsKey = argsKey;
215
+ async #runPreviewDiff(): Promise<void> {
216
+ const editMode = this.#editMode;
217
+ if (!editMode) return;
218
+ const strategy = EDIT_MODE_STRATEGIES[editMode];
219
+ if (!strategy) return;
220
+
221
+ const args = this.#args;
222
+ if (args == null || typeof args !== "object") return;
223
+
224
+ const partialJson = (args as { __partialJson?: string }).__partialJson;
225
+ let effectiveArgs: unknown;
226
+ try {
227
+ effectiveArgs = strategy.extractCompleteEdits(args, partialJson);
228
+ } catch {
229
+ effectiveArgs = args;
230
+ }
239
231
 
240
- computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result =>
241
- this.#applyEditDiffResult(argsKey, result),
242
- );
243
- } else if ("path" in first && ("diff" in first || ("op" in first && !("content" in first)))) {
244
- // Patch mode (has diff or op without content — chunk edits always have content)
245
- const { path, op, rename, diff } = first;
246
- if (!path) return;
232
+ // Coalesce duplicate computes for identical args.
233
+ let argsKey: string;
234
+ try {
235
+ argsKey = JSON.stringify(effectiveArgs);
236
+ } catch {
237
+ argsKey = String(Date.now());
238
+ }
239
+ if (argsKey === this.#editDiffLastArgsKey) return;
240
+ this.#editDiffLastArgsKey = argsKey;
247
241
 
248
- const argsKey = JSON.stringify({ path, op, rename, diff });
249
- if (this.#editDiffArgsKey === argsKey) return;
250
- this.#editDiffArgsKey = argsKey;
242
+ this.#editDiffAbort?.abort();
243
+ const controller = new AbortController();
244
+ this.#editDiffAbort = controller;
251
245
 
252
- computePatchDiff({ path, op, rename, diff }, this.#cwd, {
246
+ try {
247
+ const previews = await strategy.computeDiffPreview(effectiveArgs, {
248
+ cwd: this.#cwd,
249
+ signal: controller.signal,
253
250
  fuzzyThreshold: this.#editFuzzyThreshold,
254
251
  allowFuzzy: this.#editAllowFuzzy,
255
- }).then(result => this.#applyEditDiffResult(argsKey, result));
256
- } else if ("loc" in first && "path" in first) {
257
- // Hashline mode — group edits by path, preview first file
258
- const path = first.path;
259
- if (!path) return;
260
- const fileEdits = edits.filter((e: any) => e.path === path);
261
- const move = this.#args?.move;
262
-
263
- const argsKey = JSON.stringify({ path, edits: fileEdits, move });
264
- if (this.#editDiffArgsKey === argsKey) return;
265
- this.#editDiffArgsKey = argsKey;
266
-
267
- computeHashlineDiff({ path, edits: fileEdits, move }, this.#cwd).then(result =>
268
- this.#applyEditDiffResult(argsKey, result),
269
- );
252
+ });
253
+ if (controller.signal.aborted) return;
254
+ if (previews) {
255
+ this.#editDiffPreview = previews;
256
+ this.#updateDisplay();
257
+ this.#ui.requestRender();
258
+ }
259
+ } catch (err) {
260
+ if (controller.signal.aborted) return;
261
+ logger.warn("Edit preview diff failed", { tool: this.#toolName, error: String(err) });
270
262
  }
271
- // Chunk mode edits don't have a pre-execution diff preview
272
- }
273
-
274
- #applyEditDiffResult(argsKey: string, result: DiffResult | DiffError): void {
275
- if (this.#editDiffArgsKey !== argsKey) return;
276
- this.#editDiffPreview = result;
277
- this.#updateDisplay();
278
- this.#ui.requestRender();
279
263
  }
280
264
 
281
265
  updateResult(
@@ -378,6 +362,12 @@ export class ToolExecutionComponent extends Container {
378
362
  this.#spinnerInterval = undefined;
379
363
  this.#spinnerFrame = undefined;
380
364
  }
365
+ if (this.#editDiffScheduleTimer) {
366
+ clearTimeout(this.#editDiffScheduleTimer);
367
+ this.#editDiffScheduleTimer = undefined;
368
+ }
369
+ this.#editDiffAbort?.abort();
370
+ this.#editDiffAbort = undefined;
381
371
  }
382
372
 
383
373
  setExpanded(expanded: boolean): void {
@@ -549,6 +539,9 @@ export class ToolExecutionComponent extends Container {
549
539
  this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
550
540
  this.#contentBox.clear();
551
541
 
542
+ const renderContext = this.#buildRenderContext();
543
+ this.#renderState.renderContext = renderContext;
544
+
552
545
  const shouldRenderCall = !this.#result || !renderer.mergeCallAndResult;
553
546
  if (shouldRenderCall) {
554
547
  // Render call component
@@ -567,10 +560,6 @@ export class ToolExecutionComponent extends Container {
567
560
  // Render result component if we have a result
568
561
  if (this.#result) {
569
562
  try {
570
- // Build render context for tools that need extra state
571
- const renderContext = this.#buildRenderContext();
572
- this.#renderState.renderContext = renderContext;
573
-
574
563
  const resultComponent = renderer.renderResult(
575
564
  {
576
565
  content: this.#result.content as any,
@@ -646,10 +635,20 @@ export class ToolExecutionComponent extends Container {
646
635
  if (!isEditLikeToolName(this.#toolName)) {
647
636
  return this.#args;
648
637
  }
649
- if (!this.#editDiffPreview || !("diff" in this.#editDiffPreview) || !this.#editDiffPreview.diff) {
638
+ const previews = this.#editDiffPreview;
639
+ if (!previews || previews.length === 0) {
650
640
  return this.#args;
651
641
  }
652
- return { ...(this.#args as Record<string, unknown>), previewDiff: this.#editDiffPreview.diff };
642
+ // Single-file previews feed the existing `previewDiff` channel consumed
643
+ // by `formatStreamingDiff` in the renderer. Multi-file previews are
644
+ // piped via `renderContext.perFileDiffPreview`, so the args we hand to
645
+ // `renderCall` only need the first file's diff to preserve prior
646
+ // single-file behavior.
647
+ const first = previews[0];
648
+ if (!first?.diff) {
649
+ return this.#args;
650
+ }
651
+ return { ...(this.#args as Record<string, unknown>), previewDiff: first.diff };
653
652
  }
654
653
 
655
654
  /**
@@ -680,8 +679,19 @@ export class ToolExecutionComponent extends Container {
680
679
  context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
681
680
  context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 600);
682
681
  } else if (isEditLikeToolName(this.#toolName)) {
683
- // Edit needs diff preview and renderDiff function
684
- context.editDiffPreview = this.#editDiffPreview;
682
+ context.editMode = this.#editMode;
683
+ const previews = this.#editDiffPreview;
684
+ if (previews && previews.length > 0) {
685
+ const first = previews[0];
686
+ if (first?.diff || first?.error) {
687
+ context.editDiffPreview = first.error
688
+ ? { error: first.error }
689
+ : { diff: first.diff ?? "", firstChangedLine: first.firstChangedLine };
690
+ }
691
+ if (previews.length > 1) {
692
+ context.perFileDiffPreview = previews;
693
+ }
694
+ }
685
695
  context.renderDiff = renderDiff;
686
696
  }
687
697
 
@@ -715,7 +715,7 @@ export class InputController {
715
715
  return;
716
716
  }
717
717
 
718
- const currentText = this.ctx.editor.getText();
718
+ const currentText = this.ctx.editor.getExpandedText?.() ?? this.ctx.editor.getText();
719
719
 
720
720
  let ttyHandle: fs.FileHandle | null = null;
721
721
  try {
@@ -63,7 +63,6 @@ import { SelectorController } from "./controllers/selector-controller";
63
63
  import { SSHCommandController } from "./controllers/ssh-command-controller";
64
64
  import { OAuthManualInputManager } from "./oauth-manual-input";
65
65
  import { SessionObserverRegistry } from "./session-observer-registry";
66
- import { setMermaidRenderCallback } from "./theme/mermaid-cache";
67
66
  import type { Theme } from "./theme/theme";
68
67
  import {
69
68
  getEditorTheme,
@@ -220,7 +219,6 @@ export class InteractiveMode implements InteractiveModeContext {
220
219
 
221
220
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"));
222
221
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
223
- setMermaidRenderCallback(() => this.ui.requestRender());
224
222
  this.chatContainer = new Container();
225
223
  this.pendingMessagesContainer = new Container();
226
224
  this.statusContainer = new Container();
@@ -1,63 +1,24 @@
1
- import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
1
+ import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
2
2
 
3
- const cache = new Map<bigint | number, string | null>();
3
+ const cache = new Map<string, string | null>();
4
4
 
5
- let onRenderNeeded: (() => void) | null = null;
6
-
7
- /**
8
- * Set callback to trigger TUI re-render when mermaid ASCII renders become available.
9
- */
10
- export function setMermaidRenderCallback(callback: (() => void) | null): void {
11
- onRenderNeeded = callback;
12
- }
13
-
14
- /**
15
- * Get a pre-rendered mermaid ASCII diagram by hash.
16
- * Returns null if not cached or rendering failed.
17
- */
18
- export function getMermaidAscii(hash: bigint | number): string | null {
19
- return cache.get(hash) ?? null;
5
+ function normalizeMermaidSource(source: string): string {
6
+ return source.replace(/\r\n?/g, "\n").trim();
20
7
  }
21
8
 
22
9
  /**
23
- * Render all mermaid blocks in markdown text.
24
- * Caches results and calls render callback when new diagrams are available.
10
+ * Resolve mermaid ASCII from fenced block source text.
11
+ * Returns null when rendering fails, while memoizing failures to avoid repeated work.
25
12
  */
26
- export function prerenderMermaid(markdown: string): void {
27
- const blocks = extractMermaidBlocks(markdown);
28
- if (blocks.length === 0) return;
29
-
30
- let hasNew = false;
31
-
32
- for (const { source, hash } of blocks) {
33
- if (cache.has(hash)) continue;
34
-
35
- const ascii = renderMermaidAsciiSafe(source);
36
- if (ascii) {
37
- cache.set(hash, ascii);
38
- hasNew = true;
39
- } else {
40
- cache.set(hash, null);
41
- }
13
+ export function resolveMermaidAscii(source: string): string | null {
14
+ const normalizedSource = normalizeMermaidSource(source);
15
+ if (cache.has(normalizedSource)) {
16
+ return cache.get(normalizedSource) ?? null;
42
17
  }
43
18
 
44
- if (hasNew && onRenderNeeded) {
45
- try {
46
- onRenderNeeded();
47
- } catch (error) {
48
- logger.warn("Mermaid render callback failed", {
49
- error: error instanceof Error ? error.message : String(error),
50
- });
51
- }
52
- }
53
- }
54
-
55
- /**
56
- * Check if markdown contains mermaid blocks that aren't cached yet.
57
- */
58
- export function hasPendingMermaid(markdown: string): boolean {
59
- const blocks = extractMermaidBlocks(markdown);
60
- return blocks.some(({ hash }) => !cache.has(hash));
19
+ const ascii = normalizedSource ? renderMermaidAsciiSafe(normalizedSource) : null;
20
+ cache.set(normalizedSource, ascii);
21
+ return ascii;
61
22
  }
62
23
 
63
24
  /**
@@ -18,7 +18,7 @@ import chalk from "chalk";
18
18
  import darkThemeJson from "./dark.json" with { type: "json" };
19
19
  import { defaultThemes } from "./defaults";
20
20
  import lightThemeJson from "./light.json" with { type: "json" };
21
- import { getMermaidAscii } from "./mermaid-cache";
21
+ import { resolveMermaidAscii } from "./mermaid-cache";
22
22
 
23
23
  export { getLanguageFromPath } from "../../utils/lang-from-path";
24
24
 
@@ -2339,7 +2339,7 @@ export function getMarkdownTheme(): MarkdownTheme {
2339
2339
  underline: (text: string) => theme.underline(text),
2340
2340
  strikethrough: (text: string) => chalk.strikethrough(text),
2341
2341
  symbols: getSymbolTheme(),
2342
- getMermaidAscii,
2342
+ resolveMermaidAscii,
2343
2343
  highlightCode: (code: string, lang?: string): string[] => {
2344
2344
  const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
2345
2345
  try {
@@ -187,7 +187,7 @@ You **MUST NOT** use Python or Bash when a specialized tool exists.
187
187
  {{/ifAny}}
188
188
 
189
189
  {{#ifAny (includes tools "read") (includes tools "write") (includes tools "grep") (includes tools "find") (includes tools "edit")}}
190
- {{#has tools "read"}}- Use `read`, not `cat` or `open`.{{/has}}
190
+ {{#has tools "read"}}- Use `read`, not `cat`.{{/has}}
191
191
  {{#has tools "write"}}- Use `write`, not shell redirection.{{/has}}
192
192
  {{#has tools "grep"}}- Use `grep`, not shell regex search.{{/has}}
193
193
  {{#has tools "find"}}- Use `find`, not shell file globbing.{{/has}}
@@ -10,6 +10,7 @@ Performs structural code search using AST matching via native ast-grep.
10
10
  - Metavariable names are UPPERCASE and must be the whole AST node — partial-text like `prefix$VAR`, `"hello $NAME"`, or `a $OP b` does NOT work; match the whole node instead
11
11
  - When the same metavariable appears twice, both occurrences **MUST** match identical code (`$A == $A` matches `x == x`, not `x == y`)
12
12
  - Patterns **MUST** parse as a single valid AST node for the target language. For method fragments or body snippets that don't parse standalone, wrap in valid context (e.g. `class $_ { … }`) and set `sel` to target the inner node — results return for the selected node, not the outer wrapper. If ast-grep reports `Multiple AST nodes are detected`, the pattern isn't a single parseable node — wrap and use `sel`
13
+ - C++ qualified calls used as expression statements need the statement semicolon in the pattern: use `ns::doThing($ARG);`, `$CALLEE($ARG)`, or wrap a statement snippet and select `call_expression`. Without `;`, tree-sitter-cpp may parse `ns::doThing($ARG)` as declaration-like syntax and return no matches
13
14
  - For TS declarations/methods, tolerate unknown annotations: `async function $NAME($$$ARGS): $_ { $$$BODY }` or `class $_ { method($ARG: $_): $_ { $$$BODY } }`
14
15
  - Declaration forms are structurally distinct — top-level `function foo`, class method `foo()`, and `const foo = () => {}` are different AST shapes; search the right form before concluding absence
15
16
  - Loosest existence check: `pat: ["executeBash"]` with `sel: "identifier"`
@@ -1,6 +1,7 @@
1
1
  Navigates, clicks, types, scrolls, drags, queries DOM content, and captures screenshots.
2
2
 
3
3
  <instruction>
4
+ - For fetching static web content (articles, docs, issues/PRs, JSON, PDFs, feeds), prefer the `read` tool with a URL — it returns clean reader-mode text without spinning up a browser. Use this tool only when you need JS execution, authentication, or interactive actions.
4
5
  - `"open"` starts a headless session (or implicitly on first action); `"goto"` navigates to `url`; `"close"` releases the browser
5
6
  - `"observe"` captures a numbered accessibility snapshot — prefer `click_id`/`type_id`/`fill_id` using returned `element_id` values; flags: `include_all`, `viewport_only`
6
7
  - `"click"`, `"type"`, `"fill"`, `"press"`, `"scroll"`, `"drag"` for selector-based interactions — prefer ARIA/text selectors (`p-aria/[name="Sign in"]`, `p-text/Continue`) over brittle CSS
@@ -1,13 +1,14 @@
1
- Edits files via syntax-aware chunks. Run `read(path="file.ts")` first.
1
+ Edits files via syntax-aware chunks. Use `read(path="file.ts")` to read and discover chunks before editing.
2
+ - `read` is the canonical read path for chunk source and `sel="?"` tree listings.
2
3
  - `write` rewrites the entire targeted region — best for most edits.
3
- - `replace` does surgical find-and-replace within a chunk — use when making small changes to a large chunk, or batching multiple substitutions.
4
4
  - `insert` adds content before/after a chunk.
5
+ - `delete` deletes a targeted chunk and must be explicit.
5
6
 
6
7
  Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
7
8
 
8
9
  <rules>
9
- - **MUST** `read` first. Never invent chunk paths or IDs. Copy them from the latest `read` output or edit response.
10
- - `path` format: `file:selector` — e.g. `src/app.ts:fn_foo#ABCD~`. Append `~` for body, `^` for head, or nothing for the whole chunk. Include `#ID` for `put`/`find`+`replace`/`delete`.
10
+ - **MUST** inspect first with `read`. Never invent chunk paths or IDs. Copy them from the latest `read` output or edit response.
11
+ - `path` format: `file:selector` — e.g. `src/app.ts:fn_foo#ABCD~`. Append `~` for body, `^` for head, or nothing for the whole chunk. Include `#ID` for `write`/`delete`.
11
12
  - If the exact chunk path is unclear, run `read(path="file", sel="?")` and copy a selector from that listing.
12
13
  {{#if chunkAutoIndent}}
13
14
  - Use `\t` for indentation in `content`. Write content at indent-level 0 — the tool re-indents it to match the chunk's position in the file. For example, to replace `~` of a method, write the body starting at column 0:
@@ -16,6 +17,8 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
16
17
  ```
17
18
  The tool adds the correct base indent automatically. Never manually pad with the chunk's own indentation.
18
19
  Multiple sibling body lines at the same level all start at column 0: `"print(a)\nprint(b)\nprint(c)\n"`. Only use `\t` when nesting deeper (e.g. `"if cond:\n\tinner\nouter\n"`).
20
+ Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0 and use one `\t` per relative nesting level.
21
+ Multi-line replacements use the same relative-indentation model: the replacement text is dedented, then re-indented to the matched source line. Do not include the chunk's base indentation in replacement text.
19
22
  **Common mistake** when replacing `~` of a function body: do NOT include the function's own indentation.
20
23
  Wrong: `"if b == 0:\n\t\treturn None\n\treturn a / b\n"` — adds the function's base `\t` to every line.
21
24
  Correct: `"if b == 0:\n\treturn None\nreturn a / b\n"` — `if` and `return a / b` at column 0, only `return None` gets `\t` for nesting.
@@ -26,10 +29,15 @@ Call format: `{"edits": [{"path": "file:chunk#ID~", "write": "new body"}, …]}`
26
29
  content: "if (x) {\n return true;\n}"
27
30
  ```
28
31
  The tool adds the correct base indent automatically, then preserves the tabs/spaces you used inside the snippet. Never manually pad with the chunk's own indentation.
32
+ Before applying the target's base indent, the tool strips any common leading whitespace shared by all non-empty `write` lines as a safety net. Do not rely on that cleanup for mixed indentation; write `~` bodies at column 0.
33
+ Multi-line replacements use the same relative-indentation model: the replacement text is dedented, then re-indented to the matched source line. Do not include the chunk's base indentation in replacement text.
29
34
  {{/if}}
30
- - Region suffixes only apply to container chunks (classes, functions, impl blocks, sections). On leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` silently fall back to whole-chunk replacement — prefer the unsuffixed form and always supply the complete replacement (condition + body, not just the body) to avoid dropping structural parts.
31
- - `put`, `find`+`replace`, and `delete` require the current ID. `prepend`/`append` do not.
35
+ - Region suffixes only apply to chunks with a real head/body boundary (classes, functions, impl blocks, and similar containers). On code leaf chunks (enum variants, fields, single statements, and compound statements like `if`/`for`/`while`/`match`/`try`), `~` and `^` are rejected. Use the unsuffixed selector and supply the complete replacement content, or edit the parent container's `~` body.
36
+ - Unsuffixed `write` on a leaf chunk uses your content verbatim after normal replacement; it is not a body-region rewrite. Include the exact indentation and punctuation the leaf needs in the file.
37
+ - `^` head writes and `~` body writes use the same base-indent model: write content at column 0 relative to the target region, and the tool applies the chunk's file indentation.
38
+ - `write` and `delete` require the current ID. `prepend`/`append` do not.
32
39
  - **IDs change after every edit.** The edit response always carries the new IDs — use those for the next call or run `read(path="file", sel="?")` to refresh. Never reuse an ID from before the latest edit.
40
+ - Same-file edit batches are transactional: if any operation in that file fails, no changes from that file's batch are saved. Multi-file edit calls run per file, so a later file error does not roll back earlier files that already succeeded.
33
41
  </rules>
34
42
 
35
43
  <critical>
@@ -42,24 +50,25 @@ You **MUST** use the narrowest region that covers your change. Putting without a
42
50
 
43
51
  <regions>
44
52
  In `read` output, lines marked `^` between the line number and `|` are **head** lines (doc comments, attributes/decorators, signature). Lines without `^` are **body** lines. Use this to decide which region to target:
45
- - `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On leaf chunks, falls back to whole chunk.
53
+ - `fn_foo#ID~` — **body only (the default choice for most edits).** Head lines (`^`) are preserved automatically — doc comments, attributes, and signature stay untouched. On code leaf chunks, this is rejected because there is no safe body boundary.
46
54
  - `fn_foo#ID^` — head only (decorators, attributes, doc comments, signature, opening delimiter). Body stays untouched.
47
55
  - `fn_foo#ID` — entire chunk including leading trivia. **You must include doc comments and attributes in `content`; omitting them deletes them.**
48
- - `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*.
56
+ - `chunk~` + `append`/`prepend` inserts *inside* the container. `chunk` + `append`/`prepend` inserts *outside*. Appending to a container without `~` emits a warning because it lands after the closing delimiter, not before it.
49
57
 
50
- **Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. If you need to rewrite a decorator, check the `?` listing for a sibling `chunk#ID` directly above your target.
58
+ **Note on leading trivia:** whether a decorator/doc comment belongs to `^` depends on the parser. In Rust and Python, attributes and decorators are attached to the function chunk, so `^` covers them. In TypeScript/JavaScript, a `@decorator` + `/** jsdoc */` block immediately above a method often surfaces as a **separate sibling chunk** (shown as `chunk#ID` in the `?` listing) rather than as part of the function's `^`. JSDoc directly above a plain function is more likely to be absorbed into that function's `^`. If you need to rewrite a decorated member, run `read(path="file", sel="?")` and check for a sibling `chunk#ID` directly above your target.
51
59
 
52
- **Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, fenced code blocks, frontmatter), `^` and `~` fall back to the whole chunk. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers) in your `content` omitting them deletes them. For markdown sections (`sect_*`), always use unsuffixed whole-chunk replace — `^` and `~` on section containers also fall back to whole-chunk replace. When editing fenced code blocks in markdown, use the exact whitespace from the file (read with `raw` first) — the tool preserves literal indentation inside fenced blocks, but any content you supply is written verbatim. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
60
+ **Python notes:** Python docstrings are body lines, not head lines. A `~` body write on a function that has a docstring deletes the docstring unless you include the docstring in `content`. Python enum members and nested functions/closures are often opaque inside their parent chunk and may not appear as addressable child chunks; rewrite the parent container body. Python decorated class/function `^` writes and Python `^` deletes are rejected because indentation-sensitive bodies can become attached to the wrong block while still parsing.
61
+
62
+ **Note on non-code formats:** for prose and data formats (markdown, YAML, JSON, frontmatter), unsupported `^` and `~` suffixes warn and fall back to whole-chunk editing. Always replace the entire chunk and include any delimiter syntax (fence backticks, `---` frontmatter markers, list markers, table rows, headings) in your `content` — omitting them deletes them. For markdown sections (`sect_*`), prefer unsuffixed whole-chunk replace because `^`/`~` on prose sections can replace the heading and child content too; if you only need the heading, target the heading child chunk shown in `sel="?"`. Fenced code blocks with a declared language are parsed again and can expose inner chunks such as `code_py#ID.fn_gre#ID`; target those inner chunks when available. Markdown root writes preserve fenced code indentation verbatim. Recognized pipe tables expose `row_N` children for row-level edits; table cells and list items are not independently addressable, so rewrite the whole list/table chunk for those structural changes. Appending a table-row-shaped string (`| value |`) to a table chunk inserts it before the trailing blank-line separator so it remains part of the table. Otherwise read with `raw` first and preserve the exact whitespace inside fences. To insert content after a markdown section heading, use `after` on the heading chunk (`sect_*.chunk` or `sect_*.chunk_1`) — not `before`/`prepend` on the section itself, which lands physically before the heading and gets absorbed by the preceding section on reparse.
53
63
  </regions>
54
64
 
55
65
  <ops>
56
- Each edit entry has `path` (`file:selector`) plus **exactly one** operation field — `write`, `replace`, or `insert`. Never set more than one on the same entry.
66
+ Each edit entry has `path` (`file:selector`) plus **exactly one** operation field — `write`, `insert`, or `delete`. Never set more than one on the same entry. `write:null`, `write:""`, and bare `{path}` entries are rejected; they do not delete.
57
67
 
58
68
  |fields|path (selector part)|effect|
59
69
  |---|---|---|
60
70
  |`write: "content"`|`file:chunk#ID`, `file:chunk#ID~`, or `file:chunk#ID^`|write complete new content to the region|
61
- |`write: null`|`file:chunk#ID`|delete the chunk|
62
- |`replace: {old, new}`|`file:chunk#ID`|find a literal substring in the chunk and replace it|
71
+ |`delete: true`|`file:chunk#ID`|delete the chunk explicitly|
63
72
  |`insert: {loc, body}`|`file:chunk` or `file:chunk~`|insert before/after the chunk (`loc`: `"prepend"` or `"append"`)|
64
73
  </ops>
65
74
 
@@ -185,12 +194,6 @@ Result — the head (all `^` lines + opening brace) changes, body untouched:
185
194
  }
186
195
  ```
187
196
 
188
- **Find and replace** (surgical edit within a chunk):
189
- ```
190
- { "path": "counter.rs:impl_Counte.fn_increm#MNHV", "replace": { "old": "self.value += 1;", "new": "self.value = (self.value + 1).min(self.max);" } }
191
- ```
192
- Result — only the matched substring changes, everything else is preserved.
193
-
194
197
  **Insert before a chunk** (`prepend`):
195
198
  ```
196
199
  { "path": "counter.rs:impl_Counte.fn_get", "insert": { "loc": "prepend", "body": "/// Resets the counter to zero.\npub fn reset(&mut self) {\n\tself.value = 0;\n}\n\n" } }
@@ -258,7 +261,7 @@ Result — a new method is added at the end of the impl body, before the closing
258
261
 
259
262
  **Delete a chunk**:
260
263
  ```
261
- { "path": "counter.rs:impl_Counte.fn_decrem#TTWB", "write": null }
264
+ { "path": "counter.rs:impl_Counte.fn_decrem#TTWB", "delete": true }
262
265
  ```
263
266
  Result — the method (including its doc comment and signature) is removed.
264
267
  - Indentation rules (important):
@@ -268,12 +271,12 @@ Result — the method (including its doc comment and signature) is removed.
268
271
  - Match the file's real indentation characters in your snippet. The tool preserves your literal tabs/spaces after adding the target region's base indent.
269
272
  {{/if}}
270
273
  - Do NOT include the chunk's base indentation — only indent relative to the region's opening level.
274
+ - For `write`, the tool strips common leading whitespace shared by all non-empty lines, then adds the target region's base indent. If lines have mixed relative indentation, write them at column 0 so the common-margin cleanup cannot change the structure.
271
275
  - For `~` of a function: write at column 0, and use `\t` for *relative* nesting. Flat body: `"return x;\n"`. Multiple sibling lines: `"print(a)\nprint(b)\nprint(c)\n"` — all at column 0, the tool adds the function's base indent. Nested body: `"if (cond) {\n\treturn x;\n}\n"` — the `if` is at column 0, the `return` is one tab in. Python example — to replace `~` of `def divide(a, b):`, write: `"if b == 0:\n\treturn None\nreturn a / b\n"` — the `if` and `return a / b` are at column 0, `return None` is one `\t` in.
272
- - For `^`: write at the chunk's own depth. A class member's head uses `"/// doc\n#[attr]\npub fn start() {"`.
276
+ - For `^`: write at column 0 relative to the head region, just like `~`. A class member's head uses `"/// doc\n#[attr]\npub fn start() {"` — do not include the class/member base indentation.
273
277
  {{#if chunkAutoIndent}}
274
278
  - For a top-level item: start at zero indent. Write `"fn foo() {\n\treturn 1;\n}\n"`.
275
279
  {{else}}
276
280
  - For a top-level item: start at zero indent. Write `"fn foo() {\n return 1;\n}\n"`.
277
281
  {{/if}}
278
- - The tool strips common leading indentation from your content as a safety net, so accidental over-indentation is corrected.
279
282
  </examples>
@@ -2,7 +2,8 @@ Pushes a checked-out pull request branch back to its source branch through local
2
2
 
3
3
  <instruction>
4
4
  - Defaults to the current checked-out git branch
5
- - Uses branch metadata recorded by `gh_pr_checkout` to push back to the contributor fork and PR head branch
5
+ - Requires branch metadata recorded by `gh_pr_checkout`; fail instead of pushing if the branch was not checked out with `gh_pr_checkout`
6
+ - Pushes back to the contributor fork and PR head branch recorded in that metadata
6
7
  - Use `forceWithLease` only when rewriting the branch intentionally
7
8
  </instruction>
8
9
 
@@ -21,7 +21,8 @@ Searches files using powerful regex matching.
21
21
  </output>
22
22
 
23
23
  <critical>
24
- - You **MUST** use Grep when searching for content.
25
- - You **MUST NOT** invoke `grep` or `rg` via Bash.
26
- - If the search is open-ended, requiring multiple rounds, you **MUST** use Task tool with explore subagent instead.
24
+ - You **MUST** use the built-in Grep tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
25
+ - Bash `grep`/`rg` returns raw text without chunk paths, loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The Grep tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
26
+ - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the search through the Grep tool instead.
27
+ - If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining Grep calls yourself.
27
28
  </critical>
@@ -31,3 +31,9 @@ Interacts with Language Server Protocol servers for code intelligence.
31
31
  - Glob expansion samples up to 20 files per request; use `file: "*"` for broader coverage
32
32
  - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `occurrence` values return an explicit error instead of silently falling back
33
33
  </caution>
34
+
35
+ <critical>
36
+ - You **MUST** use `lsp` for symbol-aware operations (rename, find references, go to definition/implementation, code actions) whenever a language server is available — it is safer and more accurate than text-based alternatives.
37
+ - You **MUST NOT** perform cross-file renames with `ast_edit`, `sed`, `rsed`, or manual edits when `lsp` `rename` can do it. Text-based renames miss shadowing, re-exports, and usages in other files.
38
+ - Prefer `lsp` `code_actions` for imports, quick-fixes, and refactors the language server already knows how to apply.
39
+ </critical>