@oh-my-pi/pi-coding-agent 14.2.1 → 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/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 +2 -1
- package/src/edit/modes/chunk.ts +132 -56
- 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 +8 -1
- package/src/lsp/defaults.json +2 -1
- 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/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/bash.ts +13 -0
- package/src/tools/gh.ts +6 -16
- package/src/tools/sqlite-reader.ts +116 -3
- package/src/web/search/providers/codex.ts +129 -6
|
@@ -45,7 +45,6 @@ export class PiProtocolHandler implements ProtocolHandler {
|
|
|
45
45
|
content,
|
|
46
46
|
contentType: "text/markdown",
|
|
47
47
|
size: Buffer.byteLength(content, "utf-8"),
|
|
48
|
-
sourcePath: "pi://",
|
|
49
48
|
};
|
|
50
49
|
}
|
|
51
50
|
|
|
@@ -78,7 +77,6 @@ export class PiProtocolHandler implements ProtocolHandler {
|
|
|
78
77
|
content,
|
|
79
78
|
contentType: "text/markdown",
|
|
80
79
|
size: Buffer.byteLength(content, "utf-8"),
|
|
81
|
-
sourcePath: `pi://${normalized}`,
|
|
82
80
|
};
|
|
83
81
|
}
|
|
84
82
|
}
|
package/src/lsp/client.ts
CHANGED
|
@@ -484,7 +484,14 @@ export async function getOrCreateClient(config: ServerConfig, cwd: string, initT
|
|
|
484
484
|
|
|
485
485
|
// Reject any pending requests — the server is gone, they will never complete.
|
|
486
486
|
if (client.pendingRequests.size > 0) {
|
|
487
|
-
|
|
487
|
+
// Strip informational log lines (e.g. marksman's [INF]/[DBG] prefix)
|
|
488
|
+
// — they are startup noise, not actionable errors.
|
|
489
|
+
const rawStderr = proc.peekStderr().trim();
|
|
490
|
+
const stderr = rawStderr
|
|
491
|
+
.split("\n")
|
|
492
|
+
.filter(line => !/^\[\d{2}:\d{2}:\d{2} (?:INF|DBG|VRB)\]/.test(line))
|
|
493
|
+
.join("\n")
|
|
494
|
+
.trim();
|
|
488
495
|
const code = proc.exitCode;
|
|
489
496
|
const err = new Error(
|
|
490
497
|
stderr ? `LSP server exited (code ${code}): ${stderr}` : `LSP server exited unexpectedly (code ${code})`,
|
package/src/lsp/defaults.json
CHANGED
|
@@ -39,11 +39,14 @@ import {
|
|
|
39
39
|
} from "@agentclientprotocol/sdk";
|
|
40
40
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
41
41
|
import { logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
42
|
+
import { disableProvider, enableProvider } from "../../capability";
|
|
43
|
+
import { Settings } from "../../config/settings";
|
|
42
44
|
import type { ExtensionUIContext } from "../../extensibility/extensions";
|
|
43
45
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
44
46
|
import { loadSlashCommands } from "../../extensibility/slash-commands";
|
|
45
47
|
import { MCPManager } from "../../mcp/manager";
|
|
46
48
|
import type { MCPServerConfig } from "../../mcp/types";
|
|
49
|
+
import { loadAllExtensions } from "../../modes/components/extensions/state-manager";
|
|
47
50
|
import { theme } from "../../modes/theme/theme";
|
|
48
51
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
49
52
|
import {
|
|
@@ -379,8 +382,79 @@ export class AcpAgent implements Agent {
|
|
|
379
382
|
}
|
|
380
383
|
}
|
|
381
384
|
|
|
382
|
-
async extMethod(
|
|
383
|
-
|
|
385
|
+
async extMethod(method: string, params: { [key: string]: unknown }): Promise<{ [key: string]: unknown }> {
|
|
386
|
+
switch (method) {
|
|
387
|
+
case "omp/sessions/listAll": {
|
|
388
|
+
const limit = typeof params.limit === "number" ? Math.max(1, Math.min(5000, params.limit as number)) : 1000;
|
|
389
|
+
const sessions = await SessionManager.listAll();
|
|
390
|
+
const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
|
|
391
|
+
return {
|
|
392
|
+
sessions: sorted.map(s => this.#toSessionInfo(s)),
|
|
393
|
+
total: sessions.length,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
case "omp/projects/list": {
|
|
397
|
+
const sessions = await SessionManager.listAll();
|
|
398
|
+
const buckets = new Map<
|
|
399
|
+
string,
|
|
400
|
+
{ cwd: string; sessionCount: number; lastActivityAt: number; lastTitle: string }
|
|
401
|
+
>();
|
|
402
|
+
for (const s of sessions) {
|
|
403
|
+
if (!s.cwd) continue;
|
|
404
|
+
const ts = s.modified.getTime();
|
|
405
|
+
const existing = buckets.get(s.cwd);
|
|
406
|
+
if (existing) {
|
|
407
|
+
existing.sessionCount += 1;
|
|
408
|
+
if (ts > existing.lastActivityAt) {
|
|
409
|
+
existing.lastActivityAt = ts;
|
|
410
|
+
existing.lastTitle = s.title ?? "";
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
buckets.set(s.cwd, {
|
|
414
|
+
cwd: s.cwd,
|
|
415
|
+
sessionCount: 1,
|
|
416
|
+
lastActivityAt: ts,
|
|
417
|
+
lastTitle: s.title ?? "",
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const projects = Array.from(buckets.values()).sort((a, b) => b.lastActivityAt - a.lastActivityAt);
|
|
422
|
+
return { projects, totalSessions: sessions.length };
|
|
423
|
+
}
|
|
424
|
+
case "omp/chats/byCwd": {
|
|
425
|
+
const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
|
|
426
|
+
if (!cwd) throw new Error("cwd required");
|
|
427
|
+
const limit = typeof params.limit === "number" ? Math.max(1, Math.min(500, params.limit as number)) : 100;
|
|
428
|
+
const sessions = await SessionManager.list(cwd);
|
|
429
|
+
const sorted = sessions.sort((l, r) => r.modified.getTime() - l.modified.getTime()).slice(0, limit);
|
|
430
|
+
return { sessions: sorted.map(s => this.#toSessionInfo(s)) };
|
|
431
|
+
}
|
|
432
|
+
case "omp/usage": {
|
|
433
|
+
const [firstRecord] = this.#sessions.values();
|
|
434
|
+
const target = firstRecord?.session ?? this.#initialSession;
|
|
435
|
+
const reports = await target.fetchUsageReports();
|
|
436
|
+
return { reports: reports ?? [] };
|
|
437
|
+
}
|
|
438
|
+
case "omp/extensions": {
|
|
439
|
+
const cwd = typeof params.cwd === "string" ? (params.cwd as string) : undefined;
|
|
440
|
+
const sm = await Settings.init();
|
|
441
|
+
const disabledIds = (sm.get("disabledExtensions") as string[] | undefined) ?? [];
|
|
442
|
+
const extensions = await loadAllExtensions(cwd, disabledIds);
|
|
443
|
+
return { extensions: extensions as unknown as Array<{ [key: string]: unknown }> };
|
|
444
|
+
}
|
|
445
|
+
case "omp/extensions/toggle": {
|
|
446
|
+
const providerId = params.providerId;
|
|
447
|
+
if (typeof providerId !== "string") throw new Error("providerId required");
|
|
448
|
+
if (params.enabled === false) {
|
|
449
|
+
disableProvider(providerId);
|
|
450
|
+
return { enabled: false };
|
|
451
|
+
}
|
|
452
|
+
enableProvider(providerId);
|
|
453
|
+
return { enabled: true };
|
|
454
|
+
}
|
|
455
|
+
default:
|
|
456
|
+
throw new Error(`Unknown ACP ext method: ${method}`);
|
|
457
|
+
}
|
|
384
458
|
}
|
|
385
459
|
|
|
386
460
|
async extNotification(_method: string, _params: { [key: string]: unknown }): Promise<void> {}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { formatNumber
|
|
3
|
+
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { settings } from "../../config/settings";
|
|
5
|
-
import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache";
|
|
6
5
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
6
|
import { resolveImageOptions } from "../../tools/render-utils";
|
|
8
7
|
|
|
@@ -12,7 +11,6 @@ import { resolveImageOptions } from "../../tools/render-utils";
|
|
|
12
11
|
export class AssistantMessageComponent extends Container {
|
|
13
12
|
#contentContainer: Container;
|
|
14
13
|
#lastMessage?: AssistantMessage;
|
|
15
|
-
#prerenderInFlight = false;
|
|
16
14
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
17
15
|
#usageInfo?: Usage;
|
|
18
16
|
|
|
@@ -85,34 +83,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
85
83
|
this.#contentContainer.addChild(new Text(theme.fg("toolOutput", `[Image: ${image.mimeType}]`), 1, 0));
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
|
-
#triggerMermaidPrerender(message: AssistantMessage): void {
|
|
89
|
-
if (!TERMINAL.imageProtocol || this.#prerenderInFlight) return;
|
|
90
|
-
|
|
91
|
-
// Check if any text content has pending mermaid blocks
|
|
92
|
-
const hasPending = message.content.some(c => c.type === "text" && c.text.trim() && hasPendingMermaid(c.text));
|
|
93
|
-
if (!hasPending) return;
|
|
94
|
-
|
|
95
|
-
this.#prerenderInFlight = true;
|
|
96
|
-
|
|
97
|
-
// Fire off background prerender
|
|
98
|
-
void (async () => {
|
|
99
|
-
try {
|
|
100
|
-
for (const content of message.content) {
|
|
101
|
-
if (content.type === "text" && content.text.trim() && hasPendingMermaid(content.text)) {
|
|
102
|
-
prerenderMermaid(content.text);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
} catch (error) {
|
|
106
|
-
logger.warn("Background mermaid prerender failed", {
|
|
107
|
-
error: error instanceof Error ? error.message : String(error),
|
|
108
|
-
});
|
|
109
|
-
} finally {
|
|
110
|
-
this.#prerenderInFlight = false;
|
|
111
|
-
// Invalidate to re-render with cached images
|
|
112
|
-
this.invalidate();
|
|
113
|
-
}
|
|
114
|
-
})();
|
|
115
|
-
}
|
|
116
86
|
|
|
117
87
|
updateContent(message: AssistantMessage): void {
|
|
118
88
|
this.#lastMessage = message;
|
|
@@ -120,9 +90,6 @@ export class AssistantMessageComponent extends Container {
|
|
|
120
90
|
// Clear content container
|
|
121
91
|
this.#contentContainer.clear();
|
|
122
92
|
|
|
123
|
-
// Trigger background mermaid pre-rendering if needed
|
|
124
|
-
this.#triggerMermaidPrerender(message);
|
|
125
|
-
|
|
126
93
|
const hasVisibleContent = message.content.some(
|
|
127
94
|
c => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()),
|
|
128
95
|
);
|
|
@@ -136,7 +136,7 @@ export class HookEditorComponent extends Container {
|
|
|
136
136
|
const editorCmd = getEditorCommand();
|
|
137
137
|
if (!editorCmd) return;
|
|
138
138
|
|
|
139
|
-
const currentText = this.#editor.
|
|
139
|
+
const currentText = this.#editor.getExpandedText();
|
|
140
140
|
try {
|
|
141
141
|
this.#tui.stop();
|
|
142
142
|
const result = await openInEditor(editorCmd, currentText);
|
|
@@ -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}}
|
|
@@ -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
|