@bdsqqq/pi 0.0.1
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/dist/chunk-DsIazq42.js +26 -0
- package/dist/command-palette-B97yKqne.js +406 -0
- package/dist/core/agents-md.d.ts +22 -0
- package/dist/core/agents-md.js +174 -0
- package/dist/core/box-chrome.d.ts +56 -0
- package/dist/core/box-chrome.js +207 -0
- package/dist/core/box-format.d.ts +82 -0
- package/dist/core/box-format.js +381 -0
- package/dist/core/config.d.ts +14 -0
- package/dist/core/config.js +201 -0
- package/dist/core/file-tracker.d.ts +65 -0
- package/dist/core/file-tracker.js +439 -0
- package/dist/core/github-api.d.ts +45 -0
- package/dist/core/github-api.js +101 -0
- package/dist/core/html-to-md.d.ts +5 -0
- package/dist/core/html-to-md.js +289 -0
- package/dist/core/interpolate.d.ts +44 -0
- package/dist/core/interpolate.js +475 -0
- package/dist/core/mutex.d.ts +8 -0
- package/dist/core/mutex.js +142 -0
- package/dist/core/output-buffer.d.ts +80 -0
- package/dist/core/output-buffer.js +517 -0
- package/dist/core/permissions.d.ts +19 -0
- package/dist/core/permissions.js +140 -0
- package/dist/core/pi-spawn.d.ts +62 -0
- package/dist/core/pi-spawn.js +244 -0
- package/dist/core/prompt-patch.d.ts +11 -0
- package/dist/core/prompt-patch.js +16 -0
- package/dist/core/show-renderer.d.ts +10 -0
- package/dist/core/show-renderer.js +39 -0
- package/dist/core/show.d.ts +29 -0
- package/dist/core/show.js +288 -0
- package/dist/core/sub-agent-render.d.ts +56 -0
- package/dist/core/sub-agent-render.js +579 -0
- package/dist/core/tool-cost.d.ts +17 -0
- package/dist/core/tool-cost.js +58 -0
- package/dist/core/tui.d.ts +9 -0
- package/dist/core/tui.js +33 -0
- package/dist/editor-BljciPdx.js +537 -0
- package/dist/extensions/bash.d.ts +12 -0
- package/dist/extensions/bash.js +261 -0
- package/dist/extensions/code-review.d.ts +14 -0
- package/dist/extensions/code-review.js +175 -0
- package/dist/extensions/command-palette.d.ts +6 -0
- package/dist/extensions/command-palette.js +2 -0
- package/dist/extensions/create-file.d.ts +7 -0
- package/dist/extensions/create-file.js +88 -0
- package/dist/extensions/edit-file.d.ts +7 -0
- package/dist/extensions/edit-file.js +395 -0
- package/dist/extensions/editor.d.ts +6 -0
- package/dist/extensions/editor.js +2 -0
- package/dist/extensions/finder.d.ts +13 -0
- package/dist/extensions/finder.js +108 -0
- package/dist/extensions/format-file.d.ts +11 -0
- package/dist/extensions/format-file.js +145 -0
- package/dist/extensions/github.d.ts +13 -0
- package/dist/extensions/github.js +527 -0
- package/dist/extensions/glob.d.ts +10 -0
- package/dist/extensions/glob.js +163 -0
- package/dist/extensions/grep.d.ts +13 -0
- package/dist/extensions/grep.js +356 -0
- package/dist/extensions/handoff.d.ts +6 -0
- package/dist/extensions/handoff.js +277 -0
- package/dist/extensions/librarian.d.ts +13 -0
- package/dist/extensions/librarian.js +113 -0
- package/dist/extensions/look-at.d.ts +13 -0
- package/dist/extensions/look-at.js +121 -0
- package/dist/extensions/ls.d.ts +8 -0
- package/dist/extensions/ls.js +87 -0
- package/dist/extensions/mermaid.d.ts +20 -0
- package/dist/extensions/mermaid.js +2 -0
- package/dist/extensions/oracle.d.ts +13 -0
- package/dist/extensions/oracle.js +143 -0
- package/dist/extensions/read-session.d.ts +13 -0
- package/dist/extensions/read-session.js +265 -0
- package/dist/extensions/read-web-page.d.ts +11 -0
- package/dist/extensions/read-web-page.js +234 -0
- package/dist/extensions/read.d.ts +23 -0
- package/dist/extensions/read.js +323 -0
- package/dist/extensions/search-sessions.d.ts +29 -0
- package/dist/extensions/search-sessions.js +426 -0
- package/dist/extensions/session-name.d.ts +6 -0
- package/dist/extensions/session-name.js +54 -0
- package/dist/extensions/skill.d.ts +7 -0
- package/dist/extensions/skill.js +232 -0
- package/dist/extensions/system-prompt.d.ts +6 -0
- package/dist/extensions/system-prompt.js +31 -0
- package/dist/extensions/task.d.ts +11 -0
- package/dist/extensions/task.js +114 -0
- package/dist/extensions/tool-harness.d.ts +6 -0
- package/dist/extensions/tool-harness.js +37 -0
- package/dist/extensions/undo-edit.d.ts +7 -0
- package/dist/extensions/undo-edit.js +127 -0
- package/dist/extensions/web-search.d.ts +12 -0
- package/dist/extensions/web-search.js +254 -0
- package/dist/extensions.d.ts +29 -0
- package/dist/extensions.js +29 -0
- package/dist/mermaid-C7xCVBrw.js +95296 -0
- package/package.json +67 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Message } from "@mariozechner/pi-ai";
|
|
2
|
+
|
|
3
|
+
//#region packages/core/pi-spawn/index.d.ts
|
|
4
|
+
interface UsageStats {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
cacheRead: number;
|
|
8
|
+
cacheWrite: number;
|
|
9
|
+
cost: number;
|
|
10
|
+
contextTokens: number;
|
|
11
|
+
turns: number;
|
|
12
|
+
}
|
|
13
|
+
interface PiSpawnResult {
|
|
14
|
+
exitCode: number;
|
|
15
|
+
messages: Message[];
|
|
16
|
+
stderr: string;
|
|
17
|
+
usage: UsageStats;
|
|
18
|
+
model?: string;
|
|
19
|
+
stopReason?: string;
|
|
20
|
+
errorMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
interface PiSpawnConfig {
|
|
23
|
+
cwd: string;
|
|
24
|
+
task: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
builtinTools?: string[];
|
|
27
|
+
extensionTools?: string[];
|
|
28
|
+
systemPromptBody?: string;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
onUpdate?: (result: PiSpawnResult) => void;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
repo?: string;
|
|
33
|
+
/**
|
|
34
|
+
* inject a follow-up user message after the agent's first turn.
|
|
35
|
+
*
|
|
36
|
+
* uses pi's RPC mode instead of print mode. the follow-up is queued
|
|
37
|
+
* eagerly at startup (not delivered until idle), so the agent loop's
|
|
38
|
+
* getFollowUpMessages() finds it after exploration completes. the
|
|
39
|
+
* process is killed after the second end_turn.
|
|
40
|
+
*
|
|
41
|
+
* primary use case: code_review — agent explores the diff first,
|
|
42
|
+
* then receives the report format instructions.
|
|
43
|
+
*/
|
|
44
|
+
followUp?: string;
|
|
45
|
+
}
|
|
46
|
+
declare function zeroUsage(): UsageStats;
|
|
47
|
+
/**
|
|
48
|
+
* resolve a prompt from either an inline string or a file.
|
|
49
|
+
*
|
|
50
|
+
* precedence: promptString (if non-empty) → readAgentPrompt(promptFile).
|
|
51
|
+
* lets extensions externalize prompt content via config while
|
|
52
|
+
* keeping sops-managed .md files as the default source.
|
|
53
|
+
*/
|
|
54
|
+
declare function resolvePrompt(promptString: string, promptFile: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* read an agent prompt .md file, strip frontmatter, return body.
|
|
57
|
+
* looks in ~/.pi/agent/agents/{filename}.
|
|
58
|
+
*/
|
|
59
|
+
declare function readAgentPrompt(filename: string): string;
|
|
60
|
+
declare function piSpawn(config: PiSpawnConfig): Promise<PiSpawnResult>;
|
|
61
|
+
//#endregion
|
|
62
|
+
export { PiSpawnConfig, PiSpawnResult, UsageStats, piSpawn, readAgentPrompt, resolvePrompt, zeroUsage };
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { interpolatePromptVars } from "./interpolate.js";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
//#region packages/core/pi-spawn/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* shared pi process spawning for dedicated sub-agent tools.
|
|
9
|
+
*
|
|
10
|
+
* extracts the spawn-parse-collect loop from the generic subagent
|
|
11
|
+
* extension into a reusable function. each dedicated tool (finder,
|
|
12
|
+
* oracle, Task) calls piSpawn() with its own config.
|
|
13
|
+
*
|
|
14
|
+
* uses shared interpolation from @bds_pi/interpolate for template variables
|
|
15
|
+
* ({cwd}, {roots}, {date}, etc.) in system prompts.
|
|
16
|
+
*/
|
|
17
|
+
function writePromptToTempFile(label, prompt) {
|
|
18
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
|
|
19
|
+
const safeName = label.replace(/[^\w.-]+/g, "_");
|
|
20
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
21
|
+
fs.writeFileSync(filePath, prompt, {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
mode: 384
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
dir: tmpDir,
|
|
27
|
+
filePath
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function zeroUsage() {
|
|
31
|
+
return {
|
|
32
|
+
input: 0,
|
|
33
|
+
output: 0,
|
|
34
|
+
cacheRead: 0,
|
|
35
|
+
cacheWrite: 0,
|
|
36
|
+
cost: 0,
|
|
37
|
+
contextTokens: 0,
|
|
38
|
+
turns: 0
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* resolve a prompt from either an inline string or a file.
|
|
43
|
+
*
|
|
44
|
+
* precedence: promptString (if non-empty) → readAgentPrompt(promptFile).
|
|
45
|
+
* lets extensions externalize prompt content via config while
|
|
46
|
+
* keeping sops-managed .md files as the default source.
|
|
47
|
+
*/
|
|
48
|
+
function resolvePrompt(promptString, promptFile) {
|
|
49
|
+
if (promptString) return promptString;
|
|
50
|
+
return readAgentPrompt(promptFile);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* read an agent prompt .md file, strip frontmatter, return body.
|
|
54
|
+
* looks in ~/.pi/agent/agents/{filename}.
|
|
55
|
+
*/
|
|
56
|
+
function readAgentPrompt(filename) {
|
|
57
|
+
const promptPath = path.join(os.homedir(), ".pi", "agent", "agents", filename);
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(promptPath, "utf-8");
|
|
60
|
+
if (content.startsWith("---")) {
|
|
61
|
+
const endIdx = content.indexOf("\n---", 3);
|
|
62
|
+
if (endIdx !== -1) return content.slice(endIdx + 4).trim();
|
|
63
|
+
}
|
|
64
|
+
return content;
|
|
65
|
+
} catch {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function piSpawn(config) {
|
|
70
|
+
const useRpc = !!config.followUp;
|
|
71
|
+
const args = useRpc ? [
|
|
72
|
+
"--mode",
|
|
73
|
+
"rpc",
|
|
74
|
+
"--no-session"
|
|
75
|
+
] : [
|
|
76
|
+
"--mode",
|
|
77
|
+
"json",
|
|
78
|
+
"-p",
|
|
79
|
+
"--no-session"
|
|
80
|
+
];
|
|
81
|
+
if (config.model) args.push("--model", config.model);
|
|
82
|
+
if (config.builtinTools !== void 0) if (config.builtinTools.length === 0) args.push("--no-tools");
|
|
83
|
+
else args.push("--tools", config.builtinTools.join(","));
|
|
84
|
+
let tmpPromptDir = null;
|
|
85
|
+
let tmpPromptPath = null;
|
|
86
|
+
const result = {
|
|
87
|
+
exitCode: 0,
|
|
88
|
+
messages: [],
|
|
89
|
+
stderr: "",
|
|
90
|
+
usage: zeroUsage()
|
|
91
|
+
};
|
|
92
|
+
try {
|
|
93
|
+
if (config.systemPromptBody?.trim()) {
|
|
94
|
+
const tmp = writePromptToTempFile("subagent", interpolatePromptVars(config.systemPromptBody, config.cwd, {
|
|
95
|
+
sessionId: config.sessionId,
|
|
96
|
+
repo: config.repo
|
|
97
|
+
}));
|
|
98
|
+
tmpPromptDir = tmp.dir;
|
|
99
|
+
tmpPromptPath = tmp.filePath;
|
|
100
|
+
args.push("--append-system-prompt", tmpPromptPath);
|
|
101
|
+
}
|
|
102
|
+
if (!useRpc) args.push(`Task: ${config.task}`);
|
|
103
|
+
const spawnEnv = { ...process.env };
|
|
104
|
+
if (config.extensionTools !== void 0) if (config.extensionTools.length === 0) spawnEnv.PI_INCLUDE_TOOLS = "NONE";
|
|
105
|
+
else spawnEnv.PI_INCLUDE_TOOLS = config.extensionTools.join(",");
|
|
106
|
+
let wasAborted = false;
|
|
107
|
+
const debugEnabled = !!process.env.PI_SPAWN_DEBUG;
|
|
108
|
+
const debug = (label, data) => {
|
|
109
|
+
if (!debugEnabled) return;
|
|
110
|
+
const suffix = data ? ` ${JSON.stringify(data)}` : "";
|
|
111
|
+
process.stderr.write(`[pi-spawn] ${label}${suffix}\n`);
|
|
112
|
+
};
|
|
113
|
+
result.exitCode = await new Promise((resolve) => {
|
|
114
|
+
const proc = spawn("pi", args, {
|
|
115
|
+
cwd: config.cwd,
|
|
116
|
+
shell: false,
|
|
117
|
+
stdio: [
|
|
118
|
+
useRpc ? "pipe" : "ignore",
|
|
119
|
+
"pipe",
|
|
120
|
+
"pipe"
|
|
121
|
+
],
|
|
122
|
+
env: spawnEnv
|
|
123
|
+
});
|
|
124
|
+
let endTurnCount = 0;
|
|
125
|
+
if (useRpc && proc.stdin) {
|
|
126
|
+
const promptCmd = JSON.stringify({
|
|
127
|
+
type: "prompt",
|
|
128
|
+
message: `Task: ${config.task}`
|
|
129
|
+
});
|
|
130
|
+
debug("send_prompt");
|
|
131
|
+
proc.stdin.write(promptCmd + "\n");
|
|
132
|
+
if (config.followUp) {
|
|
133
|
+
const followUpCmd = JSON.stringify({
|
|
134
|
+
type: "follow_up",
|
|
135
|
+
message: config.followUp
|
|
136
|
+
});
|
|
137
|
+
debug("send_follow_up");
|
|
138
|
+
proc.stdin.write(followUpCmd + "\n");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let buffer = "";
|
|
142
|
+
const processLine = (line) => {
|
|
143
|
+
if (!line.trim()) return;
|
|
144
|
+
let event;
|
|
145
|
+
try {
|
|
146
|
+
event = JSON.parse(line);
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (event.type === "response") return;
|
|
151
|
+
if (event.type === "message_end" && event.message) {
|
|
152
|
+
const msg = event.message;
|
|
153
|
+
result.messages.push(msg);
|
|
154
|
+
if (msg.role === "assistant") {
|
|
155
|
+
result.usage.turns++;
|
|
156
|
+
const usage = msg.usage;
|
|
157
|
+
if (usage) {
|
|
158
|
+
result.usage.input += usage.input || 0;
|
|
159
|
+
result.usage.output += usage.output || 0;
|
|
160
|
+
result.usage.cacheRead += usage.cacheRead || 0;
|
|
161
|
+
result.usage.cacheWrite += usage.cacheWrite || 0;
|
|
162
|
+
result.usage.cost += usage.cost?.total || 0;
|
|
163
|
+
result.usage.contextTokens = usage.totalTokens || 0;
|
|
164
|
+
}
|
|
165
|
+
if (!result.model && msg.model) result.model = msg.model;
|
|
166
|
+
if (msg.stopReason) result.stopReason = msg.stopReason;
|
|
167
|
+
if (msg.errorMessage) result.errorMessage = msg.errorMessage;
|
|
168
|
+
const stopReason = msg.stopReason;
|
|
169
|
+
const isTurnEnd = stopReason === "end_turn" || stopReason === "stop";
|
|
170
|
+
const expectedTurns = config.followUp ? 2 : 1;
|
|
171
|
+
debug("turn_end", {
|
|
172
|
+
stopReason,
|
|
173
|
+
isTurnEnd,
|
|
174
|
+
endTurnCount,
|
|
175
|
+
expectedTurns
|
|
176
|
+
});
|
|
177
|
+
if (useRpc && isTurnEnd) {
|
|
178
|
+
endTurnCount++;
|
|
179
|
+
if (endTurnCount >= expectedTurns) {
|
|
180
|
+
debug("kill_after_turn", { endTurnCount });
|
|
181
|
+
proc.kill("SIGTERM");
|
|
182
|
+
setTimeout(() => {
|
|
183
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
184
|
+
}, 5e3);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (useRpc && (stopReason === "error" || stopReason === "aborted")) {
|
|
188
|
+
debug("kill_after_error", { stopReason });
|
|
189
|
+
proc.kill("SIGTERM");
|
|
190
|
+
setTimeout(() => {
|
|
191
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
192
|
+
}, 5e3);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (config.onUpdate) config.onUpdate({ ...result });
|
|
196
|
+
}
|
|
197
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
198
|
+
result.messages.push(event.message);
|
|
199
|
+
if (config.onUpdate) config.onUpdate({ ...result });
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
proc.stdout.on("data", (data) => {
|
|
203
|
+
buffer += data.toString();
|
|
204
|
+
const lines = buffer.split("\n");
|
|
205
|
+
buffer = lines.pop() || "";
|
|
206
|
+
for (const line of lines) processLine(line);
|
|
207
|
+
});
|
|
208
|
+
proc.stderr.on("data", (data) => {
|
|
209
|
+
result.stderr += data.toString();
|
|
210
|
+
});
|
|
211
|
+
proc.on("close", (code) => {
|
|
212
|
+
if (buffer.trim()) processLine(buffer);
|
|
213
|
+
resolve(code ?? 0);
|
|
214
|
+
});
|
|
215
|
+
proc.on("error", () => resolve(1));
|
|
216
|
+
if (config.signal) {
|
|
217
|
+
const killProc = () => {
|
|
218
|
+
wasAborted = true;
|
|
219
|
+
proc.kill("SIGTERM");
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
222
|
+
}, 5e3);
|
|
223
|
+
};
|
|
224
|
+
if (config.signal.aborted) killProc();
|
|
225
|
+
else config.signal.addEventListener("abort", killProc, { once: true });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
if (wasAborted) {
|
|
229
|
+
result.exitCode = 1;
|
|
230
|
+
result.stopReason = "aborted";
|
|
231
|
+
}
|
|
232
|
+
if (useRpc && result.exitCode !== 0 && (result.stopReason === "end_turn" || result.stopReason === "stop")) result.exitCode = 0;
|
|
233
|
+
return result;
|
|
234
|
+
} finally {
|
|
235
|
+
if (tmpPromptPath) try {
|
|
236
|
+
fs.unlinkSync(tmpPromptPath);
|
|
237
|
+
} catch {}
|
|
238
|
+
if (tmpPromptDir) try {
|
|
239
|
+
fs.rmdirSync(tmpPromptDir);
|
|
240
|
+
} catch {}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
//#endregion
|
|
244
|
+
export { piSpawn, readAgentPrompt, resolvePrompt, zeroUsage };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
//#region packages/core/prompt-patch/index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* derives promptSnippet and promptGuidelines from a tool's description
|
|
6
|
+
* so tools don't need to define them manually. snippet = first paragraph,
|
|
7
|
+
* guidelines = lines starting with "- ".
|
|
8
|
+
*/
|
|
9
|
+
declare function withPromptPatch(tool: ToolDefinition): ToolDefinition;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { withPromptPatch };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region packages/core/prompt-patch/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* derives promptSnippet and promptGuidelines from a tool's description
|
|
4
|
+
* so tools don't need to define them manually. snippet = first paragraph,
|
|
5
|
+
* guidelines = lines starting with "- ".
|
|
6
|
+
*/
|
|
7
|
+
function withPromptPatch(tool) {
|
|
8
|
+
const snippet = (tool.description?.split("\n\n")[0] ?? "").trim();
|
|
9
|
+
const guidelines = (tool.description ?? "").split("\n").map((line) => line.trim()).filter((line) => line.startsWith("- "));
|
|
10
|
+
const patched = { ...tool };
|
|
11
|
+
if (!patched.promptSnippet) patched.promptSnippet = snippet;
|
|
12
|
+
if (!patched.promptGuidelines && guidelines.length > 0) patched.promptGuidelines = guidelines;
|
|
13
|
+
return patched;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { withPromptPatch };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Excerpt } from "./show.js";
|
|
2
|
+
|
|
3
|
+
//#region packages/core/show-renderer/index.d.ts
|
|
4
|
+
interface ShowRenderer {
|
|
5
|
+
render: (width: number) => string[];
|
|
6
|
+
invalidate: () => void;
|
|
7
|
+
}
|
|
8
|
+
declare function makeShowRenderer(text: string, excerpts: Excerpt[]): ShowRenderer;
|
|
9
|
+
//#endregion
|
|
10
|
+
export { ShowRenderer, makeShowRenderer };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { show } from "./show.js";
|
|
2
|
+
//#region packages/core/show-renderer/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* makeShowRenderer — wraps show() in the width-aware caching pattern required
|
|
5
|
+
* by the pi TUI renderer contract.
|
|
6
|
+
*
|
|
7
|
+
* pi calls render(width) on every frame and invalidate() when the component
|
|
8
|
+
* must be re-evaluated. caching by width avoids recomputing visual-line
|
|
9
|
+
* breaks on every frame (show() is O(lines)).
|
|
10
|
+
*
|
|
11
|
+
* usage:
|
|
12
|
+
* renderResult(result, { expanded }, theme) {
|
|
13
|
+
* const text = result.content?.[0]?.text ?? "";
|
|
14
|
+
* if (expanded) return new Text(text, 0, 0);
|
|
15
|
+
* return makeShowRenderer(text, [
|
|
16
|
+
* { focus: "head", context: 3 },
|
|
17
|
+
* { focus: "tail", context: 5 },
|
|
18
|
+
* ]);
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
function makeShowRenderer(text, excerpts) {
|
|
22
|
+
let cachedWidth;
|
|
23
|
+
let cachedLines;
|
|
24
|
+
return {
|
|
25
|
+
render(width) {
|
|
26
|
+
if (cachedLines === void 0 || cachedWidth !== width) {
|
|
27
|
+
cachedLines = show(text, excerpts, width).visualLines;
|
|
28
|
+
cachedWidth = width;
|
|
29
|
+
}
|
|
30
|
+
return cachedLines;
|
|
31
|
+
},
|
|
32
|
+
invalidate() {
|
|
33
|
+
cachedWidth = void 0;
|
|
34
|
+
cachedLines = void 0;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
export { makeShowRenderer };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region packages/core/show/index.d.ts
|
|
2
|
+
interface Excerpt {
|
|
3
|
+
focus: number | "head" | "tail";
|
|
4
|
+
context: number;
|
|
5
|
+
}
|
|
6
|
+
interface WindowResult<T> {
|
|
7
|
+
items: T[];
|
|
8
|
+
skippedRanges: Array<[number, number]>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* generic excerpt windowing. picks items to keep based on excerpts,
|
|
12
|
+
* inserts caller-provided elision markers for gaps.
|
|
13
|
+
*
|
|
14
|
+
* if excerpts is empty, returns all items unchanged.
|
|
15
|
+
*/
|
|
16
|
+
declare function windowItems<T>(items: T[], excerpts: Excerpt[], makeElision: (count: number) => T): WindowResult<T>;
|
|
17
|
+
interface ShowResult {
|
|
18
|
+
/** visual lines to render, with "... (N lines) ..." elision markers for gaps */
|
|
19
|
+
visualLines: string[];
|
|
20
|
+
/** ranges of visual lines omitted, as [startInclusive, endExclusive] pairs */
|
|
21
|
+
skippedRanges: Array<[number, number]>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* text-specific windowing: expands text to visual lines via pi-tui Text,
|
|
25
|
+
* then applies excerpt windowing.
|
|
26
|
+
*/
|
|
27
|
+
declare function show(text: string, excerpts: Excerpt[], width: number, paddingX?: number): ShowResult;
|
|
28
|
+
//#endregion
|
|
29
|
+
export { Excerpt, ShowResult, WindowResult, show, windowItems };
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
2
|
+
//#region packages/core/show/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* show — excerpt-based windowing for tool output.
|
|
5
|
+
*
|
|
6
|
+
* two layers:
|
|
7
|
+
* windowItems<T>() — generic windowing primitive. operates on any array.
|
|
8
|
+
* show() — text-specific wrapper: Text.render() → windowItems<string>().
|
|
9
|
+
*
|
|
10
|
+
* focus semantics:
|
|
11
|
+
* "head" — first `context` items (one-sided from start)
|
|
12
|
+
* "tail" — last `context` items (one-sided from end)
|
|
13
|
+
* N — ±context items around index N (symmetric)
|
|
14
|
+
*
|
|
15
|
+
* multiple excerpts are sorted and merged when overlapping or adjacent.
|
|
16
|
+
* gaps get an elision marker via the caller-provided makeElision factory.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* generic excerpt windowing. picks items to keep based on excerpts,
|
|
20
|
+
* inserts caller-provided elision markers for gaps.
|
|
21
|
+
*
|
|
22
|
+
* if excerpts is empty, returns all items unchanged.
|
|
23
|
+
*/
|
|
24
|
+
function windowItems(items, excerpts, makeElision) {
|
|
25
|
+
const total = items.length;
|
|
26
|
+
if (total === 0 || excerpts.length === 0) return {
|
|
27
|
+
items: [...items],
|
|
28
|
+
skippedRanges: []
|
|
29
|
+
};
|
|
30
|
+
const ranges = excerpts.map(({ focus, context }) => {
|
|
31
|
+
if (focus === "head") return [0, Math.min(context - 1, total - 1)];
|
|
32
|
+
else if (focus === "tail") return [Math.max(0, total - context), total - 1];
|
|
33
|
+
else return [Math.max(0, focus - context), Math.min(total - 1, focus + context)];
|
|
34
|
+
});
|
|
35
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
36
|
+
const merged = [];
|
|
37
|
+
for (const range of ranges) {
|
|
38
|
+
const lastMerged = merged[merged.length - 1];
|
|
39
|
+
if (merged.length === 0 || !lastMerged || range[0] > lastMerged[1] + 1) merged.push([range[0], range[1]]);
|
|
40
|
+
else lastMerged[1] = Math.max(lastMerged[1], range[1]);
|
|
41
|
+
}
|
|
42
|
+
const result = [];
|
|
43
|
+
const skippedRanges = [];
|
|
44
|
+
let cursor = 0;
|
|
45
|
+
for (const [start, end] of merged) {
|
|
46
|
+
if (cursor < start) {
|
|
47
|
+
skippedRanges.push([cursor, start]);
|
|
48
|
+
result.push(makeElision(start - cursor));
|
|
49
|
+
}
|
|
50
|
+
for (let i = start; i <= end; i++) {
|
|
51
|
+
const item = items[i];
|
|
52
|
+
if (item !== void 0) result.push(item);
|
|
53
|
+
}
|
|
54
|
+
cursor = end + 1;
|
|
55
|
+
}
|
|
56
|
+
if (cursor < total) {
|
|
57
|
+
skippedRanges.push([cursor, total]);
|
|
58
|
+
result.push(makeElision(total - cursor));
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
items: result,
|
|
62
|
+
skippedRanges
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* text-specific windowing: expands text to visual lines via pi-tui Text,
|
|
67
|
+
* then applies excerpt windowing.
|
|
68
|
+
*/
|
|
69
|
+
function show(text, excerpts, width, paddingX = 0) {
|
|
70
|
+
if (!text) return {
|
|
71
|
+
visualLines: [],
|
|
72
|
+
skippedRanges: []
|
|
73
|
+
};
|
|
74
|
+
const allVisualLines = new Text(text, paddingX, 0).render(width);
|
|
75
|
+
if (excerpts.length === 0) return {
|
|
76
|
+
visualLines: allVisualLines,
|
|
77
|
+
skippedRanges: []
|
|
78
|
+
};
|
|
79
|
+
const result = windowItems(allVisualLines, excerpts, (count) => `... (${count} ${count === 1 ? "line" : "lines"}) ...`);
|
|
80
|
+
return {
|
|
81
|
+
visualLines: result.items,
|
|
82
|
+
skippedRanges: result.skippedRanges
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (import.meta.vitest) {
|
|
86
|
+
const { describe, it, expect } = import.meta.vitest;
|
|
87
|
+
describe("windowItems", () => {
|
|
88
|
+
it("returns all items when no excerpts", () => {
|
|
89
|
+
const result = windowItems([
|
|
90
|
+
"a",
|
|
91
|
+
"b",
|
|
92
|
+
"c"
|
|
93
|
+
], [], (n) => `...${n}...`);
|
|
94
|
+
expect(result.items).toEqual([
|
|
95
|
+
"a",
|
|
96
|
+
"b",
|
|
97
|
+
"c"
|
|
98
|
+
]);
|
|
99
|
+
expect(result.skippedRanges).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
it("returns empty array for empty input", () => {
|
|
102
|
+
const result = windowItems([], [{
|
|
103
|
+
focus: 0,
|
|
104
|
+
context: 2
|
|
105
|
+
}], (n) => `...${n}...`);
|
|
106
|
+
expect(result.items).toEqual([]);
|
|
107
|
+
expect(result.skippedRanges).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
it("extracts head excerpt", () => {
|
|
110
|
+
const result = windowItems([
|
|
111
|
+
"a",
|
|
112
|
+
"b",
|
|
113
|
+
"c",
|
|
114
|
+
"d",
|
|
115
|
+
"e"
|
|
116
|
+
], [{
|
|
117
|
+
focus: "head",
|
|
118
|
+
context: 2
|
|
119
|
+
}], (n) => `...${n}...`);
|
|
120
|
+
expect(result.items).toEqual([
|
|
121
|
+
"a",
|
|
122
|
+
"b",
|
|
123
|
+
"...3..."
|
|
124
|
+
]);
|
|
125
|
+
expect(result.skippedRanges).toEqual([[2, 5]]);
|
|
126
|
+
});
|
|
127
|
+
it("extracts tail excerpt", () => {
|
|
128
|
+
const result = windowItems([
|
|
129
|
+
"a",
|
|
130
|
+
"b",
|
|
131
|
+
"c",
|
|
132
|
+
"d",
|
|
133
|
+
"e"
|
|
134
|
+
], [{
|
|
135
|
+
focus: "tail",
|
|
136
|
+
context: 2
|
|
137
|
+
}], (n) => `...${n}...`);
|
|
138
|
+
expect(result.items).toEqual([
|
|
139
|
+
"...3...",
|
|
140
|
+
"d",
|
|
141
|
+
"e"
|
|
142
|
+
]);
|
|
143
|
+
expect(result.skippedRanges).toEqual([[0, 3]]);
|
|
144
|
+
});
|
|
145
|
+
it("extracts symmetric excerpt around index", () => {
|
|
146
|
+
const result = windowItems([
|
|
147
|
+
"a",
|
|
148
|
+
"b",
|
|
149
|
+
"c",
|
|
150
|
+
"d",
|
|
151
|
+
"e"
|
|
152
|
+
], [{
|
|
153
|
+
focus: 2,
|
|
154
|
+
context: 1
|
|
155
|
+
}], (n) => `...${n}...`);
|
|
156
|
+
expect(result.items).toEqual([
|
|
157
|
+
"...1...",
|
|
158
|
+
"b",
|
|
159
|
+
"c",
|
|
160
|
+
"d",
|
|
161
|
+
"...1..."
|
|
162
|
+
]);
|
|
163
|
+
expect(result.skippedRanges).toEqual([[0, 1], [4, 5]]);
|
|
164
|
+
});
|
|
165
|
+
it("merges overlapping excerpts", () => {
|
|
166
|
+
const result = windowItems([
|
|
167
|
+
"a",
|
|
168
|
+
"b",
|
|
169
|
+
"c",
|
|
170
|
+
"d",
|
|
171
|
+
"e",
|
|
172
|
+
"f"
|
|
173
|
+
], [{
|
|
174
|
+
focus: 1,
|
|
175
|
+
context: 1
|
|
176
|
+
}, {
|
|
177
|
+
focus: 3,
|
|
178
|
+
context: 1
|
|
179
|
+
}], (n) => `...${n}...`);
|
|
180
|
+
expect(result.items).toEqual([
|
|
181
|
+
"a",
|
|
182
|
+
"b",
|
|
183
|
+
"c",
|
|
184
|
+
"d",
|
|
185
|
+
"e",
|
|
186
|
+
"...1..."
|
|
187
|
+
]);
|
|
188
|
+
expect(result.skippedRanges).toEqual([[5, 6]]);
|
|
189
|
+
});
|
|
190
|
+
it("merges adjacent excerpts", () => {
|
|
191
|
+
const result = windowItems([
|
|
192
|
+
"a",
|
|
193
|
+
"b",
|
|
194
|
+
"c",
|
|
195
|
+
"d",
|
|
196
|
+
"e"
|
|
197
|
+
], [{
|
|
198
|
+
focus: 0,
|
|
199
|
+
context: 0
|
|
200
|
+
}, {
|
|
201
|
+
focus: 1,
|
|
202
|
+
context: 0
|
|
203
|
+
}], (n) => `...${n}...`);
|
|
204
|
+
expect(result.items).toEqual([
|
|
205
|
+
"a",
|
|
206
|
+
"b",
|
|
207
|
+
"...3..."
|
|
208
|
+
]);
|
|
209
|
+
expect(result.skippedRanges).toEqual([[2, 5]]);
|
|
210
|
+
});
|
|
211
|
+
it("clamps head context to available items", () => {
|
|
212
|
+
const result = windowItems(["a", "b"], [{
|
|
213
|
+
focus: "head",
|
|
214
|
+
context: 5
|
|
215
|
+
}], (n) => `...${n}...`);
|
|
216
|
+
expect(result.items).toEqual(["a", "b"]);
|
|
217
|
+
expect(result.skippedRanges).toEqual([]);
|
|
218
|
+
});
|
|
219
|
+
it("clamps tail context to available items", () => {
|
|
220
|
+
const result = windowItems(["a", "b"], [{
|
|
221
|
+
focus: "tail",
|
|
222
|
+
context: 5
|
|
223
|
+
}], (n) => `...${n}...`);
|
|
224
|
+
expect(result.items).toEqual(["a", "b"]);
|
|
225
|
+
expect(result.skippedRanges).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
it("clamps symmetric context to bounds", () => {
|
|
228
|
+
const result = windowItems([
|
|
229
|
+
"a",
|
|
230
|
+
"b",
|
|
231
|
+
"c"
|
|
232
|
+
], [{
|
|
233
|
+
focus: 0,
|
|
234
|
+
context: 5
|
|
235
|
+
}], (n) => `...${n}...`);
|
|
236
|
+
expect(result.items).toEqual([
|
|
237
|
+
"a",
|
|
238
|
+
"b",
|
|
239
|
+
"c"
|
|
240
|
+
]);
|
|
241
|
+
expect(result.skippedRanges).toEqual([]);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
describe("show", () => {
|
|
245
|
+
it("returns empty for empty text", () => {
|
|
246
|
+
const result = show("", [{
|
|
247
|
+
focus: "head",
|
|
248
|
+
context: 2
|
|
249
|
+
}], 80);
|
|
250
|
+
expect(result.visualLines).toEqual([]);
|
|
251
|
+
expect(result.skippedRanges).toEqual([]);
|
|
252
|
+
});
|
|
253
|
+
it("returns all lines when no excerpts", () => {
|
|
254
|
+
const result = show("a\nb\nc", [], 80);
|
|
255
|
+
expect(result.visualLines.length).toBe(3);
|
|
256
|
+
expect(result.visualLines[0]?.trim()).toBe("a");
|
|
257
|
+
expect(result.visualLines[1]?.trim()).toBe("b");
|
|
258
|
+
expect(result.visualLines[2]?.trim()).toBe("c");
|
|
259
|
+
expect(result.skippedRanges).toEqual([]);
|
|
260
|
+
});
|
|
261
|
+
it("applies head excerpt to visual lines", () => {
|
|
262
|
+
const result = show("line1\nline2\nline3\nline4\nline5", [{
|
|
263
|
+
focus: "head",
|
|
264
|
+
context: 2
|
|
265
|
+
}], 80);
|
|
266
|
+
expect(result.visualLines.length).toBe(3);
|
|
267
|
+
expect(result.visualLines[0]?.trim()).toBe("line1");
|
|
268
|
+
expect(result.visualLines[1]?.trim()).toBe("line2");
|
|
269
|
+
expect(result.visualLines[2]).toBe("... (3 lines) ...");
|
|
270
|
+
expect(result.skippedRanges).toEqual([[2, 5]]);
|
|
271
|
+
});
|
|
272
|
+
it("uses singular 'line' for single skipped line", () => {
|
|
273
|
+
const result = show("line1\nline2\nline3", [{
|
|
274
|
+
focus: "head",
|
|
275
|
+
context: 2
|
|
276
|
+
}], 80);
|
|
277
|
+
expect(result.visualLines.length).toBe(3);
|
|
278
|
+
expect(result.visualLines[0]?.trim()).toBe("line1");
|
|
279
|
+
expect(result.visualLines[1]?.trim()).toBe("line2");
|
|
280
|
+
expect(result.visualLines[2]).toBe("... (1 line) ...");
|
|
281
|
+
});
|
|
282
|
+
it("wraps long lines at width", () => {
|
|
283
|
+
expect(show("a".repeat(100), [], 50).visualLines.length).toBeGreaterThan(1);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
export { show, windowItems };
|