@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.
- package/CHANGELOG.md +59 -0
- package/package.json +19 -19
- package/src/cli/args.ts +10 -1
- package/src/cli/shell-cli.ts +15 -3
- package/src/config/settings-schema.ts +60 -1
- package/src/dap/session.ts +8 -2
- package/src/debug/system-info.ts +6 -2
- package/src/discovery/claude.ts +58 -36
- package/src/discovery/opencode.ts +20 -2
- package/src/edit/index.ts +3 -1
- package/src/edit/modes/chunk.ts +133 -53
- package/src/edit/modes/hashline.ts +36 -11
- package/src/edit/renderer.ts +98 -133
- package/src/edit/streaming.ts +351 -0
- package/src/exec/bash-executor.ts +60 -5
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/pi-protocol.ts +0 -2
- package/src/lsp/client.ts +22 -6
- package/src/lsp/defaults.json +2 -1
- package/src/lsp/index.ts +53 -10
- package/src/lsp/types.ts +2 -0
- package/src/modes/acp/acp-agent.ts +76 -2
- package/src/modes/components/assistant-message.ts +1 -34
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/tool-execution.ts +111 -101
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +0 -2
- package/src/modes/theme/mermaid-cache.ts +13 -52
- package/src/modes/theme/theme.ts +2 -2
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/chunk-edit.md +25 -22
- package/src/prompts/tools/gh-pr-push.md +2 -1
- package/src/prompts/tools/grep.md +4 -3
- package/src/prompts/tools/lsp.md +6 -0
- package/src/prompts/tools/read-chunk.md +46 -7
- package/src/prompts/tools/read.md +7 -4
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +36 -20
- package/src/session/session-manager.ts +228 -57
- package/src/session/streaming-output.ts +11 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +1 -0
- package/src/tools/ast-edit.ts +37 -2
- package/src/tools/bash.ts +75 -12
- package/src/tools/find.ts +19 -26
- package/src/tools/gh.ts +6 -16
- package/src/tools/grep.ts +94 -37
- package/src/tools/path-utils.ts +31 -3
- package/src/tools/resolve.ts +12 -3
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/tools/vim.ts +1 -1
- 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
|
-
//
|
|
115
|
-
#
|
|
116
|
-
#
|
|
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.#
|
|
191
|
+
this.#schedulePreviewDiff(0);
|
|
186
192
|
}
|
|
187
193
|
|
|
188
194
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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
|
-
#
|
|
193
|
-
if (!
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
this.#editDiffAbort?.abort();
|
|
243
|
+
const controller = new AbortController();
|
|
244
|
+
this.#editDiffAbort = controller;
|
|
251
245
|
|
|
252
|
-
|
|
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
|
-
})
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
638
|
+
const previews = this.#editDiffPreview;
|
|
639
|
+
if (!previews || previews.length === 0) {
|
|
650
640
|
return this.#args;
|
|
651
641
|
}
|
|
652
|
-
|
|
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
|
-
|
|
684
|
-
|
|
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 {
|
|
1
|
+
import { renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
2
2
|
|
|
3
|
-
const cache = new Map<
|
|
3
|
+
const cache = new Map<string, string | null>();
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
/**
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
|
31
|
-
-
|
|
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,
|
|
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
|
|
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
|
-
**
|
|
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`, `
|
|
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
|
-
|`
|
|
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", "
|
|
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
|
|
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
|
-
-
|
|
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
|
|
25
|
-
-
|
|
26
|
-
- If
|
|
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>
|
package/src/prompts/tools/lsp.md
CHANGED
|
@@ -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>
|