@oh-my-pi/pi-coding-agent 13.5.1 → 13.5.3
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 +36 -0
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +2 -1
- package/src/config/settings-schema.ts +18 -0
- package/src/debug/index.ts +11 -0
- package/src/exa/mcp-client.ts +57 -2
- package/src/modes/controllers/command-controller.ts +20 -0
- package/src/modes/controllers/extension-ui-controller.ts +52 -7
- package/src/modes/interactive-mode.ts +4 -0
- package/src/modes/theme/mermaid-cache.ts +16 -57
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +41 -0
- package/src/prompts/system/plan-mode-active.md +12 -11
- package/src/prompts/system/plan-mode-subagent.md +3 -5
- package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
- package/src/prompts/tools/bash.md +6 -4
- package/src/prompts/tools/checkpoint.md +16 -0
- package/src/prompts/tools/hashline.md +26 -69
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/prompts/tools/rewind.md +13 -0
- package/src/sdk.ts +2 -0
- package/src/session/agent-session.ts +150 -3
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/tools/ask.ts +83 -51
- package/src/tools/bash.ts +5 -1
- package/src/tools/checkpoint.ts +128 -0
- package/src/tools/index.ts +31 -0
- package/src/tools/render-mermaid.ts +67 -0
- package/src/utils/prompt-format.ts +16 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.5.3] - 2026-03-01
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Auto-include `ast_grep` and `ast_edit` tools when their text-based counterparts (`grep`, `edit`) are requested and the AST tools are enabled
|
|
10
|
+
- Enforced tool decision in plan mode—agent now requires calling either `ask` or `exit_plan_mode` when a turn ends without a required tool call
|
|
11
|
+
- Auto-correction of escaped tab indentation in edits (enabled by default, controllable via `PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS` environment variable)
|
|
12
|
+
- Warning when suspicious Unicode escape placeholder `\uDDDD` is detected in edit content
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Updated bash tool description to conditionally show `ast_grep` and `ast_edit` guidance based on tool availability in the session
|
|
17
|
+
- Replaced timeout-based cancellation with AbortSignal-based cancellation in the `ask` tool for more reliable user interaction handling
|
|
18
|
+
- Updated `ask` tool to distinguish between user-initiated cancellation and timeout-driven auto-selection, with only user cancellation aborting the turn
|
|
19
|
+
- Updated hashline documentation to clarify that `\t` in JSON represents a real tab character, not a literal backslash-t sequence
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed race condition in dialog overlay handling where multiple concurrent resolutions could occur
|
|
24
|
+
- Cancelling the `ask` tool now aborts the current turn instead of returning a normal cancelled selection, while timeout-driven auto-cancel still returns without aborting
|
|
25
|
+
|
|
26
|
+
## [13.5.2] - 2026-03-01
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Added `checkpoint` tool to create context checkpoints before exploratory work, allowing you to investigate with many intermediate tool calls and minimize context cost afterward
|
|
31
|
+
- Added `rewind` tool to end an active checkpoint and replace intermediate exploration messages with a concise investigation report
|
|
32
|
+
- Added `checkpoint.enabled` setting to control availability of the checkpoint and rewind tools
|
|
33
|
+
- Added `render_mermaid` tool to convert Mermaid graph source into ASCII diagram output
|
|
34
|
+
- Added `renderMermaid.enabled` setting to control availability of the render_mermaid tool
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Changed Mermaid rendering from PNG images to ASCII diagrams in theme rendering
|
|
39
|
+
- Changed `prerenderMermaid()` function to synchronously render ASCII instead of asynchronously rendering PNG
|
|
40
|
+
|
|
5
41
|
## [13.5.0] - 2026-03-01
|
|
6
42
|
|
|
7
43
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.5.
|
|
4
|
+
"version": "13.5.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.5.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.5.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.5.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.5.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.5.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.5.3",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.5.3",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.5.3",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.5.3",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.5.3",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.5.3",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -255,7 +255,8 @@ handlebars.registerHelper("SECTION_SEPERATOR", (name: unknown): string => sectio
|
|
|
255
255
|
*/
|
|
256
256
|
function formatHashlineRef(lineNum: unknown, content: unknown): { num: number; text: string; ref: string } {
|
|
257
257
|
const num = typeof lineNum === "number" ? lineNum : Number.parseInt(String(lineNum), 10);
|
|
258
|
-
const
|
|
258
|
+
const raw = typeof content === "string" ? content : String(content ?? "");
|
|
259
|
+
const text = raw.replace(/\\t/g, "\t").replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
259
260
|
const ref = `${num}#${computeLineHash(num, text)}`;
|
|
260
261
|
return { num, text, ref };
|
|
261
262
|
}
|
|
@@ -460,11 +460,29 @@ export const SETTINGS_SCHEMA = {
|
|
|
460
460
|
description: "Enable the ast_edit tool for structural AST rewrites",
|
|
461
461
|
},
|
|
462
462
|
},
|
|
463
|
+
"renderMermaid.enabled": {
|
|
464
|
+
type: "boolean",
|
|
465
|
+
default: false,
|
|
466
|
+
ui: {
|
|
467
|
+
tab: "tools",
|
|
468
|
+
label: "Enable Render Mermaid",
|
|
469
|
+
description: "Enable the render_mermaid tool for Mermaid-to-ASCII rendering",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
463
472
|
"notebook.enabled": {
|
|
464
473
|
type: "boolean",
|
|
465
474
|
default: true,
|
|
466
475
|
ui: { tab: "tools", label: "Enable Notebook", description: "Enable the notebook tool for notebook editing" },
|
|
467
476
|
},
|
|
477
|
+
"checkpoint.enabled": {
|
|
478
|
+
type: "boolean",
|
|
479
|
+
default: false,
|
|
480
|
+
ui: {
|
|
481
|
+
tab: "tools",
|
|
482
|
+
label: "Enable Checkpoint/Rewind",
|
|
483
|
+
description: "Enable the checkpoint and rewind tools for context checkpointing",
|
|
484
|
+
},
|
|
485
|
+
},
|
|
468
486
|
"fetch.enabled": {
|
|
469
487
|
type: "boolean",
|
|
470
488
|
default: true,
|
package/src/debug/index.ts
CHANGED
|
@@ -27,6 +27,11 @@ const DEBUG_MENU_ITEMS: SelectItem[] = [
|
|
|
27
27
|
{ value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
|
|
28
28
|
{ value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
|
|
29
29
|
{ value: "system", label: "View: system info", description: "Show environment details" },
|
|
30
|
+
{
|
|
31
|
+
value: "transcript",
|
|
32
|
+
label: "Export: TUI transcript",
|
|
33
|
+
description: "Write visible TUI conversation to a temp txt",
|
|
34
|
+
},
|
|
30
35
|
{ value: "clear-cache", label: "Clear: artifact cache", description: "Remove old session artifacts" },
|
|
31
36
|
];
|
|
32
37
|
|
|
@@ -95,6 +100,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
95
100
|
case "system":
|
|
96
101
|
await this.#handleViewSystemInfo();
|
|
97
102
|
break;
|
|
103
|
+
case "transcript":
|
|
104
|
+
await this.#handleTranscriptExport();
|
|
105
|
+
break;
|
|
98
106
|
case "clear-cache":
|
|
99
107
|
await this.#handleClearCache();
|
|
100
108
|
break;
|
|
@@ -323,6 +331,9 @@ export class DebugSelectorComponent extends Container {
|
|
|
323
331
|
this.ctx.ui.requestRender();
|
|
324
332
|
}
|
|
325
333
|
|
|
334
|
+
async #handleTranscriptExport(): Promise<void> {
|
|
335
|
+
await this.ctx.handleDebugTranscriptCommand();
|
|
336
|
+
}
|
|
326
337
|
async #handleOpenArtifacts(): Promise<void> {
|
|
327
338
|
const sessionFile = this.ctx.sessionManager.getSessionFile();
|
|
328
339
|
if (!sessionFile) {
|
package/src/exa/mcp-client.ts
CHANGED
|
@@ -16,6 +16,61 @@ export function findApiKey(): string | null {
|
|
|
16
16
|
return $env.EXA_API_KEY;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
20
|
+
if (typeof value !== "object" || value === null) return null;
|
|
21
|
+
return value as Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseJsonContent(text: string): unknown | null {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize tools/call payloads across MCP servers.
|
|
34
|
+
*
|
|
35
|
+
* Exa currently returns different shapes depending on deployment/environment:
|
|
36
|
+
* - direct payload in result
|
|
37
|
+
* - structured payload under result.structuredContent / result.data / result.result
|
|
38
|
+
* - JSON payload embedded as text in result.content[]
|
|
39
|
+
*/
|
|
40
|
+
function normalizeMcpToolPayload(payload: unknown): unknown {
|
|
41
|
+
const candidates: unknown[] = [];
|
|
42
|
+
const root = asRecord(payload);
|
|
43
|
+
|
|
44
|
+
if (root) {
|
|
45
|
+
if (root.structuredContent !== undefined) candidates.push(root.structuredContent);
|
|
46
|
+
if (root.data !== undefined) candidates.push(root.data);
|
|
47
|
+
if (root.result !== undefined) candidates.push(root.result);
|
|
48
|
+
candidates.push(root);
|
|
49
|
+
|
|
50
|
+
const content = root.content;
|
|
51
|
+
if (Array.isArray(content)) {
|
|
52
|
+
for (const item of content) {
|
|
53
|
+
const part = asRecord(item);
|
|
54
|
+
if (!part) continue;
|
|
55
|
+
const text = part.text;
|
|
56
|
+
if (typeof text !== "string" || text.trim().length === 0) continue;
|
|
57
|
+
const parsed = parseJsonContent(text);
|
|
58
|
+
if (parsed !== null) candidates.push(parsed);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
candidates.push(payload);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (isSearchResponse(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return payload;
|
|
72
|
+
}
|
|
73
|
+
|
|
19
74
|
/** Fetch available tools from Exa MCP */
|
|
20
75
|
export async function fetchExaTools(apiKey: string | null, toolNames: string[]): Promise<MCPTool[]> {
|
|
21
76
|
const params = new URLSearchParams();
|
|
@@ -65,7 +120,7 @@ export async function callExaTool(
|
|
|
65
120
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
66
121
|
}
|
|
67
122
|
|
|
68
|
-
return response.result;
|
|
123
|
+
return normalizeMcpToolPayload(response.result);
|
|
69
124
|
}
|
|
70
125
|
|
|
71
126
|
/** Call a tool on Websets MCP */
|
|
@@ -85,7 +140,7 @@ export async function callWebsetsTool(
|
|
|
85
140
|
throw new Error(`MCP error: ${response.error.message}`);
|
|
86
141
|
}
|
|
87
142
|
|
|
88
|
-
return response.result;
|
|
143
|
+
return normalizeMcpToolPayload(response.result);
|
|
89
144
|
}
|
|
90
145
|
|
|
91
146
|
/** Format search results for LLM */
|
|
@@ -28,6 +28,7 @@ import type { AuthStorage } from "../../session/auth-storage";
|
|
|
28
28
|
import { createCompactionSummaryMessage } from "../../session/messages";
|
|
29
29
|
import { outputMeta } from "../../tools/output-meta";
|
|
30
30
|
import { resolveToCwd } from "../../tools/path-utils";
|
|
31
|
+
import { replaceTabs } from "../../tools/render-utils";
|
|
31
32
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog";
|
|
32
33
|
import { openPath } from "../../utils/open";
|
|
33
34
|
|
|
@@ -70,6 +71,25 @@ export class CommandController {
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
async handleDebugTranscriptCommand(): Promise<void> {
|
|
75
|
+
try {
|
|
76
|
+
const width = Math.max(1, this.ctx.ui.terminal.columns);
|
|
77
|
+
const renderedLines = this.ctx.chatContainer.render(width).map(line => replaceTabs(Bun.stripANSI(line)));
|
|
78
|
+
const rendered = renderedLines.join("\n").trimEnd();
|
|
79
|
+
if (!rendered) {
|
|
80
|
+
this.ctx.showError("No messages to dump yet.");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const tmpPath = path.join(os.tmpdir(), `${Snowflake.next()}-tmp.txt`);
|
|
84
|
+
await Bun.write(tmpPath, `${rendered}\n`);
|
|
85
|
+
this.ctx.showStatus(`Debug transcript written to:\n${tmpPath}`);
|
|
86
|
+
} catch (error: unknown) {
|
|
87
|
+
this.ctx.showError(
|
|
88
|
+
`Failed to write debug transcript: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
async handleShareCommand(): Promise<void> {
|
|
74
94
|
const tmpFile = path.join(os.tmpdir(), `${Snowflake.next()}.html`);
|
|
75
95
|
const cleanupTempFile = async () => {
|
|
@@ -41,7 +41,7 @@ export class ExtensionUiController {
|
|
|
41
41
|
const uiContext: ExtensionUIContext = {
|
|
42
42
|
select: (title, options, dialogOptions) => this.showHookSelector(title, options, dialogOptions),
|
|
43
43
|
confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
|
|
44
|
-
input: (title, placeholder,
|
|
44
|
+
input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
|
|
45
45
|
notify: (message, type) => this.showHookNotify(message, type),
|
|
46
46
|
onTerminalInput: handler => this.addExtensionTerminalInputListener(handler),
|
|
47
47
|
setStatus: (key, text) => this.setHookStatus(key, text),
|
|
@@ -561,6 +561,20 @@ export class ExtensionUiController {
|
|
|
561
561
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
562
562
|
): Promise<string | undefined> {
|
|
563
563
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
564
|
+
let settled = false;
|
|
565
|
+
const onAbort = () => {
|
|
566
|
+
this.hideHookSelector();
|
|
567
|
+
if (!settled) {
|
|
568
|
+
settled = true;
|
|
569
|
+
resolve(undefined);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
const finish = (value: string | undefined) => {
|
|
573
|
+
if (settled) return;
|
|
574
|
+
settled = true;
|
|
575
|
+
dialogOptions?.signal?.removeEventListener("abort", onAbort);
|
|
576
|
+
resolve(value);
|
|
577
|
+
};
|
|
564
578
|
this.#hookSelectorOverlay?.hide();
|
|
565
579
|
this.#hookSelectorOverlay = undefined;
|
|
566
580
|
const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
|
|
@@ -569,11 +583,11 @@ export class ExtensionUiController {
|
|
|
569
583
|
options,
|
|
570
584
|
option => {
|
|
571
585
|
this.hideHookSelector();
|
|
572
|
-
|
|
586
|
+
finish(option);
|
|
573
587
|
},
|
|
574
588
|
() => {
|
|
575
589
|
this.hideHookSelector();
|
|
576
|
-
|
|
590
|
+
finish(undefined);
|
|
577
591
|
},
|
|
578
592
|
{
|
|
579
593
|
initialIndex: dialogOptions?.initialIndex,
|
|
@@ -584,9 +598,15 @@ export class ExtensionUiController {
|
|
|
584
598
|
},
|
|
585
599
|
);
|
|
586
600
|
this.#hookSelectorOverlay = this.ctx.ui.showOverlay(this.ctx.hookSelector, this.#dialogOverlayOptions);
|
|
601
|
+
if (dialogOptions?.signal) {
|
|
602
|
+
if (dialogOptions.signal.aborted) {
|
|
603
|
+
onAbort();
|
|
604
|
+
} else {
|
|
605
|
+
dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
606
|
+
}
|
|
607
|
+
}
|
|
587
608
|
return promise;
|
|
588
609
|
}
|
|
589
|
-
|
|
590
610
|
/**
|
|
591
611
|
* Hide the hook selector.
|
|
592
612
|
*/
|
|
@@ -610,8 +630,26 @@ export class ExtensionUiController {
|
|
|
610
630
|
/**
|
|
611
631
|
* Show a text input for hooks.
|
|
612
632
|
*/
|
|
613
|
-
showHookInput(
|
|
633
|
+
showHookInput(
|
|
634
|
+
title: string,
|
|
635
|
+
placeholder?: string,
|
|
636
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
637
|
+
): Promise<string | undefined> {
|
|
614
638
|
const { promise, resolve } = Promise.withResolvers<string | undefined>();
|
|
639
|
+
let settled = false;
|
|
640
|
+
const onAbort = () => {
|
|
641
|
+
this.hideHookInput();
|
|
642
|
+
if (!settled) {
|
|
643
|
+
settled = true;
|
|
644
|
+
resolve(undefined);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
const finish = (value: string | undefined) => {
|
|
648
|
+
if (settled) return;
|
|
649
|
+
settled = true;
|
|
650
|
+
dialogOptions?.signal?.removeEventListener("abort", onAbort);
|
|
651
|
+
resolve(value);
|
|
652
|
+
};
|
|
615
653
|
this.#hookInputOverlay?.hide();
|
|
616
654
|
this.#hookInputOverlay = undefined;
|
|
617
655
|
this.ctx.hookInput = new HookInputComponent(
|
|
@@ -619,14 +657,21 @@ export class ExtensionUiController {
|
|
|
619
657
|
placeholder,
|
|
620
658
|
value => {
|
|
621
659
|
this.hideHookInput();
|
|
622
|
-
|
|
660
|
+
finish(value);
|
|
623
661
|
},
|
|
624
662
|
() => {
|
|
625
663
|
this.hideHookInput();
|
|
626
|
-
|
|
664
|
+
finish(undefined);
|
|
627
665
|
},
|
|
628
666
|
);
|
|
629
667
|
this.#hookInputOverlay = this.ctx.ui.showOverlay(this.ctx.hookInput, this.#dialogOverlayOptions);
|
|
668
|
+
if (dialogOptions?.signal) {
|
|
669
|
+
if (dialogOptions.signal.aborted) {
|
|
670
|
+
onAbort();
|
|
671
|
+
} else {
|
|
672
|
+
dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
630
675
|
return promise;
|
|
631
676
|
}
|
|
632
677
|
|
|
@@ -947,6 +947,10 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
947
947
|
return this.#commandController.handleDumpCommand();
|
|
948
948
|
}
|
|
949
949
|
|
|
950
|
+
handleDebugTranscriptCommand(): Promise<void> {
|
|
951
|
+
return this.#commandController.handleDebugTranscriptCommand();
|
|
952
|
+
}
|
|
953
|
+
|
|
950
954
|
handleShareCommand(): Promise<void> {
|
|
951
955
|
return this.#commandController.handleShareCommand();
|
|
952
956
|
}
|
|
@@ -1,88 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
extractMermaidBlocks,
|
|
3
|
-
type MermaidImage,
|
|
4
|
-
type MermaidRenderOptions,
|
|
5
|
-
renderMermaidToPng,
|
|
6
|
-
} from "@oh-my-pi/pi-tui";
|
|
7
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { extractMermaidBlocks, logger, renderMermaidAsciiSafe } from "@oh-my-pi/pi-utils";
|
|
8
2
|
|
|
9
|
-
const cache = new Map<bigint,
|
|
10
|
-
const pending = new Map<bigint, Promise<MermaidImage | null>>();
|
|
3
|
+
const cache = new Map<bigint, string>();
|
|
11
4
|
const failed = new Set<bigint>();
|
|
12
5
|
|
|
13
|
-
const defaultOptions: MermaidRenderOptions = {
|
|
14
|
-
theme: "dark",
|
|
15
|
-
backgroundColor: "transparent",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
6
|
let onRenderNeeded: (() => void) | null = null;
|
|
19
7
|
|
|
20
8
|
/**
|
|
21
|
-
* Set callback to trigger TUI re-render when mermaid
|
|
9
|
+
* Set callback to trigger TUI re-render when mermaid ASCII renders become available.
|
|
22
10
|
*/
|
|
23
11
|
export function setMermaidRenderCallback(callback: (() => void) | null): void {
|
|
24
12
|
onRenderNeeded = callback;
|
|
25
13
|
}
|
|
26
14
|
|
|
27
15
|
/**
|
|
28
|
-
* Get a pre-rendered mermaid
|
|
16
|
+
* Get a pre-rendered mermaid ASCII diagram by hash.
|
|
29
17
|
* Returns null if not cached or rendering failed.
|
|
30
18
|
*/
|
|
31
|
-
export function
|
|
19
|
+
export function getMermaidAscii(hash: bigint): string | null {
|
|
32
20
|
return cache.get(hash) ?? null;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
23
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* Calls render callback when new images are cached.
|
|
24
|
+
* Render all mermaid blocks in markdown text.
|
|
25
|
+
* Caches results and calls render callback when new diagrams are available.
|
|
39
26
|
*/
|
|
40
|
-
export
|
|
41
|
-
markdown: string,
|
|
42
|
-
options: MermaidRenderOptions = defaultOptions,
|
|
43
|
-
): Promise<void> {
|
|
27
|
+
export function prerenderMermaid(markdown: string): void {
|
|
44
28
|
const blocks = extractMermaidBlocks(markdown);
|
|
45
29
|
if (blocks.length === 0) return;
|
|
46
30
|
|
|
47
|
-
|
|
31
|
+
let hasNew = false;
|
|
48
32
|
|
|
49
33
|
for (const { source, hash } of blocks) {
|
|
50
34
|
if (cache.has(hash) || failed.has(hash)) continue;
|
|
51
35
|
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
const ascii = renderMermaidAsciiSafe(source);
|
|
37
|
+
if (ascii) {
|
|
38
|
+
cache.set(hash, ascii);
|
|
39
|
+
hasNew = true;
|
|
40
|
+
} else {
|
|
41
|
+
failed.add(hash);
|
|
56
42
|
}
|
|
57
|
-
|
|
58
|
-
promises.push(
|
|
59
|
-
promise
|
|
60
|
-
.then(image => {
|
|
61
|
-
pending.delete(hash);
|
|
62
|
-
if (image) {
|
|
63
|
-
cache.set(hash, image);
|
|
64
|
-
failed.delete(hash);
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
failed.add(hash);
|
|
68
|
-
return false;
|
|
69
|
-
})
|
|
70
|
-
.catch(error => {
|
|
71
|
-
pending.delete(hash);
|
|
72
|
-
failed.add(hash);
|
|
73
|
-
logger.warn("Mermaid render failed", {
|
|
74
|
-
hash,
|
|
75
|
-
error: error instanceof Error ? error.message : String(error),
|
|
76
|
-
});
|
|
77
|
-
return false;
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
43
|
}
|
|
81
44
|
|
|
82
|
-
|
|
83
|
-
const newImages = results.some(added => added);
|
|
84
|
-
|
|
85
|
-
if (newImages && onRenderNeeded) {
|
|
45
|
+
if (hasNew && onRenderNeeded) {
|
|
86
46
|
try {
|
|
87
47
|
onRenderNeeded();
|
|
88
48
|
} catch (error) {
|
|
@@ -107,5 +67,4 @@ export function hasPendingMermaid(markdown: string): boolean {
|
|
|
107
67
|
export function clearMermaidCache(): void {
|
|
108
68
|
cache.clear();
|
|
109
69
|
failed.clear();
|
|
110
|
-
pending.clear();
|
|
111
70
|
}
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -16,7 +16,7 @@ import chalk from "chalk";
|
|
|
16
16
|
import darkThemeJson from "./dark.json" with { type: "json" };
|
|
17
17
|
import { defaultThemes } from "./defaults";
|
|
18
18
|
import lightThemeJson from "./light.json" with { type: "json" };
|
|
19
|
-
import {
|
|
19
|
+
import { getMermaidAscii } from "./mermaid-cache";
|
|
20
20
|
|
|
21
21
|
// ============================================================================
|
|
22
22
|
// Symbol Presets
|
|
@@ -2340,7 +2340,7 @@ export function getMarkdownTheme(): MarkdownTheme {
|
|
|
2340
2340
|
underline: (text: string) => theme.underline(text),
|
|
2341
2341
|
strikethrough: (text: string) => chalk.strikethrough(text),
|
|
2342
2342
|
symbols: getSymbolTheme(),
|
|
2343
|
-
|
|
2343
|
+
getMermaidAscii,
|
|
2344
2344
|
highlightCode: (code: string, lang?: string): string[] => {
|
|
2345
2345
|
const validLang = lang && nativeSupportsLanguage(lang) ? lang : undefined;
|
|
2346
2346
|
try {
|
package/src/modes/types.ts
CHANGED
|
@@ -154,6 +154,7 @@ export interface InteractiveModeContext {
|
|
|
154
154
|
handleChangelogCommand(showFull?: boolean): Promise<void>;
|
|
155
155
|
handleHotkeysCommand(): void;
|
|
156
156
|
handleDumpCommand(): Promise<void>;
|
|
157
|
+
handleDebugTranscriptCommand(): Promise<void>;
|
|
157
158
|
handleClearCommand(): Promise<void>;
|
|
158
159
|
handleForkCommand(): Promise<void>;
|
|
159
160
|
handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
|
package/src/patch/hashline.ts
CHANGED
|
@@ -411,6 +411,45 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
|
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
|
|
414
|
+
function isEscapedTabAutocorrectEnabled(): boolean {
|
|
415
|
+
const value = Bun.env.PI_HASHLINE_AUTOCORRECT_ESCAPED_TABS;
|
|
416
|
+
if (value === "0") return false;
|
|
417
|
+
if (value === "1") return true;
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function maybeAutocorrectEscapedTabIndentation(edits: HashlineEdit[], warnings: string[]): void {
|
|
422
|
+
if (!isEscapedTabAutocorrectEnabled()) return;
|
|
423
|
+
for (const edit of edits) {
|
|
424
|
+
if (edit.lines.length === 0) continue;
|
|
425
|
+
const hasEscapedTabs = edit.lines.some(line => line.includes("\\t"));
|
|
426
|
+
if (!hasEscapedTabs) continue;
|
|
427
|
+
const hasRealTabs = edit.lines.some(line => line.includes("\t"));
|
|
428
|
+
if (hasRealTabs) continue;
|
|
429
|
+
let correctedCount = 0;
|
|
430
|
+
const corrected = edit.lines.map(line =>
|
|
431
|
+
line.replace(/^((?:\\t)+)/, escaped => {
|
|
432
|
+
correctedCount += escaped.length / 2;
|
|
433
|
+
return "\t".repeat(escaped.length / 2);
|
|
434
|
+
}),
|
|
435
|
+
);
|
|
436
|
+
if (correctedCount === 0) continue;
|
|
437
|
+
edit.lines = corrected;
|
|
438
|
+
warnings.push(
|
|
439
|
+
`Auto-corrected escaped tab indentation in edit: converted leading \\t sequence(s) to real tab characters`,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function maybeWarnSuspiciousUnicodeEscapePlaceholder(edits: HashlineEdit[], warnings: string[]): void {
|
|
445
|
+
for (const edit of edits) {
|
|
446
|
+
if (edit.lines.length === 0) continue;
|
|
447
|
+
if (!edit.lines.some(line => /\\uDDDD/i.test(line))) continue;
|
|
448
|
+
warnings.push(
|
|
449
|
+
`Detected literal \\uDDDD in edit content; no autocorrection applied. Verify whether this should be a real Unicode escape or plain text.`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
414
453
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
415
454
|
// Edit Application
|
|
416
455
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -493,6 +532,8 @@ export function applyHashlineEdits(
|
|
|
493
532
|
if (mismatches.length > 0) {
|
|
494
533
|
throw new HashlineMismatchError(mismatches, fileLines);
|
|
495
534
|
}
|
|
535
|
+
maybeAutocorrectEscapedTabIndentation(edits, warnings);
|
|
536
|
+
maybeWarnSuspiciousUnicodeEscapePlaceholder(edits, warnings);
|
|
496
537
|
// Deduplicate identical edits targeting the same line(s)
|
|
497
538
|
const seenEditKeys = new Map<string, number>();
|
|
498
539
|
const dedupIndices = new Set<number>();
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
Plan mode active. You **MUST** perform READ-ONLY operations only.
|
|
3
3
|
|
|
4
4
|
You **MUST NOT**:
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
5
|
+
- Create, edit, or delete files (except plan file below)
|
|
6
|
+
- Run state-changing commands (git commit, npm install, etc.)
|
|
7
|
+
- Make any system changes
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
To implement: call `{{exitToolName}}` → user approves → new session starts with full write access to execute the plan.
|
|
10
|
+
You **MUST NOT** ask the user to exit plan mode for you; you **MUST** call `{{exitToolName}}` yourself.
|
|
10
11
|
</critical>
|
|
11
12
|
|
|
12
13
|
## Plan File
|
|
@@ -32,7 +33,7 @@ Plan execution runs in fresh context (session cleared). You **MUST** make the pl
|
|
|
32
33
|
3. Decide:
|
|
33
34
|
- **Different task** → Overwrite plan
|
|
34
35
|
- **Same task, continuing** → Update and clean outdated sections
|
|
35
|
-
4. Call `
|
|
36
|
+
4. Call `{{exitToolName}}` when complete
|
|
36
37
|
</procedure>
|
|
37
38
|
{{/if}}
|
|
38
39
|
|
|
@@ -43,7 +44,7 @@ Plan execution runs in fresh context (session cleared). You **MUST** make the pl
|
|
|
43
44
|
### 1. Explore
|
|
44
45
|
You **MUST** use `find`, `grep`, `read`, `ls` to understand the codebase.
|
|
45
46
|
### 2. Interview
|
|
46
|
-
You **MUST** use `
|
|
47
|
+
You **MUST** use `{{askToolName}}` to clarify:
|
|
47
48
|
- Ambiguous requirements
|
|
48
49
|
- Technical decisions and tradeoffs
|
|
49
50
|
- Preferences: UI/UX, performance, edge cases
|
|
@@ -78,7 +79,7 @@ You **MUST** focus on the request and associated code. You **SHOULD** launch par
|
|
|
78
79
|
You **MUST** draft an approach based on exploration. You **MUST** consider trade-offs briefly, then choose.
|
|
79
80
|
|
|
80
81
|
### Phase 3: Review
|
|
81
|
-
You **MUST** read critical files. You **MUST** verify plan matches original request. You **SHOULD** use `
|
|
82
|
+
You **MUST** read critical files. You **MUST** verify plan matches original request. You **SHOULD** use `{{askToolName}}` to clarify remaining questions.
|
|
82
83
|
|
|
83
84
|
### Phase 4: Update Plan
|
|
84
85
|
You **MUST** update `{{planFilePath}}` (`{{editToolName}}` for changes, `{{writeToolName}}` only if creating from scratch):
|
|
@@ -93,14 +94,14 @@ You **MUST** ask questions throughout. You **MUST NOT** make large assumptions a
|
|
|
93
94
|
{{/if}}
|
|
94
95
|
|
|
95
96
|
<directives>
|
|
96
|
-
- You **MUST** use `
|
|
97
|
+
- You **MUST** use `{{askToolName}}` only for clarifying requirements or choosing approaches
|
|
97
98
|
</directives>
|
|
98
99
|
|
|
99
100
|
<critical>
|
|
100
101
|
Your turn ends ONLY by:
|
|
101
|
-
1. Using `
|
|
102
|
-
2. Calling `
|
|
102
|
+
1. Using `{{askToolName}}` to gather information, OR
|
|
103
|
+
2. Calling `{{exitToolName}}` when ready — this triggers user approval, then a new implementation session with full tool access
|
|
103
104
|
|
|
104
|
-
You **MUST NOT** ask plan approval via text or `
|
|
105
|
+
You **MUST NOT** ask plan approval via text or `{{askToolName}}`; you **MUST** use `{{exitToolName}}`.
|
|
105
106
|
You **MUST** keep going until complete.
|
|
106
107
|
</critical>
|
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
Plan mode active. You **MUST** perform READ-ONLY operations only.
|
|
3
3
|
|
|
4
4
|
You **MUST NOT**:
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
Supersedes all other instructions.
|
|
5
|
+
- Create, edit, delete, move, or copy files
|
|
6
|
+
- Run state-changing commands
|
|
7
|
+
- Make any changes to the system
|
|
10
8
|
</critical>
|
|
11
9
|
|
|
12
10
|
<role>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<system-reminder>
|
|
2
|
+
Plan mode turn ended without a required tool call.
|
|
3
|
+
|
|
4
|
+
You **MUST** choose exactly one next action now:
|
|
5
|
+
1. Call `{{askToolName}}` to gather required clarification, OR
|
|
6
|
+
2. Call `{{exitToolName}}` to finish planning and request approval
|
|
7
|
+
|
|
8
|
+
You **MUST NOT** output plain text in this turn.
|
|
9
|
+
</system-reminder>
|