@selesai/code 0.10.1 → 0.11.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 +23 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/defaults/settings.json +0 -3
- package/dist/extensions/auto-session-name.test.ts +157 -0
- package/dist/extensions/auto-session-name.ts +96 -0
- package/dist/extensions/cost-reconcile.test.ts +76 -0
- package/dist/extensions/cost-reconcile.ts +278 -0
- package/dist/extensions/enable-readonly-tools.test.ts +11 -0
- package/dist/extensions/enable-readonly-tools.ts +7 -3
- package/dist/extensions/package.json +3 -0
- package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
- package/dist/extensions/pi-tool-display/LICENSE +21 -0
- package/dist/extensions/pi-tool-display/README.md +417 -0
- package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
- package/dist/extensions/pi-tool-display/index.ts +3 -0
- package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
- package/dist/extensions/pi-tool-display/package.json +86 -0
- package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
- package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
- package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
- package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
- package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
- package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
- package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
- package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
- package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
- package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
- package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
- package/dist/extensions/pi-tool-display/src/index.ts +109 -0
- package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
- package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
- package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
- package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
- package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
- package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
- package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
- package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
- package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
- package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
- package/dist/extensions/pi-tool-display/src/types.ts +109 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
- package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
- package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
- package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
- package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
- package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
- package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
- package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
- package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
- package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
- package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
- package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
- package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
- package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
- package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
- package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
- package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
- package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
- package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +85 -122
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Box, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { renderEditDiffResult } from "../src/diff-renderer.ts";
|
|
5
|
+
|
|
6
|
+
const theme = {
|
|
7
|
+
fg: (_color: string, text: string): string => text,
|
|
8
|
+
bold: (text: string): string => text,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function stripAnsi(text: string): string {
|
|
12
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderInsideToolBox(component: Component, width: number): string[] {
|
|
16
|
+
const box = new Box(1, 1);
|
|
17
|
+
box.addChild(component);
|
|
18
|
+
return box.render(width).map(stripAnsi);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractDisplayedLineNumber(line: string): string | null {
|
|
22
|
+
const match = line.match(/^\s*(?:[▌+\-]?\s*)(\d*)\s+│/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return match[1] ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function countDisplayedLineNumber(lines: string[], lineNumber: string): number {
|
|
30
|
+
return lines.reduce((count, line) => count + (extractDisplayedLineNumber(line) === lineNumber ? 1 : 0), 0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function extractUnifiedDiffRows(lines: string[]): string[] {
|
|
34
|
+
return lines.filter((line) => extractDisplayedLineNumber(line) !== null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function splitRenderedRow(line: string, width: number): { left: string; right: string } {
|
|
38
|
+
const separatorWidth = 3;
|
|
39
|
+
const leftWidth = Math.floor((width - separatorWidth) / 2);
|
|
40
|
+
return {
|
|
41
|
+
left: line.slice(0, leftWidth),
|
|
42
|
+
right: line.slice(leftWidth + separatorWidth),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractSplitDiffRows(lines: string[], width: number): Array<{ left: string; right: string }> {
|
|
47
|
+
return lines
|
|
48
|
+
.map((line) => splitRenderedRow(line, width))
|
|
49
|
+
.filter(({ left, right }) => extractDisplayedLineNumber(left) !== null || extractDisplayedLineNumber(right) !== null);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractCellContent(cell: string): string {
|
|
53
|
+
const dividerIndex = cell.indexOf("│ ");
|
|
54
|
+
if (dividerIndex === -1) {
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
return cell.slice(dividerIndex + 2).trimEnd();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("unified diff wrapped continuation rows should not repeat line numbers", () => {
|
|
61
|
+
const width = 60;
|
|
62
|
+
const component = renderEditDiffResult(
|
|
63
|
+
{
|
|
64
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-this is a very long line that exceeds the available width and will wrap to multiple rows\n",
|
|
65
|
+
},
|
|
66
|
+
{ expanded: false, filePath: "demo.txt" },
|
|
67
|
+
{
|
|
68
|
+
diffViewMode: "unified",
|
|
69
|
+
diffSplitMinWidth: 80,
|
|
70
|
+
diffCollapsedLines: 24,
|
|
71
|
+
diffWordWrap: true,
|
|
72
|
+
} as any,
|
|
73
|
+
theme,
|
|
74
|
+
"",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const lines = renderInsideToolBox(component, width);
|
|
78
|
+
|
|
79
|
+
assert.strictEqual(
|
|
80
|
+
countDisplayedLineNumber(lines, "1"),
|
|
81
|
+
1,
|
|
82
|
+
`Expected wrapped unified rows to show line number 1 only once.\n${lines.join("\n")}`,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("split diff falls back to unified with the same continuation numbering behavior", () => {
|
|
87
|
+
const width = 50;
|
|
88
|
+
const component = renderEditDiffResult(
|
|
89
|
+
{
|
|
90
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-this is a very long line that will definitely need to wrap when rendered in narrow panes\n",
|
|
91
|
+
},
|
|
92
|
+
{ expanded: false, filePath: "demo.txt" },
|
|
93
|
+
{
|
|
94
|
+
diffViewMode: "split",
|
|
95
|
+
diffSplitMinWidth: 80,
|
|
96
|
+
diffCollapsedLines: 24,
|
|
97
|
+
diffWordWrap: true,
|
|
98
|
+
} as any,
|
|
99
|
+
theme,
|
|
100
|
+
"",
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const lines = renderInsideToolBox(component, width);
|
|
104
|
+
|
|
105
|
+
assert.strictEqual(
|
|
106
|
+
countDisplayedLineNumber(lines, "1"),
|
|
107
|
+
1,
|
|
108
|
+
`Expected split fallback rows to show line number 1 only once.\n${lines.join("\n")}`,
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("multi-line unified diff keeps continuation rows blank while preserving logical line numbers", () => {
|
|
113
|
+
const width = 50;
|
|
114
|
+
const component = renderEditDiffResult(
|
|
115
|
+
{
|
|
116
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1,2 +1,2 @@\n-first very long line that will definitely need to wrap for sure\n-second very long line also wrapping because it is too long\n+first replacement also very long and needs to wrap\n+second replacement line that wraps too\n",
|
|
117
|
+
},
|
|
118
|
+
{ expanded: false, filePath: "demo.txt" },
|
|
119
|
+
{
|
|
120
|
+
diffViewMode: "unified",
|
|
121
|
+
diffSplitMinWidth: 80,
|
|
122
|
+
diffCollapsedLines: 24,
|
|
123
|
+
diffWordWrap: true,
|
|
124
|
+
} as any,
|
|
125
|
+
theme,
|
|
126
|
+
"",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const lines = renderInsideToolBox(component, width);
|
|
130
|
+
const diffRows = extractUnifiedDiffRows(lines);
|
|
131
|
+
const displayedNumbers = diffRows.map(extractDisplayedLineNumber);
|
|
132
|
+
|
|
133
|
+
assert.ok(diffRows.length > 4, `Expected wrapped unified output to span multiple visual rows.\n${lines.join("\n")}`);
|
|
134
|
+
assert.strictEqual(countDisplayedLineNumber(diffRows, "1"), 2, `Expected logical line 1 once per side.\n${diffRows.join("\n")}`);
|
|
135
|
+
assert.strictEqual(countDisplayedLineNumber(diffRows, "2"), 2, `Expected logical line 2 once per side.\n${diffRows.join("\n")}`);
|
|
136
|
+
assert.ok(displayedNumbers.includes(""), `Expected at least one continuation row with a blank line number.\n${diffRows.join("\n")}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("split diff keeps wrapped rows vertically aligned with blank placeholder cells", () => {
|
|
140
|
+
const width = 120;
|
|
141
|
+
const component = renderEditDiffResult(
|
|
142
|
+
{
|
|
143
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-this is a very long original line that will wrap across multiple visual rows in split mode\n+short replacement\n",
|
|
144
|
+
},
|
|
145
|
+
{ expanded: false, filePath: "demo.txt" },
|
|
146
|
+
{
|
|
147
|
+
diffViewMode: "split",
|
|
148
|
+
diffSplitMinWidth: 80,
|
|
149
|
+
diffCollapsedLines: 24,
|
|
150
|
+
diffWordWrap: true,
|
|
151
|
+
} as any,
|
|
152
|
+
theme,
|
|
153
|
+
"",
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const lines = renderInsideToolBox(component, width);
|
|
157
|
+
const diffRows = extractSplitDiffRows(lines, width);
|
|
158
|
+
const firstRow = diffRows[0];
|
|
159
|
+
const secondRow = diffRows[1];
|
|
160
|
+
|
|
161
|
+
assert.ok(firstRow, `Expected at least one split diff row.\n${lines.join("\n")}`);
|
|
162
|
+
assert.ok(secondRow, `Expected a continuation row for the wrapped left side.\n${lines.join("\n")}`);
|
|
163
|
+
assert.strictEqual(extractDisplayedLineNumber(firstRow.left), "1", `Expected first left cell to show line number 1.\n${firstRow.left}`);
|
|
164
|
+
assert.strictEqual(extractDisplayedLineNumber(firstRow.right), "1", `Expected first right cell to show line number 1.\n${firstRow.right}`);
|
|
165
|
+
assert.strictEqual(extractDisplayedLineNumber(secondRow.left), "", `Expected wrapped left continuation row to blank its line number.\n${secondRow.left}`);
|
|
166
|
+
assert.strictEqual(extractCellContent(secondRow.left).length > 0, true, `Expected wrapped left continuation content to remain visible.\n${secondRow.left}`);
|
|
167
|
+
assert.strictEqual(extractDisplayedLineNumber(secondRow.right), "", `Expected blank placeholder row on the unwrapped right side.\n${secondRow.right}`);
|
|
168
|
+
assert.strictEqual(extractCellContent(secondRow.right), "", `Expected blank placeholder cell content on the unwrapped right side.\n${secondRow.right}`);
|
|
169
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type {
|
|
4
|
+
ExtensionAPI,
|
|
5
|
+
ExtensionCommandContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import toolDisplayExtension from "../src/index.ts";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
interface CapturedHandler {
|
|
14
|
+
event: string;
|
|
15
|
+
handler: (...args: unknown[]) => unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface CapturedCommand {
|
|
19
|
+
name: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
handler?: (...args: unknown[]) => unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createApiStub(
|
|
25
|
+
overrides: Partial<{
|
|
26
|
+
registerTool: (tool: unknown) => void;
|
|
27
|
+
registerCommand: (name: string, cmd: unknown) => void;
|
|
28
|
+
on: (event: string, handler: (...args: unknown[]) => unknown) => void;
|
|
29
|
+
getAllTools: () => unknown[];
|
|
30
|
+
getCommands: () => Array<{ name: string }>;
|
|
31
|
+
}> = {},
|
|
32
|
+
): {
|
|
33
|
+
api: ExtensionAPI;
|
|
34
|
+
capturedTools: Array<{ name: string } & Record<string, unknown>>;
|
|
35
|
+
capturedCommands: CapturedCommand[];
|
|
36
|
+
capturedHandlers: CapturedHandler[];
|
|
37
|
+
} {
|
|
38
|
+
const capturedTools: Array<{ name: string } & Record<string, unknown>> = [];
|
|
39
|
+
const capturedCommands: CapturedCommand[] = [];
|
|
40
|
+
const capturedHandlers: CapturedHandler[] = [];
|
|
41
|
+
|
|
42
|
+
const api = {
|
|
43
|
+
registerTool(tool: unknown): void {
|
|
44
|
+
capturedTools.push(tool as { name: string } & Record<string, unknown>);
|
|
45
|
+
overrides.registerTool?.(tool);
|
|
46
|
+
},
|
|
47
|
+
registerCommand(name: string, cmd: unknown): void {
|
|
48
|
+
capturedCommands.push({ name, ...(cmd as object) } as CapturedCommand);
|
|
49
|
+
overrides.registerCommand?.(name, cmd);
|
|
50
|
+
},
|
|
51
|
+
on(event: string, handler: (...args: unknown[]) => unknown): void {
|
|
52
|
+
capturedHandlers.push({ event, handler });
|
|
53
|
+
overrides.on?.(event, handler);
|
|
54
|
+
},
|
|
55
|
+
getAllTools(): unknown[] {
|
|
56
|
+
return overrides.getAllTools?.() ?? [];
|
|
57
|
+
},
|
|
58
|
+
getCommands(): Array<{ name: string }> {
|
|
59
|
+
return overrides.getCommands?.() ?? [];
|
|
60
|
+
},
|
|
61
|
+
} as unknown as ExtensionAPI;
|
|
62
|
+
|
|
63
|
+
return { api, capturedTools, capturedCommands, capturedHandlers };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Tests
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
test("entry point registers expected lifecycle handlers", () => {
|
|
71
|
+
const { api, capturedHandlers } = createApiStub();
|
|
72
|
+
toolDisplayExtension(api);
|
|
73
|
+
|
|
74
|
+
const eventNames = capturedHandlers.map((h) => h.event);
|
|
75
|
+
// Thinking-label handlers
|
|
76
|
+
assert.ok(eventNames.includes("message_update"), "message_update handler registered");
|
|
77
|
+
assert.ok(eventNames.includes("message_end"), "message_end handler registered");
|
|
78
|
+
assert.ok(eventNames.includes("context"), "context handler registered");
|
|
79
|
+
// Lifecycle handlers from index.ts directly
|
|
80
|
+
assert.ok(eventNames.includes("session_start"), "session_start handler registered");
|
|
81
|
+
assert.ok(eventNames.includes("before_agent_start"), "before_agent_start handler registered");
|
|
82
|
+
// User-message-box lifecycle handlers
|
|
83
|
+
const sessionStartCount = eventNames.filter((e) => e === "session_start").length;
|
|
84
|
+
assert.ok(sessionStartCount >= 1, "at least one session_start handler registered");
|
|
85
|
+
const beforeAgentStartCount = eventNames.filter((e) => e === "before_agent_start").length;
|
|
86
|
+
assert.ok(beforeAgentStartCount >= 1, "at least one before_agent_start handler registered");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("entry point registers tool-display command", () => {
|
|
90
|
+
const { api, capturedCommands } = createApiStub();
|
|
91
|
+
toolDisplayExtension(api);
|
|
92
|
+
|
|
93
|
+
const cmdNames = capturedCommands.map((c) => c.name);
|
|
94
|
+
assert.ok(cmdNames.includes("tool-display"), "tool-display command registered");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("entry point registers built-in tool overrides", () => {
|
|
98
|
+
const { api, capturedTools } = createApiStub();
|
|
99
|
+
toolDisplayExtension(api);
|
|
100
|
+
|
|
101
|
+
const toolNames = capturedTools.map((t) => t.name);
|
|
102
|
+
// find, ls, write are registered immediately; read/grep/edit/bash are deferred
|
|
103
|
+
assert.ok(toolNames.includes("find"), "find tool override registered");
|
|
104
|
+
assert.ok(toolNames.includes("ls"), "ls tool override registered");
|
|
105
|
+
assert.ok(toolNames.includes("write"), "write tool override registered");
|
|
106
|
+
|
|
107
|
+
// Disabled tools (if config disables them) would not appear; the default
|
|
108
|
+
// config enables all, so we expect at least these 3 immediately.
|
|
109
|
+
assert.ok(toolNames.length >= 3, "at least 3 tool overrides registered immediately");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("session_start handler refreshes capabilities and notifies pending errors", async () => {
|
|
113
|
+
const { api, capturedHandlers } = createApiStub();
|
|
114
|
+
toolDisplayExtension(api);
|
|
115
|
+
|
|
116
|
+
const sessionHandler = capturedHandlers.find((h) => h.event === "session_start")?.handler;
|
|
117
|
+
assert.ok(sessionHandler, "session_start handler captured");
|
|
118
|
+
|
|
119
|
+
const ctx = {
|
|
120
|
+
ui: {
|
|
121
|
+
theme: { fg: (_c: string, t: string) => t },
|
|
122
|
+
notify: (_msg: string, _level: string) => { /* no-op */ },
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Should not throw
|
|
127
|
+
await assert.doesNotReject(async () => sessionHandler({}, ctx));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("before_agent_start handler refreshes capabilities without crashing", async () => {
|
|
131
|
+
const { api, capturedHandlers } = createApiStub();
|
|
132
|
+
toolDisplayExtension(api);
|
|
133
|
+
|
|
134
|
+
const beforeHandler = capturedHandlers.find((h) => h.event === "before_agent_start")?.handler;
|
|
135
|
+
assert.ok(beforeHandler, "before_agent_start handler captured");
|
|
136
|
+
|
|
137
|
+
// Should not throw
|
|
138
|
+
await assert.doesNotReject(async () => beforeHandler());
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("multiple calls to toolDisplayExtension are idempotent", () => {
|
|
142
|
+
const { api, capturedTools, capturedCommands, capturedHandlers } = createApiStub();
|
|
143
|
+
|
|
144
|
+
// Call twice
|
|
145
|
+
toolDisplayExtension(api);
|
|
146
|
+
toolDisplayExtension(api);
|
|
147
|
+
|
|
148
|
+
// Second call should not throw. Tools may be registered again (that's up
|
|
149
|
+
// to the extension loader to deduplicate), but the extension itself must
|
|
150
|
+
// not crash.
|
|
151
|
+
const toolNames = capturedTools.map((t) => t.name);
|
|
152
|
+
assert.ok(toolNames.filter((n) => n === "find").length >= 1, "find registered at least once");
|
|
153
|
+
assert.ok(toolNames.filter((n) => n === "ls").length >= 1, "ls registered at least once");
|
|
154
|
+
assert.ok(toolNames.filter((n) => n === "write").length >= 1, "write registered at least once");
|
|
155
|
+
|
|
156
|
+
const cmdNames = capturedCommands.map((c) => c.name);
|
|
157
|
+
assert.ok(cmdNames.filter((n) => n === "tool-display").length >= 1, "command registered at least once");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("entry point tolerates empty getAllTools and getCommands results", () => {
|
|
161
|
+
// Stub that returns empty arrays for discovery methods
|
|
162
|
+
const { api } = createApiStub({
|
|
163
|
+
getAllTools: () => [],
|
|
164
|
+
getCommands: () => [],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
assert.doesNotThrow(() => toolDisplayExtension(api));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("entry point tolerates tools with existing owners in getAllTools", () => {
|
|
171
|
+
const { api } = createApiStub({
|
|
172
|
+
getAllTools: () => [
|
|
173
|
+
{ name: "read", sourceInfo: { source: "local", path: "/ext/read.ts" } },
|
|
174
|
+
{ name: "edit", sourceInfo: { source: "local", path: "/ext/edit.ts" } },
|
|
175
|
+
{ name: "grep", sourceInfo: { source: "local", path: "/ext/grep.ts" } },
|
|
176
|
+
],
|
|
177
|
+
getCommands: () => [{ name: "custom" }],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
assert.doesNotThrow(() => toolDisplayExtension(api));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("graceful degradation: extension throws when registerCommand is missing", () => {
|
|
184
|
+
// Simulate a minimal stub missing registerCommand
|
|
185
|
+
const minimalApi = {
|
|
186
|
+
registerTool(): void { /* no-op */ },
|
|
187
|
+
on(): void { /* no-op */ },
|
|
188
|
+
getAllTools(): unknown[] { return []; },
|
|
189
|
+
getCommands(): Array<{ name: string }> { return []; },
|
|
190
|
+
} as unknown as ExtensionAPI;
|
|
191
|
+
|
|
192
|
+
// registerToolDisplayCommand calls pi.registerCommand directly, so this
|
|
193
|
+
// is expected to throw in a peer-dep mismatch scenario.
|
|
194
|
+
assert.throws(
|
|
195
|
+
() => toolDisplayExtension(minimalApi),
|
|
196
|
+
/registerCommand/i,
|
|
197
|
+
"missing registerCommand should propagate",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("graceful degradation: extension throws when on is missing", () => {
|
|
202
|
+
const minimalApi = {
|
|
203
|
+
registerTool(): void { /* no-op */ },
|
|
204
|
+
registerCommand(): void { /* no-op */ },
|
|
205
|
+
getAllTools(): unknown[] { return []; },
|
|
206
|
+
getCommands(): Array<{ name: string }> { return []; },
|
|
207
|
+
} as unknown as ExtensionAPI;
|
|
208
|
+
|
|
209
|
+
// registerNativeUserMessageBox calls pi.on, so this should throw when on is missing
|
|
210
|
+
assert.throws(
|
|
211
|
+
() => toolDisplayExtension(minimalApi),
|
|
212
|
+
/pi\.on is not a function|on is not a function/i,
|
|
213
|
+
"missing on should propagate",
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("lifecycle events fire in expected order during a session lifecycle", async () => {
|
|
218
|
+
// Simulate the sequence: setup → before_agent_start → session_start
|
|
219
|
+
const { api, capturedHandlers } = createApiStub();
|
|
220
|
+
|
|
221
|
+
toolDisplayExtension(api);
|
|
222
|
+
|
|
223
|
+
// Manually invoke handlers in expected lifecycle order
|
|
224
|
+
const beforeHandler = capturedHandlers.find((h) => h.event === "before_agent_start")?.handler;
|
|
225
|
+
const sessionHandler = capturedHandlers.find((h) => h.event === "session_start")?.handler;
|
|
226
|
+
const messageUpdateHandler = capturedHandlers.find((h) => h.event === "message_update")?.handler;
|
|
227
|
+
const messageEndHandler = capturedHandlers.find((h) => h.event === "message_end")?.handler;
|
|
228
|
+
const contextHandler = capturedHandlers.find((h) => h.event === "context")?.handler;
|
|
229
|
+
|
|
230
|
+
assert.ok(beforeHandler, "before_agent_start handler found");
|
|
231
|
+
assert.ok(sessionHandler, "session_start handler found");
|
|
232
|
+
|
|
233
|
+
// Simulate a session lifecycle
|
|
234
|
+
await beforeHandler();
|
|
235
|
+
await sessionHandler(
|
|
236
|
+
{},
|
|
237
|
+
{ ui: { theme: { fg: (_c: string, t: string) => t }, notify: () => {} } },
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
// Simulate message lifecycle for thinking labels
|
|
241
|
+
if (messageUpdateHandler) {
|
|
242
|
+
await messageUpdateHandler(
|
|
243
|
+
{
|
|
244
|
+
message: {
|
|
245
|
+
role: "assistant",
|
|
246
|
+
api: "anthropic-messages",
|
|
247
|
+
content: [{ type: "thinking", thinking: "test" }],
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{ ui: { theme: { fg: (_c: string, t: string) => `[${_c}]${t}` } } },
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (messageEndHandler) {
|
|
255
|
+
await messageEndHandler(
|
|
256
|
+
{
|
|
257
|
+
message: {
|
|
258
|
+
role: "assistant",
|
|
259
|
+
api: "openai-chat",
|
|
260
|
+
content: [{ type: "thinking", thinking: "done" }],
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (contextHandler) {
|
|
268
|
+
await contextHandler(
|
|
269
|
+
{
|
|
270
|
+
messages: [
|
|
271
|
+
{
|
|
272
|
+
role: "assistant",
|
|
273
|
+
content: [{ type: "thinking", thinking: "\x1b[31mThinking: \x1b[0mcontext" }],
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
{},
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// All handlers executed without throwing - this is the main assertion
|
|
282
|
+
assert.ok(true, "lifecycle handlers completed without error");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("session_start handler tolerates missing ctx.ui", async () => {
|
|
286
|
+
const { api, capturedHandlers } = createApiStub();
|
|
287
|
+
toolDisplayExtension(api);
|
|
288
|
+
|
|
289
|
+
const sessionHandler = capturedHandlers.find((h) => h.event === "session_start")?.handler;
|
|
290
|
+
assert.ok(sessionHandler);
|
|
291
|
+
|
|
292
|
+
// ctx with no ui (edge case from older pi versions)
|
|
293
|
+
await assert.doesNotReject(async () => sessionHandler({}, {}));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("before_agent_start handler tolerates being called multiple times", async () => {
|
|
297
|
+
const { api, capturedHandlers } = createApiStub();
|
|
298
|
+
toolDisplayExtension(api);
|
|
299
|
+
|
|
300
|
+
const beforeHandler = capturedHandlers.find((h) => h.event === "before_agent_start")?.handler;
|
|
301
|
+
assert.ok(beforeHandler);
|
|
302
|
+
|
|
303
|
+
await assert.doesNotReject(async () => beforeHandler());
|
|
304
|
+
await assert.doesNotReject(async () => beforeHandler());
|
|
305
|
+
await assert.doesNotReject(async () => beforeHandler());
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("session_start handler tolerates being called multiple times", async () => {
|
|
309
|
+
const { api, capturedHandlers } = createApiStub();
|
|
310
|
+
toolDisplayExtension(api);
|
|
311
|
+
|
|
312
|
+
const sessionHandler = capturedHandlers.find((h) => h.event === "session_start")?.handler;
|
|
313
|
+
assert.ok(sessionHandler);
|
|
314
|
+
|
|
315
|
+
const ctx = { ui: { theme: {}, notify: () => {} } };
|
|
316
|
+
await assert.doesNotReject(async () => sessionHandler({}, ctx));
|
|
317
|
+
await assert.doesNotReject(async () => sessionHandler({}, ctx));
|
|
318
|
+
await assert.doesNotReject(async () => sessionHandler({}, ctx));
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("overridden tools include renderCall and renderResult functions", () => {
|
|
322
|
+
const { api, capturedTools } = createApiStub();
|
|
323
|
+
toolDisplayExtension(api);
|
|
324
|
+
|
|
325
|
+
for (const tool of capturedTools) {
|
|
326
|
+
assert.ok(
|
|
327
|
+
typeof tool.renderCall === "function",
|
|
328
|
+
`${tool.name} has renderCall`,
|
|
329
|
+
);
|
|
330
|
+
assert.ok(
|
|
331
|
+
typeof tool.renderResult === "function",
|
|
332
|
+
`${tool.name} has renderResult`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("overridden tools preserve promptSnippet and promptGuidelines from built-ins", () => {
|
|
338
|
+
const { api, capturedTools } = createApiStub();
|
|
339
|
+
toolDisplayExtension(api);
|
|
340
|
+
|
|
341
|
+
const byName = new Map(capturedTools.map((t) => [t.name, t]));
|
|
342
|
+
|
|
343
|
+
// read (deferred) won't be registered immediately; it's deferred
|
|
344
|
+
// So we only check tools registered immediately
|
|
345
|
+
for (const name of ["find", "ls", "write"] as const) {
|
|
346
|
+
const tool = byName.get(name);
|
|
347
|
+
assert.ok(tool, `${name} is registered`);
|
|
348
|
+
// promptSnippet should be a non-empty string or undefined
|
|
349
|
+
// (built-in tools may or may not have promptSnippet)
|
|
350
|
+
if (tool.promptSnippet !== undefined) {
|
|
351
|
+
assert.equal(typeof tool.promptSnippet, "string");
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import {
|
|
7
|
+
buildPendingEditPreviewData,
|
|
8
|
+
buildPendingWritePreviewData,
|
|
9
|
+
} from "../src/pending-diff-preview.ts";
|
|
10
|
+
|
|
11
|
+
function withTempWorkspace(name: string, run: (workspace: string) => void): void {
|
|
12
|
+
const workspace = mkdtempSync(join(tmpdir(), name));
|
|
13
|
+
try {
|
|
14
|
+
run(workspace);
|
|
15
|
+
} finally {
|
|
16
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("pending write preview compares existing safe workspace files", () => {
|
|
21
|
+
withTempWorkspace("pi-tool-display-write-preview-", (workspace) => {
|
|
22
|
+
writeFileSync(join(workspace, "sample.txt"), "before\n", "utf8");
|
|
23
|
+
|
|
24
|
+
const preview = buildPendingWritePreviewData(
|
|
25
|
+
{ path: "sample.txt", content: "after\n" },
|
|
26
|
+
workspace,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
assert.equal(preview?.fileExistedBeforeWrite, true);
|
|
30
|
+
assert.equal(preview?.previousContent, "before\n");
|
|
31
|
+
assert.equal(preview?.nextContent, "after\n");
|
|
32
|
+
assert.equal(preview?.notice, undefined);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("pending preview skips direct reads outside the workspace", () => {
|
|
37
|
+
withTempWorkspace("pi-tool-display-boundary-", (workspace) => {
|
|
38
|
+
const outside = mkdtempSync(join(tmpdir(), "pi-tool-display-outside-"));
|
|
39
|
+
try {
|
|
40
|
+
const outsideFile = join(outside, "secret.txt");
|
|
41
|
+
writeFileSync(outsideFile, "secret\n", "utf8");
|
|
42
|
+
|
|
43
|
+
const editPreview = buildPendingEditPreviewData(
|
|
44
|
+
{ path: outsideFile, oldText: "secret", newText: "safe" },
|
|
45
|
+
workspace,
|
|
46
|
+
);
|
|
47
|
+
const writePreview = buildPendingWritePreviewData(
|
|
48
|
+
{ path: outsideFile, content: "replacement\n" },
|
|
49
|
+
workspace,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert.equal(editPreview?.previousContent, undefined);
|
|
53
|
+
assert.equal(editPreview?.nextContent, undefined);
|
|
54
|
+
assert.match(editPreview?.notice ?? "", /outside the current workspace/);
|
|
55
|
+
assert.equal(writePreview?.previousContent, undefined);
|
|
56
|
+
assert.equal(writePreview?.nextContent, "replacement\n");
|
|
57
|
+
assert.match(writePreview?.notice ?? "", /outside the current workspace/);
|
|
58
|
+
} finally {
|
|
59
|
+
rmSync(outside, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("pending preview skips files over the read-size limit", () => {
|
|
65
|
+
withTempWorkspace("pi-tool-display-large-preview-", (workspace) => {
|
|
66
|
+
mkdirSync(join(workspace, "nested"));
|
|
67
|
+
const nestedFile = join(workspace, "nested", "large.txt");
|
|
68
|
+
writeFileSync(nestedFile, "x".repeat(1_000_001), "utf8");
|
|
69
|
+
|
|
70
|
+
const preview = buildPendingEditPreviewData(
|
|
71
|
+
{ path: "nested/large.txt", oldText: "x", newText: "y" },
|
|
72
|
+
workspace,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
assert.equal(preview?.previousContent, undefined);
|
|
76
|
+
assert.equal(preview?.nextContent, undefined);
|
|
77
|
+
assert.match(preview?.notice ?? "", /preview read limit/);
|
|
78
|
+
});
|
|
79
|
+
});
|