Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- packages/agent/src/harness/tools/write.ts +44 -0
- packages/coding-agent/docs/tui.md +961 -0
- packages/coding-agent/install-lock/package-lock.json +1917 -0
- packages/coding-agent/install-lock/package.json +15 -0
- packages/coding-agent/src/core/compaction/branch-summarization.ts +382 -0
- packages/coding-agent/src/core/compaction/compaction.ts +1012 -0
- packages/coding-agent/src/core/compaction/index.ts +7 -0
- packages/coding-agent/src/core/compaction/utils.ts +158 -0
- packages/coding-agent/src/core/export-html/ansi-to-html.ts +258 -0
- packages/coding-agent/src/core/export-html/index.ts +316 -0
- packages/coding-agent/src/core/export-html/template.css +1066 -0
- packages/coding-agent/src/core/export-html/template.html +55 -0
- packages/coding-agent/src/core/export-html/template.js +1864 -0
- packages/coding-agent/src/core/export-html/tool-renderer.ts +172 -0
- packages/coding-agent/src/core/export-html/vendor/highlight.min.js +0 -0
- packages/coding-agent/src/core/export-html/vendor/marked.min.js +78 -0
- packages/coding-agent/src/core/extensions/index.ts +193 -0
- packages/coding-agent/src/core/extensions/loader.ts +809 -0
- packages/coding-agent/src/core/extensions/runner.ts +1286 -0
- packages/coding-agent/src/core/extensions/types.ts +1797 -0
- packages/coding-agent/src/core/extensions/wrapper.ts +45 -0
- packages/coding-agent/src/core/tools/bash.ts +400 -0
- packages/coding-agent/src/core/tools/edit-diff.ts +556 -0
- packages/coding-agent/src/core/tools/edit.ts +220 -0
- packages/coding-agent/src/core/tools/file-mutation-queue.ts +61 -0
- packages/coding-agent/src/core/tools/find.ts +318 -0
- packages/coding-agent/src/core/tools/grep.ts +323 -0
- packages/coding-agent/src/core/tools/index.ts +224 -0
- packages/coding-agent/src/core/tools/ls.ts +175 -0
- packages/coding-agent/src/core/tools/output-accumulator.ts +222 -0
- packages/coding-agent/src/core/tools/path-utils.ts +118 -0
- packages/coding-agent/src/core/tools/powershell.ts +67 -0
- packages/coding-agent/src/core/tools/read.ts +197 -0
- packages/coding-agent/src/core/tools/render-utils.ts +85 -0
- packages/coding-agent/src/core/tools/renderers/bash.ts +163 -0
- packages/coding-agent/src/core/tools/renderers/edit.ts +238 -0
- packages/coding-agent/src/core/tools/renderers/find.ts +77 -0
- packages/coding-agent/src/core/tools/renderers/grep.ts +82 -0
- packages/coding-agent/src/core/tools/renderers/index.ts +63 -0
- packages/coding-agent/src/core/tools/renderers/ls.ts +70 -0
- packages/coding-agent/src/core/tools/renderers/read.ts +175 -0
- packages/coding-agent/src/core/tools/renderers/write.ts +181 -0
- packages/coding-agent/src/core/tools/tool-definition-wrapper.ts +47 -0
- packages/coding-agent/src/core/tools/truncate.ts +276 -0
- packages/coding-agent/src/core/tools/write.ts +97 -0
- packages/coding-agent/src/experimental/mini/README.md +107 -0
- packages/coding-agent/src/experimental/mini/main.ts +24 -0
- packages/coding-agent/src/experimental/mini/server/entry.ts +12 -0
- packages/coding-agent/src/experimental/mini/server/run.ts +160 -0
- packages/coding-agent/src/experimental/mini/shared/protocol.ts +138 -0
packages/agent/src/harness/tools/write.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type Static, Type } from "typebox";
|
| 2 |
+
import type { AgentHarnessTool } from "../types.ts";
|
| 3 |
+
import { getOrThrow } from "../types.ts";
|
| 4 |
+
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
| 5 |
+
import { resolveToolPath } from "./path-utils.ts";
|
| 6 |
+
import type { ExecutionToolContext } from "./tool-context.ts";
|
| 7 |
+
|
| 8 |
+
const writeSchema = Type.Object({
|
| 9 |
+
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
| 10 |
+
content: Type.String({ description: "Content to write to the file" }),
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
export type WriteToolInput = Static<typeof writeSchema>;
|
| 14 |
+
|
| 15 |
+
export function createWriteTool<TContext extends ExecutionToolContext = ExecutionToolContext>(): AgentHarnessTool<
|
| 16 |
+
TContext,
|
| 17 |
+
typeof writeSchema,
|
| 18 |
+
undefined
|
| 19 |
+
> {
|
| 20 |
+
return {
|
| 21 |
+
name: "write",
|
| 22 |
+
label: "write",
|
| 23 |
+
description:
|
| 24 |
+
"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
| 25 |
+
parameters: writeSchema,
|
| 26 |
+
async execute(_toolCallId, { path, content }, _onUpdate, { env }, _invocation, context) {
|
| 27 |
+
const absolutePath = await resolveToolPath(env, path, context);
|
| 28 |
+
return withFileMutationQueue(
|
| 29 |
+
env,
|
| 30 |
+
absolutePath,
|
| 31 |
+
async () => {
|
| 32 |
+
if (context.abortSignal?.aborted) throw new Error("Operation aborted");
|
| 33 |
+
getOrThrow(await env.writeFile(absolutePath, content, context));
|
| 34 |
+
if (context.abortSignal?.aborted) throw new Error("Operation aborted");
|
| 35 |
+
return {
|
| 36 |
+
content: [{ type: "text", text: `Successfully wrote to ${path}` }],
|
| 37 |
+
details: undefined,
|
| 38 |
+
};
|
| 39 |
+
},
|
| 40 |
+
context,
|
| 41 |
+
);
|
| 42 |
+
},
|
| 43 |
+
};
|
| 44 |
+
}
|
packages/coding-agent/docs/tui.md
ADDED
|
@@ -0,0 +1,961 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
> pi can create TUI components. Ask it to build one for your use case.
|
| 2 |
+
|
| 3 |
+
# TUI Components
|
| 4 |
+
|
| 5 |
+
Extensions and custom tools can render custom TUI components for interactive user interfaces. This page covers the component system and available building blocks.
|
| 6 |
+
|
| 7 |
+
**Source:** [`@earendil-works/pi-tui`](https://github.com/earendil-works/pi/tree/main/packages/tui)
|
| 8 |
+
|
| 9 |
+
## Component Interface
|
| 10 |
+
|
| 11 |
+
All components implement:
|
| 12 |
+
|
| 13 |
+
```typescript
|
| 14 |
+
interface Component {
|
| 15 |
+
render(width: number): string[];
|
| 16 |
+
handleInput?(data: string): void;
|
| 17 |
+
handleMouse?(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
| 18 |
+
wantsKeyRelease?: boolean;
|
| 19 |
+
invalidate(): void;
|
| 20 |
+
}
|
| 21 |
+
```
|
| 22 |
+
|
| 23 |
+
| Method | Description |
|
| 24 |
+
|--------|-------------|
|
| 25 |
+
| `render(width)` | Return array of strings (one per line). Each line **must not exceed `width`**. |
|
| 26 |
+
| `handleInput?(data)` | Receive keyboard input when component has focus. |
|
| 27 |
+
| `handleMouse?(event)` | Receive normalized pointer input in fullscreen mode. |
|
| 28 |
+
| `wantsKeyRelease?` | If true, component receives key release events (Kitty protocol). Default: false. |
|
| 29 |
+
| `invalidate()` | Clear cached render state. Called on theme changes. |
|
| 30 |
+
|
| 31 |
+
The TUI appends a full SGR reset and OSC 8 reset at the end of each rendered line. Styles do not carry across lines. If you emit multi-line text with styling, reapply styles per line or use `wrapTextWithAnsi()` so styles are preserved for each wrapped line.
|
| 32 |
+
|
| 33 |
+
## Focusable Interface (IME Support)
|
| 34 |
+
|
| 35 |
+
Components that display a text cursor and need IME (Input Method Editor) support should implement the `Focusable` interface:
|
| 36 |
+
|
| 37 |
+
```typescript
|
| 38 |
+
import { CURSOR_MARKER, type Component, type Focusable } from "@earendil-works/pi-tui";
|
| 39 |
+
|
| 40 |
+
class MyInput implements Component, Focusable {
|
| 41 |
+
focused: boolean = false; // Set by TUI when focus changes
|
| 42 |
+
|
| 43 |
+
render(width: number): string[] {
|
| 44 |
+
const marker = this.focused ? CURSOR_MARKER : "";
|
| 45 |
+
// Emit marker right before the fake cursor
|
| 46 |
+
return [`> ${beforeCursor}${marker}\x1b[7m${atCursor}\x1b[27m${afterCursor}`];
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
When a `Focusable` component has focus, TUI:
|
| 52 |
+
1. Sets `focused = true` on the component
|
| 53 |
+
2. Scans rendered output for `CURSOR_MARKER` (a zero-width APC escape sequence)
|
| 54 |
+
3. Positions the hardware terminal cursor at that location
|
| 55 |
+
4. Shows the hardware cursor only when `showHardwareCursor` is enabled
|
| 56 |
+
|
| 57 |
+
The cursor remains hidden by default. This keeps the fake cursor rendering, while still positioning the hardware cursor for terminals that track IME candidate windows with hidden cursors. Some terminals require a visible hardware cursor for IME positioning; enable it with the renderer's `showHardwareCursor` constructor argument or `setShowHardwareCursor(true)`. Pi also maps `PI_HARDWARE_CURSOR=1` to this setting before it creates its renderer. The `Editor` and `Input` built-in components already implement this interface.
|
| 58 |
+
|
| 59 |
+
### Container Components with Embedded Inputs
|
| 60 |
+
|
| 61 |
+
When a container component (dialog, selector, etc.) contains an `Input` or `Editor` child, the container must implement `Focusable` and propagate the focus state to the child. Otherwise, the hardware cursor won't be positioned correctly for IME input.
|
| 62 |
+
|
| 63 |
+
```typescript
|
| 64 |
+
import { Container, type Focusable, Input } from "@earendil-works/pi-tui";
|
| 65 |
+
|
| 66 |
+
class SearchDialog extends Container implements Focusable {
|
| 67 |
+
private searchInput: Input;
|
| 68 |
+
|
| 69 |
+
// Focusable implementation - propagate to child input for IME cursor positioning
|
| 70 |
+
private _focused = false;
|
| 71 |
+
get focused(): boolean {
|
| 72 |
+
return this._focused;
|
| 73 |
+
}
|
| 74 |
+
set focused(value: boolean) {
|
| 75 |
+
this._focused = value;
|
| 76 |
+
this.searchInput.focused = value;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
constructor() {
|
| 80 |
+
super();
|
| 81 |
+
this.searchInput = new Input();
|
| 82 |
+
this.addChild(this.searchInput);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Without this propagation, typing with an IME (Chinese, Japanese, Korean, etc.) will show the candidate window in the wrong position on screen.
|
| 88 |
+
|
| 89 |
+
## Using Components
|
| 90 |
+
|
| 91 |
+
**In extensions** via `ctx.ui.custom()`:
|
| 92 |
+
|
| 93 |
+
```typescript
|
| 94 |
+
pi.on("session_start", async (_event, ctx) => {
|
| 95 |
+
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>
|
| 96 |
+
new MyComponent({
|
| 97 |
+
theme,
|
| 98 |
+
keybindings,
|
| 99 |
+
onChange: () => tui.requestRender(),
|
| 100 |
+
onSelect: (value) => done(value),
|
| 101 |
+
onCancel: () => done(null),
|
| 102 |
+
})
|
| 103 |
+
);
|
| 104 |
+
});
|
| 105 |
+
```
|
| 106 |
+
|
| 107 |
+
**In custom tools** via `ctx.ui.custom()`:
|
| 108 |
+
|
| 109 |
+
```typescript
|
| 110 |
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
| 111 |
+
const result = await ctx.ui.custom<string | null>((tui, theme, keybindings, done) =>
|
| 112 |
+
new MyComponent({
|
| 113 |
+
theme,
|
| 114 |
+
keybindings,
|
| 115 |
+
onChange: () => tui.requestRender(),
|
| 116 |
+
onSelect: (value) => done(value),
|
| 117 |
+
onCancel: () => done(null),
|
| 118 |
+
})
|
| 119 |
+
);
|
| 120 |
+
// Use result...
|
| 121 |
+
}
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
## Overlays
|
| 125 |
+
|
| 126 |
+
Overlays render components on top of existing content without clearing the screen. Pass `{ overlay: true }` to `ctx.ui.custom()`:
|
| 127 |
+
|
| 128 |
+
```typescript
|
| 129 |
+
const result = await ctx.ui.custom<string | null>(
|
| 130 |
+
(tui, theme, keybindings, done) => new MyDialog({ onClose: done }),
|
| 131 |
+
{ overlay: true }
|
| 132 |
+
);
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
For positioning and sizing, use `overlayOptions`:
|
| 136 |
+
|
| 137 |
+
```typescript
|
| 138 |
+
const result = await ctx.ui.custom<string | null>(
|
| 139 |
+
(tui, theme, keybindings, done) => new SidePanel({ onClose: done }),
|
| 140 |
+
{
|
| 141 |
+
overlay: true,
|
| 142 |
+
overlayOptions: {
|
| 143 |
+
// Size: number or percentage string
|
| 144 |
+
width: "50%", // 50% of terminal width
|
| 145 |
+
minWidth: 40, // minimum 40 columns
|
| 146 |
+
maxHeight: "80%", // max 80% of terminal height
|
| 147 |
+
|
| 148 |
+
// Position: anchor-based (default: "center")
|
| 149 |
+
anchor: "right-center", // 9 positions: center, top-left, top-center, etc.
|
| 150 |
+
offsetX: -2, // offset from anchor
|
| 151 |
+
offsetY: 0,
|
| 152 |
+
|
| 153 |
+
// Or percentage/absolute positioning
|
| 154 |
+
row: "25%", // 25% from top
|
| 155 |
+
col: 10, // column 10
|
| 156 |
+
|
| 157 |
+
// Margins
|
| 158 |
+
margin: 2, // all sides, or { top, right, bottom, left }
|
| 159 |
+
|
| 160 |
+
// Responsive: hide on narrow terminals
|
| 161 |
+
visible: (termWidth, termHeight) => termWidth >= 80,
|
| 162 |
+
},
|
| 163 |
+
// Get handle for programmatic focus and visibility control
|
| 164 |
+
onHandle: (handle) => {
|
| 165 |
+
// handle.focus() - focus this overlay and bring it to the visual front
|
| 166 |
+
// handle.unfocus() - release input to normal fallback
|
| 167 |
+
// handle.unfocus({ target }) - release input to a specific component or null
|
| 168 |
+
// handle.setHidden(true/false) - toggle visibility
|
| 169 |
+
// handle.hide() - permanently remove
|
| 170 |
+
},
|
| 171 |
+
}
|
| 172 |
+
);
|
| 173 |
+
```
|
| 174 |
+
|
| 175 |
+
### Overlay Focus
|
| 176 |
+
|
| 177 |
+
A focused visible overlay keeps input ownership across temporary non-overlay UI. If an overlay opens another `ctx.ui.custom()` component without `{ overlay: true }`, that replacement UI receives input while it is active; when it closes, the focused overlay can reclaim input.
|
| 178 |
+
|
| 179 |
+
Use `handle.unfocus()` when a visible overlay should stop owning input and let TUI fall back to another visible capturing overlay or the previous focus target. Use `handle.unfocus({ target })` when a specific component should receive input while the overlay stays visible. Passing `{ target: null }` intentionally leaves no focused component until focus is set again.
|
| 180 |
+
|
| 181 |
+
### Overlay Lifecycle
|
| 182 |
+
|
| 183 |
+
Overlay components are disposed when closed. Don't reuse references - create fresh instances:
|
| 184 |
+
|
| 185 |
+
```typescript
|
| 186 |
+
// Wrong - stale reference
|
| 187 |
+
let menu: MenuComponent;
|
| 188 |
+
await ctx.ui.custom((_, __, ___, done) => {
|
| 189 |
+
menu = new MenuComponent(done);
|
| 190 |
+
return menu;
|
| 191 |
+
}, { overlay: true });
|
| 192 |
+
setActiveComponent(menu); // Disposed
|
| 193 |
+
|
| 194 |
+
// Correct - re-call to re-show
|
| 195 |
+
const showMenu = () => ctx.ui.custom((_, __, ___, done) =>
|
| 196 |
+
new MenuComponent(done), { overlay: true });
|
| 197 |
+
|
| 198 |
+
await showMenu(); // First show
|
| 199 |
+
await showMenu(); // "Back" = just call again
|
| 200 |
+
```
|
| 201 |
+
|
| 202 |
+
See [overlay-qa-tests.ts](../examples/extensions/overlay-qa-tests.ts) for comprehensive examples covering anchors, margins, stacking, responsive visibility, and animation.
|
| 203 |
+
|
| 204 |
+
## Built-in Components
|
| 205 |
+
|
| 206 |
+
Import from `@earendil-works/pi-tui`:
|
| 207 |
+
|
| 208 |
+
```typescript
|
| 209 |
+
import { Text, Box, Container, Spacer, Markdown } from "@earendil-works/pi-tui";
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
### Text
|
| 213 |
+
|
| 214 |
+
Multi-line text with word wrapping.
|
| 215 |
+
|
| 216 |
+
```typescript
|
| 217 |
+
const text = new Text(
|
| 218 |
+
"Hello World", // content
|
| 219 |
+
1, // paddingX (default: 1)
|
| 220 |
+
1, // paddingY (default: 1)
|
| 221 |
+
(s) => bgGray(s) // optional background function
|
| 222 |
+
);
|
| 223 |
+
text.setText("Updated");
|
| 224 |
+
```
|
| 225 |
+
|
| 226 |
+
### Box
|
| 227 |
+
|
| 228 |
+
Container with padding and background color.
|
| 229 |
+
|
| 230 |
+
```typescript
|
| 231 |
+
const box = new Box(
|
| 232 |
+
1, // paddingX
|
| 233 |
+
1, // paddingY
|
| 234 |
+
(s) => bgGray(s) // background function
|
| 235 |
+
);
|
| 236 |
+
box.addChild(new Text("Content", 0, 0));
|
| 237 |
+
box.setBgFn((s) => bgBlue(s));
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
### Container
|
| 241 |
+
|
| 242 |
+
Groups child components vertically.
|
| 243 |
+
|
| 244 |
+
```typescript
|
| 245 |
+
const container = new Container();
|
| 246 |
+
container.addChild(component1);
|
| 247 |
+
container.addChild(component2);
|
| 248 |
+
container.removeChild(component1);
|
| 249 |
+
```
|
| 250 |
+
|
| 251 |
+
### Spacer
|
| 252 |
+
|
| 253 |
+
Empty vertical space.
|
| 254 |
+
|
| 255 |
+
```typescript
|
| 256 |
+
const spacer = new Spacer(2); // 2 empty lines
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
### Markdown
|
| 260 |
+
|
| 261 |
+
Renders markdown with syntax highlighting.
|
| 262 |
+
|
| 263 |
+
```typescript
|
| 264 |
+
const md = new Markdown(
|
| 265 |
+
"# Title\n\nSome **bold** text",
|
| 266 |
+
1, // paddingX
|
| 267 |
+
1, // paddingY
|
| 268 |
+
theme // MarkdownTheme (see below)
|
| 269 |
+
);
|
| 270 |
+
md.setText("Updated markdown");
|
| 271 |
+
```
|
| 272 |
+
|
| 273 |
+
### Image
|
| 274 |
+
|
| 275 |
+
Renders images in supported terminals (Kitty, iTerm2, Ghostty, WezTerm, Warp).
|
| 276 |
+
|
| 277 |
+
```typescript
|
| 278 |
+
const image = new Image(
|
| 279 |
+
base64Data, // base64-encoded image
|
| 280 |
+
"image/png", // MIME type
|
| 281 |
+
theme, // ImageTheme
|
| 282 |
+
{ maxWidthCells: 80, maxHeightCells: 24 }
|
| 283 |
+
);
|
| 284 |
+
```
|
| 285 |
+
|
| 286 |
+
## Keyboard Input
|
| 287 |
+
|
| 288 |
+
Use `matchesKey()` for key detection:
|
| 289 |
+
|
| 290 |
+
```typescript
|
| 291 |
+
import { matchesKey, Key } from "@earendil-works/pi-tui";
|
| 292 |
+
|
| 293 |
+
handleInput(data: string) {
|
| 294 |
+
if (matchesKey(data, Key.up)) {
|
| 295 |
+
this.selectedIndex--;
|
| 296 |
+
} else if (matchesKey(data, Key.enter)) {
|
| 297 |
+
this.onSelect?.(this.selectedIndex);
|
| 298 |
+
} else if (matchesKey(data, Key.escape)) {
|
| 299 |
+
this.onCancel?.();
|
| 300 |
+
} else if (matchesKey(data, Key.ctrl("c"))) {
|
| 301 |
+
// Ctrl+C
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
**Key identifiers** (use `Key.*` for autocomplete, or string literals):
|
| 307 |
+
- Basic keys: `Key.enter`, `Key.escape`, `Key.tab`, `Key.space`, `Key.backspace`, `Key.delete`, `Key.home`, `Key.end`
|
| 308 |
+
- Arrow keys: `Key.up`, `Key.down`, `Key.left`, `Key.right`
|
| 309 |
+
- With modifiers: `Key.ctrl("c")`, `Key.shift("tab")`, `Key.alt("left")`, `Key.ctrlShift("p")`
|
| 310 |
+
- String format also works: `"enter"`, `"ctrl+c"`, `"shift+tab"`, `"ctrl+shift+p"`
|
| 311 |
+
|
| 312 |
+
## Mouse Input
|
| 313 |
+
|
| 314 |
+
Fullscreen mode routes normalized press, release, click, move, drag, and wheel events to components and overlays. Return `{ handled: true }` to suppress default behavior, `capture: true` to retain drag/release ownership, `focus: true` to request keyboard focus, and `render: true` when a hover or release visibly changes the component. Press, click, drag, and wheel render by default; no-op move/release events do not.
|
| 315 |
+
|
| 316 |
+
```typescript
|
| 317 |
+
import { MouseRegion } from "@earendil-works/pi-tui";
|
| 318 |
+
|
| 319 |
+
const clickable = new MouseRegion(content, (event) => {
|
| 320 |
+
if (event.type !== "click" || event.button !== "left") return undefined;
|
| 321 |
+
expanded = !expanded;
|
| 322 |
+
return { handled: true };
|
| 323 |
+
});
|
| 324 |
+
```
|
| 325 |
+
|
| 326 |
+
Unhandled wheel input scrolls the nearest `ScrollView`; unhandled primary-button drags retain transcript selection. OSC 8 links take precedence over parent click regions. `Input`, `Editor`, `SelectList`, and `SettingsList` include fullscreen mouse behavior. Regular mode does not capture mouse input because the terminal owns its scrollback.
|
| 327 |
+
|
| 328 |
+
## Line Width
|
| 329 |
+
|
| 330 |
+
**Critical:** Each line from `render()` must not exceed the `width` parameter.
|
| 331 |
+
|
| 332 |
+
```typescript
|
| 333 |
+
import { visibleWidth, truncateToWidth } from "@earendil-works/pi-tui";
|
| 334 |
+
|
| 335 |
+
render(width: number): string[] {
|
| 336 |
+
// Truncate long lines
|
| 337 |
+
return [truncateToWidth(this.text, width)];
|
| 338 |
+
}
|
| 339 |
+
```
|
| 340 |
+
|
| 341 |
+
Utilities:
|
| 342 |
+
- `visibleWidth(str)` - Get display width (ignores ANSI codes)
|
| 343 |
+
- `truncateToWidth(str, width, ellipsis?)` - Truncate with optional ellipsis
|
| 344 |
+
- `wrapTextWithAnsi(str, width)` - Word wrap preserving ANSI codes
|
| 345 |
+
|
| 346 |
+
## Creating Custom Components
|
| 347 |
+
|
| 348 |
+
Example: Interactive selector
|
| 349 |
+
|
| 350 |
+
```typescript
|
| 351 |
+
import {
|
| 352 |
+
matchesKey, Key,
|
| 353 |
+
truncateToWidth, visibleWidth
|
| 354 |
+
} from "@earendil-works/pi-tui";
|
| 355 |
+
|
| 356 |
+
class MySelector {
|
| 357 |
+
private items: string[];
|
| 358 |
+
private selected = 0;
|
| 359 |
+
private cachedWidth?: number;
|
| 360 |
+
private cachedLines?: string[];
|
| 361 |
+
|
| 362 |
+
public onSelect?: (item: string) => void;
|
| 363 |
+
public onCancel?: () => void;
|
| 364 |
+
|
| 365 |
+
constructor(items: string[]) {
|
| 366 |
+
this.items = items;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
handleInput(data: string): void {
|
| 370 |
+
if (matchesKey(data, Key.up) && this.selected > 0) {
|
| 371 |
+
this.selected--;
|
| 372 |
+
this.invalidate();
|
| 373 |
+
} else if (matchesKey(data, Key.down) && this.selected < this.items.length - 1) {
|
| 374 |
+
this.selected++;
|
| 375 |
+
this.invalidate();
|
| 376 |
+
} else if (matchesKey(data, Key.enter)) {
|
| 377 |
+
this.onSelect?.(this.items[this.selected]);
|
| 378 |
+
} else if (matchesKey(data, Key.escape)) {
|
| 379 |
+
this.onCancel?.();
|
| 380 |
+
}
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
render(width: number): string[] {
|
| 384 |
+
if (this.cachedLines && this.cachedWidth === width) {
|
| 385 |
+
return this.cachedLines;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
this.cachedLines = this.items.map((item, i) => {
|
| 389 |
+
const prefix = i === this.selected ? "> " : " ";
|
| 390 |
+
return truncateToWidth(prefix + item, width);
|
| 391 |
+
});
|
| 392 |
+
this.cachedWidth = width;
|
| 393 |
+
return this.cachedLines;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
invalidate(): void {
|
| 397 |
+
this.cachedWidth = undefined;
|
| 398 |
+
this.cachedLines = undefined;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
```
|
| 402 |
+
|
| 403 |
+
Usage in an extension:
|
| 404 |
+
|
| 405 |
+
```typescript
|
| 406 |
+
pi.registerCommand("pick", {
|
| 407 |
+
description: "Pick an item",
|
| 408 |
+
handler: async (_args, ctx) => {
|
| 409 |
+
const items = ["Option A", "Option B", "Option C"];
|
| 410 |
+
const selected = await ctx.ui.custom<string | null>((tui, _theme, _keybindings, done) => {
|
| 411 |
+
const selector = new MySelector(items);
|
| 412 |
+
selector.onSelect = done;
|
| 413 |
+
selector.onCancel = () => done(null);
|
| 414 |
+
|
| 415 |
+
return {
|
| 416 |
+
render: (width) => selector.render(width),
|
| 417 |
+
handleInput: (data) => {
|
| 418 |
+
selector.handleInput(data);
|
| 419 |
+
tui.requestRender();
|
| 420 |
+
},
|
| 421 |
+
invalidate: () => selector.invalidate(),
|
| 422 |
+
};
|
| 423 |
+
});
|
| 424 |
+
|
| 425 |
+
if (selected !== null) {
|
| 426 |
+
ctx.ui.notify(`Selected: ${selected}`, "info");
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
});
|
| 430 |
+
```
|
| 431 |
+
|
| 432 |
+
## Theming
|
| 433 |
+
|
| 434 |
+
Components accept theme objects for styling.
|
| 435 |
+
|
| 436 |
+
**In `renderCall`/`renderResult`**, use the `theme` parameter:
|
| 437 |
+
|
| 438 |
+
```typescript
|
| 439 |
+
renderResult(result, options, theme, context) {
|
| 440 |
+
// Use theme.fg() for foreground colors
|
| 441 |
+
return new Text(theme.fg("success", "Done!"), 0, 0);
|
| 442 |
+
|
| 443 |
+
// Use theme.bg() for background colors
|
| 444 |
+
const styled = theme.bg("toolPendingBg", theme.fg("accent", "text"));
|
| 445 |
+
}
|
| 446 |
+
```
|
| 447 |
+
|
| 448 |
+
**Foreground colors** (`theme.fg(color, text)`):
|
| 449 |
+
|
| 450 |
+
| Category | Colors |
|
| 451 |
+
|----------|--------|
|
| 452 |
+
| General | `text`, `accent`, `muted`, `dim`, `searchMatchText` |
|
| 453 |
+
| Status | `success`, `error`, `warning` |
|
| 454 |
+
| Borders | `border`, `borderAccent`, `borderMuted` |
|
| 455 |
+
| Messages | `userMessageText`, `customMessageText`, `customMessageLabel` |
|
| 456 |
+
| Tools | `toolTitle`, `toolOutput` |
|
| 457 |
+
| Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` |
|
| 458 |
+
| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` |
|
| 459 |
+
| Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` |
|
| 460 |
+
| Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh`, `thinkingMax` |
|
| 461 |
+
| Modes | `bashMode` |
|
| 462 |
+
|
| 463 |
+
**Background colors** (`theme.bg(color, text)`):
|
| 464 |
+
|
| 465 |
+
`selectedBg`, `searchMatchBg`, `userMessageBg`, `customMessageBg`, `toolPendingBg`, `toolSuccessBg`, `toolErrorBg`
|
| 466 |
+
|
| 467 |
+
**For Markdown**, use `getMarkdownTheme()`:
|
| 468 |
+
|
| 469 |
+
```typescript
|
| 470 |
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
| 471 |
+
import { Markdown } from "@earendil-works/pi-tui";
|
| 472 |
+
|
| 473 |
+
renderResult(result, options, theme, context) {
|
| 474 |
+
const mdTheme = getMarkdownTheme();
|
| 475 |
+
return new Markdown(result.details.markdown, 0, 0, mdTheme);
|
| 476 |
+
}
|
| 477 |
+
```
|
| 478 |
+
|
| 479 |
+
**For custom components**, define your own theme interface:
|
| 480 |
+
|
| 481 |
+
```typescript
|
| 482 |
+
interface MyTheme {
|
| 483 |
+
selected: (s: string) => string;
|
| 484 |
+
normal: (s: string) => string;
|
| 485 |
+
}
|
| 486 |
+
```
|
| 487 |
+
|
| 488 |
+
## Debug logging
|
| 489 |
+
|
| 490 |
+
Set `PI_TUI_WRITE_LOG` to capture the raw ANSI stream written to stdout.
|
| 491 |
+
|
| 492 |
+
```bash
|
| 493 |
+
PI_TUI_WRITE_LOG=/tmp/tui-ansi.log npx tsx packages/tui/test/chat-simple.ts
|
| 494 |
+
```
|
| 495 |
+
|
| 496 |
+
## Performance
|
| 497 |
+
|
| 498 |
+
Cache rendered output when possible:
|
| 499 |
+
|
| 500 |
+
```typescript
|
| 501 |
+
class CachedComponent {
|
| 502 |
+
private cachedWidth?: number;
|
| 503 |
+
private cachedLines?: string[];
|
| 504 |
+
|
| 505 |
+
render(width: number): string[] {
|
| 506 |
+
if (this.cachedLines && this.cachedWidth === width) {
|
| 507 |
+
return this.cachedLines;
|
| 508 |
+
}
|
| 509 |
+
// ... compute lines ...
|
| 510 |
+
this.cachedWidth = width;
|
| 511 |
+
this.cachedLines = lines;
|
| 512 |
+
return lines;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
invalidate(): void {
|
| 516 |
+
this.cachedWidth = undefined;
|
| 517 |
+
this.cachedLines = undefined;
|
| 518 |
+
}
|
| 519 |
+
}
|
| 520 |
+
```
|
| 521 |
+
|
| 522 |
+
Call `invalidate()` when state changes, then use the injected `tui.requestRender()` to trigger re-render.
|
| 523 |
+
|
| 524 |
+
## Invalidation and Theme Changes
|
| 525 |
+
|
| 526 |
+
When the theme changes, the TUI calls `invalidate()` on all components to clear their caches. Components must properly implement `invalidate()` to ensure theme changes take effect.
|
| 527 |
+
|
| 528 |
+
### The Problem
|
| 529 |
+
|
| 530 |
+
If a component pre-bakes theme colors into strings (via `theme.fg()`, `theme.bg()`, etc.) and caches them, the cached strings contain ANSI escape codes from the old theme. Simply clearing the render cache isn't enough if the component stores the themed content separately.
|
| 531 |
+
|
| 532 |
+
**Wrong approach** (theme colors won't update):
|
| 533 |
+
|
| 534 |
+
```typescript
|
| 535 |
+
class BadComponent extends Container {
|
| 536 |
+
private content: Text;
|
| 537 |
+
|
| 538 |
+
constructor(message: string, theme: Theme) {
|
| 539 |
+
super();
|
| 540 |
+
// Pre-baked theme colors stored in Text component
|
| 541 |
+
this.content = new Text(theme.fg("accent", message), 1, 0);
|
| 542 |
+
this.addChild(this.content);
|
| 543 |
+
}
|
| 544 |
+
// No invalidate override - parent's invalidate only clears
|
| 545 |
+
// child render caches, not the pre-baked content
|
| 546 |
+
}
|
| 547 |
+
```
|
| 548 |
+
|
| 549 |
+
### The Solution
|
| 550 |
+
|
| 551 |
+
Components that build content with theme colors must rebuild that content when `invalidate()` is called:
|
| 552 |
+
|
| 553 |
+
```typescript
|
| 554 |
+
class GoodComponent extends Container {
|
| 555 |
+
private message: string;
|
| 556 |
+
private content: Text;
|
| 557 |
+
|
| 558 |
+
constructor(message: string) {
|
| 559 |
+
super();
|
| 560 |
+
this.message = message;
|
| 561 |
+
this.content = new Text("", 1, 0);
|
| 562 |
+
this.addChild(this.content);
|
| 563 |
+
this.updateDisplay();
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
private updateDisplay(): void {
|
| 567 |
+
// Rebuild content with current theme
|
| 568 |
+
this.content.setText(theme.fg("accent", this.message));
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
override invalidate(): void {
|
| 572 |
+
super.invalidate(); // Clear child caches
|
| 573 |
+
this.updateDisplay(); // Rebuild with new theme
|
| 574 |
+
}
|
| 575 |
+
}
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
### Pattern: Rebuild on Invalidate
|
| 579 |
+
|
| 580 |
+
For components with complex content:
|
| 581 |
+
|
| 582 |
+
```typescript
|
| 583 |
+
class ComplexComponent extends Container {
|
| 584 |
+
private data: SomeData;
|
| 585 |
+
|
| 586 |
+
constructor(data: SomeData) {
|
| 587 |
+
super();
|
| 588 |
+
this.data = data;
|
| 589 |
+
this.rebuild();
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
private rebuild(): void {
|
| 593 |
+
this.clear(); // Remove all children
|
| 594 |
+
|
| 595 |
+
// Build UI with current theme
|
| 596 |
+
this.addChild(new Text(theme.fg("accent", theme.bold("Title")), 1, 0));
|
| 597 |
+
this.addChild(new Spacer(1));
|
| 598 |
+
|
| 599 |
+
for (const item of this.data.items) {
|
| 600 |
+
const color = item.active ? "success" : "muted";
|
| 601 |
+
this.addChild(new Text(theme.fg(color, item.label), 1, 0));
|
| 602 |
+
}
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
override invalidate(): void {
|
| 606 |
+
super.invalidate();
|
| 607 |
+
this.rebuild();
|
| 608 |
+
}
|
| 609 |
+
}
|
| 610 |
+
```
|
| 611 |
+
|
| 612 |
+
### When This Matters
|
| 613 |
+
|
| 614 |
+
This pattern is needed when:
|
| 615 |
+
|
| 616 |
+
1. **Pre-baking theme colors** - Using `theme.fg()` or `theme.bg()` to create styled strings stored in child components
|
| 617 |
+
2. **Syntax highlighting** - Using `highlightCode()` which applies theme-based syntax colors
|
| 618 |
+
3. **Complex layouts** - Building child component trees that embed theme colors
|
| 619 |
+
|
| 620 |
+
This pattern is NOT needed when:
|
| 621 |
+
|
| 622 |
+
1. **Using theme callbacks** - Passing functions like `(text) => theme.fg("accent", text)` that are called during render
|
| 623 |
+
2. **Simple containers** - Just grouping other components without adding themed content
|
| 624 |
+
3. **Stateless render** - Computing themed output fresh in every `render()` call (no caching)
|
| 625 |
+
|
| 626 |
+
## Common Patterns
|
| 627 |
+
|
| 628 |
+
These patterns cover the most common UI needs in extensions. **Copy these patterns instead of building from scratch.**
|
| 629 |
+
|
| 630 |
+
### Pattern 1: Selection Dialog (SelectList)
|
| 631 |
+
|
| 632 |
+
For letting users pick from a list of options. Use `SelectList` from `@earendil-works/pi-tui` with `DynamicBorder` for framing.
|
| 633 |
+
|
| 634 |
+
```typescript
|
| 635 |
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
| 636 |
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
| 637 |
+
import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
| 638 |
+
|
| 639 |
+
pi.registerCommand("pick", {
|
| 640 |
+
handler: async (_args, ctx) => {
|
| 641 |
+
const items: SelectItem[] = [
|
| 642 |
+
{ value: "opt1", label: "Option 1", description: "First option" },
|
| 643 |
+
{ value: "opt2", label: "Option 2", description: "Second option" },
|
| 644 |
+
{ value: "opt3", label: "Option 3" }, // description is optional
|
| 645 |
+
];
|
| 646 |
+
|
| 647 |
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
| 648 |
+
const container = new Container();
|
| 649 |
+
|
| 650 |
+
// Top border
|
| 651 |
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
| 652 |
+
|
| 653 |
+
// Title
|
| 654 |
+
container.addChild(new Text(theme.fg("accent", theme.bold("Pick an Option")), 1, 0));
|
| 655 |
+
|
| 656 |
+
// SelectList with theme
|
| 657 |
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
| 658 |
+
selectedPrefix: (t) => theme.fg("accent", t),
|
| 659 |
+
selectedText: (t) => theme.fg("accent", t),
|
| 660 |
+
description: (t) => theme.fg("muted", t),
|
| 661 |
+
scrollInfo: (t) => theme.fg("dim", t),
|
| 662 |
+
noMatch: (t) => theme.fg("warning", t),
|
| 663 |
+
});
|
| 664 |
+
selectList.onSelect = (item) => done(item.value);
|
| 665 |
+
selectList.onCancel = () => done(null);
|
| 666 |
+
container.addChild(selectList);
|
| 667 |
+
|
| 668 |
+
// Help text
|
| 669 |
+
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
|
| 670 |
+
|
| 671 |
+
// Bottom border
|
| 672 |
+
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
| 673 |
+
|
| 674 |
+
return {
|
| 675 |
+
render: (w) => container.render(w),
|
| 676 |
+
invalidate: () => container.invalidate(),
|
| 677 |
+
handleInput: (data) => { selectList.handleInput(data); tui.requestRender(); },
|
| 678 |
+
};
|
| 679 |
+
});
|
| 680 |
+
|
| 681 |
+
if (result) {
|
| 682 |
+
ctx.ui.notify(`Selected: ${result}`, "info");
|
| 683 |
+
}
|
| 684 |
+
},
|
| 685 |
+
});
|
| 686 |
+
```
|
| 687 |
+
|
| 688 |
+
**Examples:** [preset.ts](../examples/extensions/preset.ts), [tools.ts](../examples/extensions/tools.ts)
|
| 689 |
+
|
| 690 |
+
### Pattern 2: Async Operation with Cancel (BorderedLoader)
|
| 691 |
+
|
| 692 |
+
For operations that take time and should be cancellable. `BorderedLoader` shows a spinner and handles escape to cancel.
|
| 693 |
+
|
| 694 |
+
```typescript
|
| 695 |
+
import { BorderedLoader } from "@earendil-works/pi-coding-agent";
|
| 696 |
+
|
| 697 |
+
pi.registerCommand("fetch", {
|
| 698 |
+
handler: async (_args, ctx) => {
|
| 699 |
+
const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
|
| 700 |
+
const loader = new BorderedLoader(tui, theme, "Fetching data...");
|
| 701 |
+
loader.onAbort = () => done(null);
|
| 702 |
+
|
| 703 |
+
// Do async work
|
| 704 |
+
fetchData(loader.signal)
|
| 705 |
+
.then((data) => done(data))
|
| 706 |
+
.catch(() => done(null));
|
| 707 |
+
|
| 708 |
+
return loader;
|
| 709 |
+
});
|
| 710 |
+
|
| 711 |
+
if (result === null) {
|
| 712 |
+
ctx.ui.notify("Cancelled", "info");
|
| 713 |
+
} else {
|
| 714 |
+
ctx.ui.setEditorText(result);
|
| 715 |
+
}
|
| 716 |
+
},
|
| 717 |
+
});
|
| 718 |
+
```
|
| 719 |
+
|
| 720 |
+
**Examples:** [qna.ts](../examples/extensions/qna.ts), [handoff.ts](../examples/extensions/handoff.ts)
|
| 721 |
+
|
| 722 |
+
### Pattern 3: Settings/Toggles (SettingsList)
|
| 723 |
+
|
| 724 |
+
For toggling multiple settings. Use `SettingsList` from `@earendil-works/pi-tui` with `getSettingsListTheme()`.
|
| 725 |
+
|
| 726 |
+
```typescript
|
| 727 |
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
| 728 |
+
import { Container, type SettingItem, SettingsList, Text } from "@earendil-works/pi-tui";
|
| 729 |
+
|
| 730 |
+
pi.registerCommand("settings", {
|
| 731 |
+
handler: async (_args, ctx) => {
|
| 732 |
+
const items: SettingItem[] = [
|
| 733 |
+
{ id: "verbose", label: "Verbose mode", currentValue: "off", values: ["on", "off"] },
|
| 734 |
+
{ id: "color", label: "Color output", currentValue: "on", values: ["on", "off"] },
|
| 735 |
+
];
|
| 736 |
+
|
| 737 |
+
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
| 738 |
+
const container = new Container();
|
| 739 |
+
container.addChild(new Text(theme.fg("accent", theme.bold("Settings")), 1, 1));
|
| 740 |
+
|
| 741 |
+
const settingsList = new SettingsList(
|
| 742 |
+
items,
|
| 743 |
+
Math.min(items.length + 2, 15),
|
| 744 |
+
getSettingsListTheme(),
|
| 745 |
+
(id, newValue) => {
|
| 746 |
+
// Handle value change
|
| 747 |
+
ctx.ui.notify(`${id} = ${newValue}`, "info");
|
| 748 |
+
},
|
| 749 |
+
() => done(undefined), // On close
|
| 750 |
+
{ enableSearch: true }, // Optional: enable fuzzy search by label
|
| 751 |
+
);
|
| 752 |
+
container.addChild(settingsList);
|
| 753 |
+
|
| 754 |
+
return {
|
| 755 |
+
render: (w) => container.render(w),
|
| 756 |
+
invalidate: () => container.invalidate(),
|
| 757 |
+
handleInput: (data) => settingsList.handleInput?.(data),
|
| 758 |
+
};
|
| 759 |
+
});
|
| 760 |
+
},
|
| 761 |
+
});
|
| 762 |
+
```
|
| 763 |
+
|
| 764 |
+
**Examples:** [tools.ts](../examples/extensions/tools.ts)
|
| 765 |
+
|
| 766 |
+
### Pattern 4: Persistent Status Indicator
|
| 767 |
+
|
| 768 |
+
Show status in the footer that persists across renders. Good for mode indicators.
|
| 769 |
+
|
| 770 |
+
```typescript
|
| 771 |
+
// Set status (shown in footer)
|
| 772 |
+
ctx.ui.setStatus("my-ext", ctx.ui.theme.fg("accent", "● active"));
|
| 773 |
+
|
| 774 |
+
// Clear status
|
| 775 |
+
ctx.ui.setStatus("my-ext", undefined);
|
| 776 |
+
```
|
| 777 |
+
|
| 778 |
+
**Examples:** [status-line.ts](../examples/extensions/status-line.ts), [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts), [preset.ts](../examples/extensions/preset.ts)
|
| 779 |
+
|
| 780 |
+
### Pattern 4b: Working Indicator Customization
|
| 781 |
+
|
| 782 |
+
Customize the inline working indicator shown while pi is streaming a response.
|
| 783 |
+
|
| 784 |
+
```typescript
|
| 785 |
+
// Static indicator
|
| 786 |
+
ctx.ui.setWorkingIndicator({ frames: [ctx.ui.theme.fg("accent", "●")] });
|
| 787 |
+
|
| 788 |
+
// Custom animated indicator
|
| 789 |
+
ctx.ui.setWorkingIndicator({
|
| 790 |
+
frames: [
|
| 791 |
+
ctx.ui.theme.fg("dim", "·"),
|
| 792 |
+
ctx.ui.theme.fg("muted", "•"),
|
| 793 |
+
ctx.ui.theme.fg("accent", "●"),
|
| 794 |
+
ctx.ui.theme.fg("muted", "•"),
|
| 795 |
+
],
|
| 796 |
+
intervalMs: 120,
|
| 797 |
+
});
|
| 798 |
+
|
| 799 |
+
// Hide the indicator entirely
|
| 800 |
+
ctx.ui.setWorkingIndicator({ frames: [] });
|
| 801 |
+
|
| 802 |
+
// Restore pi's default spinner
|
| 803 |
+
ctx.ui.setWorkingIndicator();
|
| 804 |
+
```
|
| 805 |
+
|
| 806 |
+
This only affects the normal streaming working indicator. Compaction and retry loaders keep their built-in styling. Custom frames are rendered verbatim, so extensions must add their own colors when needed.
|
| 807 |
+
|
| 808 |
+
**Examples:** [working-indicator.ts](../examples/extensions/working-indicator.ts)
|
| 809 |
+
|
| 810 |
+
### Pattern 5: Widgets Above/Below Editor
|
| 811 |
+
|
| 812 |
+
Show persistent content above or below the input editor. Good for todo lists, progress.
|
| 813 |
+
|
| 814 |
+
```typescript
|
| 815 |
+
// Simple string array (above editor by default)
|
| 816 |
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"]);
|
| 817 |
+
|
| 818 |
+
// Render below the editor
|
| 819 |
+
ctx.ui.setWidget("my-widget", ["Line 1", "Line 2"], { placement: "belowEditor" });
|
| 820 |
+
|
| 821 |
+
// Or with theme
|
| 822 |
+
ctx.ui.setWidget("my-widget", (_tui, theme) => {
|
| 823 |
+
const lines = items.map((item, i) =>
|
| 824 |
+
item.done
|
| 825 |
+
? theme.fg("success", "✓ ") + theme.fg("muted", item.text)
|
| 826 |
+
: theme.fg("dim", "○ ") + item.text
|
| 827 |
+
);
|
| 828 |
+
return {
|
| 829 |
+
render: () => lines,
|
| 830 |
+
invalidate: () => {},
|
| 831 |
+
};
|
| 832 |
+
});
|
| 833 |
+
|
| 834 |
+
// Clear
|
| 835 |
+
ctx.ui.setWidget("my-widget", undefined);
|
| 836 |
+
```
|
| 837 |
+
|
| 838 |
+
**Examples:** [plan-mode/index.ts](../examples/extensions/plan-mode/index.ts)
|
| 839 |
+
|
| 840 |
+
### Pattern 6: Custom Footer
|
| 841 |
+
|
| 842 |
+
Replace the footer. `footerData` exposes data not otherwise accessible to extensions.
|
| 843 |
+
|
| 844 |
+
```typescript
|
| 845 |
+
ctx.ui.setFooter((tui, theme, footerData) => ({
|
| 846 |
+
invalidate() {},
|
| 847 |
+
render(width: number): string[] {
|
| 848 |
+
// footerData.getGitBranch(): string | null
|
| 849 |
+
// footerData.getExtensionStatuses(): ReadonlyMap<string, string>
|
| 850 |
+
return [`${ctx.model?.id} (${footerData.getGitBranch() || "no git"})`];
|
| 851 |
+
},
|
| 852 |
+
dispose: footerData.onBranchChange(() => tui.requestRender()), // reactive
|
| 853 |
+
}));
|
| 854 |
+
|
| 855 |
+
ctx.ui.setFooter(undefined); // restore default
|
| 856 |
+
```
|
| 857 |
+
|
| 858 |
+
Token stats available via `ctx.sessionManager.getBranch()` and `ctx.model`.
|
| 859 |
+
|
| 860 |
+
**Examples:** [custom-footer.ts](../examples/extensions/custom-footer.ts)
|
| 861 |
+
|
| 862 |
+
### Pattern 7: Custom Editor (vim mode, etc.)
|
| 863 |
+
|
| 864 |
+
Replace the main input editor with a custom implementation. Useful for modal editing (vim), different keybindings (emacs), or specialized input handling.
|
| 865 |
+
|
| 866 |
+
```typescript
|
| 867 |
+
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
| 868 |
+
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
| 869 |
+
|
| 870 |
+
type Mode = "normal" | "insert";
|
| 871 |
+
|
| 872 |
+
class VimEditor extends CustomEditor {
|
| 873 |
+
private mode: Mode = "insert";
|
| 874 |
+
|
| 875 |
+
handleInput(data: string): void {
|
| 876 |
+
// Escape: switch to normal mode, or pass through for app handling
|
| 877 |
+
if (matchesKey(data, "escape")) {
|
| 878 |
+
if (this.mode === "insert") {
|
| 879 |
+
this.mode = "normal";
|
| 880 |
+
return;
|
| 881 |
+
}
|
| 882 |
+
// In normal mode, escape aborts agent (handled by CustomEditor)
|
| 883 |
+
super.handleInput(data);
|
| 884 |
+
return;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
// Insert mode: pass everything to CustomEditor
|
| 888 |
+
if (this.mode === "insert") {
|
| 889 |
+
super.handleInput(data);
|
| 890 |
+
return;
|
| 891 |
+
}
|
| 892 |
+
|
| 893 |
+
// Normal mode: vim-style navigation
|
| 894 |
+
switch (data) {
|
| 895 |
+
case "i": this.mode = "insert"; return;
|
| 896 |
+
case "h": super.handleInput("\x1b[D"); return; // Left
|
| 897 |
+
case "j": super.handleInput("\x1b[B"); return; // Down
|
| 898 |
+
case "k": super.handleInput("\x1b[A"); return; // Up
|
| 899 |
+
case "l": super.handleInput("\x1b[C"); return; // Right
|
| 900 |
+
}
|
| 901 |
+
// Pass unhandled keys to super (ctrl+c, etc.), but filter printable chars
|
| 902 |
+
if (data.length === 1 && data.charCodeAt(0) >= 32) return;
|
| 903 |
+
super.handleInput(data);
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
render(width: number): string[] {
|
| 907 |
+
const lines = super.render(width);
|
| 908 |
+
// Add mode indicator to bottom border (use truncateToWidth for ANSI-safe truncation)
|
| 909 |
+
if (lines.length > 0) {
|
| 910 |
+
const label = this.mode === "normal" ? " NORMAL " : " INSERT ";
|
| 911 |
+
const lastLine = lines[lines.length - 1]!;
|
| 912 |
+
// Pass "" as ellipsis to avoid adding "..." when truncating
|
| 913 |
+
lines[lines.length - 1] = truncateToWidth(lastLine, width - label.length, "") + label;
|
| 914 |
+
}
|
| 915 |
+
return lines;
|
| 916 |
+
}
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
export default function (pi: ExtensionAPI) {
|
| 920 |
+
pi.on("session_start", (_event, ctx) => {
|
| 921 |
+
// Factory receives the TUI, theme, and keybindings from the app
|
| 922 |
+
ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
| 923 |
+
new VimEditor(tui, theme, keybindings)
|
| 924 |
+
);
|
| 925 |
+
});
|
| 926 |
+
}
|
| 927 |
+
```
|
| 928 |
+
|
| 929 |
+
**Key points:**
|
| 930 |
+
|
| 931 |
+
- **Extend `CustomEditor`** (not base `Editor`) to get app keybindings (escape to abort, ctrl+d to exit, model switching, etc.)
|
| 932 |
+
- **Call `super.handleInput(data)`** for keys you don't handle
|
| 933 |
+
- **Status spinners**: custom editors keep standalone status rows by default. Pass `{ embedWorkingStatus: true }` as the fourth `CustomEditor` constructor argument to embed working, compaction, branch summarization, and retry spinners in the editor border instead.
|
| 934 |
+
- **Factory pattern**: `setEditorComponent` receives a factory function that gets `tui`, `theme`, and `keybindings`
|
| 935 |
+
- **Pass `undefined`** to restore the default editor: `ctx.ui.setEditorComponent(undefined)`
|
| 936 |
+
|
| 937 |
+
**Examples:** [modal-editor.ts](../examples/extensions/modal-editor.ts)
|
| 938 |
+
|
| 939 |
+
## Key Rules
|
| 940 |
+
|
| 941 |
+
1. **Always use theme from callback** - Don't import theme directly. Use `theme` from the `ctx.ui.custom((tui, theme, keybindings, done) => ...)` callback.
|
| 942 |
+
|
| 943 |
+
2. **Always type DynamicBorder color param** - Write `(s: string) => theme.fg("accent", s)`, not `(s) => theme.fg("accent", s)`.
|
| 944 |
+
|
| 945 |
+
3. **Call tui.requestRender() after state changes** - In `handleInput`, call `tui.requestRender()` after updating state.
|
| 946 |
+
|
| 947 |
+
4. **Return the three-method object** - Custom components need `{ render, invalidate, handleInput }`.
|
| 948 |
+
|
| 949 |
+
5. **Use existing components** - `SelectList`, `SettingsList`, `BorderedLoader` cover 90% of cases. Don't rebuild them.
|
| 950 |
+
|
| 951 |
+
## Examples
|
| 952 |
+
|
| 953 |
+
- **Selection UI**: [examples/extensions/preset.ts](../examples/extensions/preset.ts) - SelectList with DynamicBorder framing
|
| 954 |
+
- **Async with cancel**: [examples/extensions/qna.ts](../examples/extensions/qna.ts) - BorderedLoader for LLM calls
|
| 955 |
+
- **Settings toggles**: [examples/extensions/tools.ts](../examples/extensions/tools.ts) - SettingsList for tool enable/disable
|
| 956 |
+
- **Status indicators**: [examples/extensions/plan-mode/index.ts](../examples/extensions/plan-mode/index.ts) - setStatus and setWidget
|
| 957 |
+
- **Working indicator**: [examples/extensions/working-indicator.ts](../examples/extensions/working-indicator.ts) - setWorkingIndicator
|
| 958 |
+
- **Custom footer**: [examples/extensions/custom-footer.ts](../examples/extensions/custom-footer.ts) - setFooter with stats
|
| 959 |
+
- **Custom editor**: [examples/extensions/modal-editor.ts](../examples/extensions/modal-editor.ts) - Vim-like modal editing
|
| 960 |
+
- **Snake game**: [examples/extensions/snake.ts](../examples/extensions/snake.ts) - Full game with keyboard input, game loop
|
| 961 |
+
- **Custom tool rendering**: [examples/extensions/todo.ts](../examples/extensions/todo.ts) - renderCall and renderResult
|
packages/coding-agent/install-lock/package-lock.json
ADDED
|
@@ -0,0 +1,1917 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@earendil-works/pi-coding-agent-install",
|
| 3 |
+
"version": "0.85.1",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "@earendil-works/pi-coding-agent-install",
|
| 9 |
+
"version": "0.85.1",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"@earendil-works/pi-coding-agent": "0.85.1"
|
| 12 |
+
},
|
| 13 |
+
"engines": {
|
| 14 |
+
"node": ">=22.19.0"
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"node_modules/@anthropic-ai/sdk": {
|
| 18 |
+
"version": "0.124.0",
|
| 19 |
+
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.124.0.tgz",
|
| 20 |
+
"integrity": "sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA==",
|
| 21 |
+
"license": "MIT",
|
| 22 |
+
"dependencies": {
|
| 23 |
+
"json-schema-to-ts": "^3.1.1",
|
| 24 |
+
"standardwebhooks": "^1.0.0"
|
| 25 |
+
},
|
| 26 |
+
"peerDependencies": {
|
| 27 |
+
"zod": "^3.25.0 || ^4.0.0"
|
| 28 |
+
},
|
| 29 |
+
"peerDependenciesMeta": {
|
| 30 |
+
"zod": {
|
| 31 |
+
"optional": true
|
| 32 |
+
}
|
| 33 |
+
},
|
| 34 |
+
"bin": {
|
| 35 |
+
"anthropic-ai-sdk": "bin/cli"
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
"node_modules/@aws-sdk/client-bedrock-runtime": {
|
| 39 |
+
"version": "3.1127.0",
|
| 40 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1127.0.tgz",
|
| 41 |
+
"integrity": "sha512-IDl/lrPb90aH+pZFHGNDmgH9nAUQj5PlZH1sJ3w7RikctyjHSnY3oNjZhrLoaBoQn/rNK0zsP6OHEqEhj2tdLA==",
|
| 42 |
+
"license": "Apache-2.0",
|
| 43 |
+
"dependencies": {
|
| 44 |
+
"@aws-sdk/core": "^3.977.9",
|
| 45 |
+
"@aws-sdk/credential-provider-node": "^3.972.82",
|
| 46 |
+
"@aws-sdk/eventstream-handler-node": "^3.972.34",
|
| 47 |
+
"@aws-sdk/middleware-eventstream": "^3.972.29",
|
| 48 |
+
"@aws-sdk/middleware-websocket": "^3.972.52",
|
| 49 |
+
"@aws-sdk/token-providers": "3.1127.0",
|
| 50 |
+
"@aws-sdk/types": "^3.974.5",
|
| 51 |
+
"@smithy/core": "^3.33.3",
|
| 52 |
+
"@smithy/fetch-http-handler": "^5.7.2",
|
| 53 |
+
"@smithy/node-http-handler": "^4.11.3",
|
| 54 |
+
"@smithy/types": "^4.17.2",
|
| 55 |
+
"tslib": "^2.6.2"
|
| 56 |
+
},
|
| 57 |
+
"engines": {
|
| 58 |
+
"node": ">=20.0.0"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"node_modules/@aws-sdk/core": {
|
| 62 |
+
"version": "3.977.9",
|
| 63 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.9.tgz",
|
| 64 |
+
"integrity": "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA==",
|
| 65 |
+
"license": "Apache-2.0",
|
| 66 |
+
"dependencies": {
|
| 67 |
+
"@aws-sdk/types": "^3.974.5",
|
| 68 |
+
"@aws-sdk/xml-builder": "^3.972.40",
|
| 69 |
+
"@aws/lambda-invoke-store": "^0.3.0",
|
| 70 |
+
"@smithy/core": "^3.33.3",
|
| 71 |
+
"@smithy/signature-v4": "^5.6.12",
|
| 72 |
+
"@smithy/types": "^4.17.2",
|
| 73 |
+
"bowser": "^2.11.0",
|
| 74 |
+
"tslib": "^2.6.2"
|
| 75 |
+
},
|
| 76 |
+
"engines": {
|
| 77 |
+
"node": ">=20.0.0"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"node_modules/@aws-sdk/credential-provider-env": {
|
| 81 |
+
"version": "3.972.70",
|
| 82 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.70.tgz",
|
| 83 |
+
"integrity": "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw==",
|
| 84 |
+
"license": "Apache-2.0",
|
| 85 |
+
"dependencies": {
|
| 86 |
+
"@aws-sdk/core": "^3.977.9",
|
| 87 |
+
"@aws-sdk/types": "^3.974.5",
|
| 88 |
+
"@smithy/core": "^3.33.3",
|
| 89 |
+
"@smithy/types": "^4.17.2",
|
| 90 |
+
"tslib": "^2.6.2"
|
| 91 |
+
},
|
| 92 |
+
"engines": {
|
| 93 |
+
"node": ">=20.0.0"
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
"node_modules/@aws-sdk/credential-provider-http": {
|
| 97 |
+
"version": "3.972.72",
|
| 98 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.72.tgz",
|
| 99 |
+
"integrity": "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w==",
|
| 100 |
+
"license": "Apache-2.0",
|
| 101 |
+
"dependencies": {
|
| 102 |
+
"@aws-sdk/core": "^3.977.9",
|
| 103 |
+
"@aws-sdk/types": "^3.974.5",
|
| 104 |
+
"@smithy/core": "^3.33.3",
|
| 105 |
+
"@smithy/fetch-http-handler": "^5.7.2",
|
| 106 |
+
"@smithy/node-http-handler": "^4.11.3",
|
| 107 |
+
"@smithy/types": "^4.17.2",
|
| 108 |
+
"tslib": "^2.6.2"
|
| 109 |
+
},
|
| 110 |
+
"engines": {
|
| 111 |
+
"node": ">=20.0.0"
|
| 112 |
+
}
|
| 113 |
+
},
|
| 114 |
+
"node_modules/@aws-sdk/credential-provider-ini": {
|
| 115 |
+
"version": "3.973.15",
|
| 116 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.15.tgz",
|
| 117 |
+
"integrity": "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg==",
|
| 118 |
+
"license": "Apache-2.0",
|
| 119 |
+
"dependencies": {
|
| 120 |
+
"@aws-sdk/core": "^3.977.9",
|
| 121 |
+
"@aws-sdk/credential-provider-env": "^3.972.70",
|
| 122 |
+
"@aws-sdk/credential-provider-http": "^3.972.72",
|
| 123 |
+
"@aws-sdk/credential-provider-login": "^3.972.77",
|
| 124 |
+
"@aws-sdk/credential-provider-process": "^3.972.70",
|
| 125 |
+
"@aws-sdk/credential-provider-sso": "^3.973.14",
|
| 126 |
+
"@aws-sdk/credential-provider-web-identity": "^3.972.76",
|
| 127 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 128 |
+
"@aws-sdk/types": "^3.974.5",
|
| 129 |
+
"@smithy/core": "^3.33.3",
|
| 130 |
+
"@smithy/credential-provider-imds": "^4.4.16",
|
| 131 |
+
"@smithy/types": "^4.17.2",
|
| 132 |
+
"tslib": "^2.6.2"
|
| 133 |
+
},
|
| 134 |
+
"engines": {
|
| 135 |
+
"node": ">=20.0.0"
|
| 136 |
+
}
|
| 137 |
+
},
|
| 138 |
+
"node_modules/@aws-sdk/credential-provider-login": {
|
| 139 |
+
"version": "3.972.77",
|
| 140 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.77.tgz",
|
| 141 |
+
"integrity": "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ==",
|
| 142 |
+
"license": "Apache-2.0",
|
| 143 |
+
"dependencies": {
|
| 144 |
+
"@aws-sdk/core": "^3.977.9",
|
| 145 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 146 |
+
"@aws-sdk/types": "^3.974.5",
|
| 147 |
+
"@smithy/core": "^3.33.3",
|
| 148 |
+
"@smithy/types": "^4.17.2",
|
| 149 |
+
"tslib": "^2.6.2"
|
| 150 |
+
},
|
| 151 |
+
"engines": {
|
| 152 |
+
"node": ">=20.0.0"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"node_modules/@aws-sdk/credential-provider-node": {
|
| 156 |
+
"version": "3.972.82",
|
| 157 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.82.tgz",
|
| 158 |
+
"integrity": "sha512-znDkEOGXB8W3kG1LJUKP3foBZY/9qLM0eil/DxWXSp37XsdsRLQHE/d/OaCGGVgKpA6znR38h/+INk8do1FjiA==",
|
| 159 |
+
"license": "Apache-2.0",
|
| 160 |
+
"dependencies": {
|
| 161 |
+
"@aws-sdk/credential-provider-env": "^3.972.70",
|
| 162 |
+
"@aws-sdk/credential-provider-http": "^3.972.72",
|
| 163 |
+
"@aws-sdk/credential-provider-ini": "^3.973.15",
|
| 164 |
+
"@aws-sdk/credential-provider-process": "^3.972.70",
|
| 165 |
+
"@aws-sdk/credential-provider-sso": "^3.973.14",
|
| 166 |
+
"@aws-sdk/credential-provider-web-identity": "^3.972.76",
|
| 167 |
+
"@aws-sdk/types": "^3.974.5",
|
| 168 |
+
"@smithy/core": "^3.33.3",
|
| 169 |
+
"@smithy/credential-provider-imds": "^4.4.16",
|
| 170 |
+
"@smithy/types": "^4.17.2",
|
| 171 |
+
"tslib": "^2.6.2"
|
| 172 |
+
},
|
| 173 |
+
"engines": {
|
| 174 |
+
"node": ">=20.0.0"
|
| 175 |
+
}
|
| 176 |
+
},
|
| 177 |
+
"node_modules/@aws-sdk/credential-provider-process": {
|
| 178 |
+
"version": "3.972.70",
|
| 179 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.70.tgz",
|
| 180 |
+
"integrity": "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q==",
|
| 181 |
+
"license": "Apache-2.0",
|
| 182 |
+
"dependencies": {
|
| 183 |
+
"@aws-sdk/core": "^3.977.9",
|
| 184 |
+
"@aws-sdk/types": "^3.974.5",
|
| 185 |
+
"@smithy/core": "^3.33.3",
|
| 186 |
+
"@smithy/types": "^4.17.2",
|
| 187 |
+
"tslib": "^2.6.2"
|
| 188 |
+
},
|
| 189 |
+
"engines": {
|
| 190 |
+
"node": ">=20.0.0"
|
| 191 |
+
}
|
| 192 |
+
},
|
| 193 |
+
"node_modules/@aws-sdk/credential-provider-sso": {
|
| 194 |
+
"version": "3.973.14",
|
| 195 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.14.tgz",
|
| 196 |
+
"integrity": "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA==",
|
| 197 |
+
"license": "Apache-2.0",
|
| 198 |
+
"dependencies": {
|
| 199 |
+
"@aws-sdk/core": "^3.977.9",
|
| 200 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 201 |
+
"@aws-sdk/token-providers": "3.1116.0",
|
| 202 |
+
"@aws-sdk/types": "^3.974.5",
|
| 203 |
+
"@smithy/core": "^3.33.3",
|
| 204 |
+
"@smithy/types": "^4.17.2",
|
| 205 |
+
"tslib": "^2.6.2"
|
| 206 |
+
},
|
| 207 |
+
"engines": {
|
| 208 |
+
"node": ">=20.0.0"
|
| 209 |
+
}
|
| 210 |
+
},
|
| 211 |
+
"node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": {
|
| 212 |
+
"version": "3.1116.0",
|
| 213 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1116.0.tgz",
|
| 214 |
+
"integrity": "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q==",
|
| 215 |
+
"license": "Apache-2.0",
|
| 216 |
+
"dependencies": {
|
| 217 |
+
"@aws-sdk/core": "^3.977.9",
|
| 218 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 219 |
+
"@aws-sdk/types": "^3.974.5",
|
| 220 |
+
"@smithy/core": "^3.33.3",
|
| 221 |
+
"@smithy/types": "^4.17.2",
|
| 222 |
+
"tslib": "^2.6.2"
|
| 223 |
+
},
|
| 224 |
+
"engines": {
|
| 225 |
+
"node": ">=20.0.0"
|
| 226 |
+
}
|
| 227 |
+
},
|
| 228 |
+
"node_modules/@aws-sdk/credential-provider-web-identity": {
|
| 229 |
+
"version": "3.972.76",
|
| 230 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.76.tgz",
|
| 231 |
+
"integrity": "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA==",
|
| 232 |
+
"license": "Apache-2.0",
|
| 233 |
+
"dependencies": {
|
| 234 |
+
"@aws-sdk/core": "^3.977.9",
|
| 235 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 236 |
+
"@aws-sdk/types": "^3.974.5",
|
| 237 |
+
"@smithy/core": "^3.33.3",
|
| 238 |
+
"@smithy/types": "^4.17.2",
|
| 239 |
+
"tslib": "^2.6.2"
|
| 240 |
+
},
|
| 241 |
+
"engines": {
|
| 242 |
+
"node": ">=20.0.0"
|
| 243 |
+
}
|
| 244 |
+
},
|
| 245 |
+
"node_modules/@aws-sdk/eventstream-handler-node": {
|
| 246 |
+
"version": "3.972.34",
|
| 247 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.34.tgz",
|
| 248 |
+
"integrity": "sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw==",
|
| 249 |
+
"license": "Apache-2.0",
|
| 250 |
+
"dependencies": {
|
| 251 |
+
"@aws-sdk/types": "^3.974.5",
|
| 252 |
+
"@smithy/core": "^3.33.3",
|
| 253 |
+
"@smithy/types": "^4.17.2",
|
| 254 |
+
"tslib": "^2.6.2"
|
| 255 |
+
},
|
| 256 |
+
"engines": {
|
| 257 |
+
"node": ">=20.0.0"
|
| 258 |
+
}
|
| 259 |
+
},
|
| 260 |
+
"node_modules/@aws-sdk/middleware-eventstream": {
|
| 261 |
+
"version": "3.972.29",
|
| 262 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.29.tgz",
|
| 263 |
+
"integrity": "sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ==",
|
| 264 |
+
"license": "Apache-2.0",
|
| 265 |
+
"dependencies": {
|
| 266 |
+
"@aws-sdk/types": "^3.974.5",
|
| 267 |
+
"@smithy/core": "^3.33.3",
|
| 268 |
+
"@smithy/types": "^4.17.2",
|
| 269 |
+
"tslib": "^2.6.2"
|
| 270 |
+
},
|
| 271 |
+
"engines": {
|
| 272 |
+
"node": ">=20.0.0"
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
"node_modules/@aws-sdk/middleware-websocket": {
|
| 276 |
+
"version": "3.972.52",
|
| 277 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.52.tgz",
|
| 278 |
+
"integrity": "sha512-vsPPM+nMbKJlUCFU+eoGZbdxdxDIAX9LbpjSXaR5Ufpmqgp8TdYQnoExhLu4T3umW/JIIPny1ydbhWidZZYokQ==",
|
| 279 |
+
"license": "Apache-2.0",
|
| 280 |
+
"dependencies": {
|
| 281 |
+
"@aws-sdk/core": "^3.977.9",
|
| 282 |
+
"@aws-sdk/types": "^3.974.5",
|
| 283 |
+
"@smithy/core": "^3.33.3",
|
| 284 |
+
"@smithy/fetch-http-handler": "^5.7.2",
|
| 285 |
+
"@smithy/signature-v4": "^5.6.12",
|
| 286 |
+
"@smithy/types": "^4.17.2",
|
| 287 |
+
"tslib": "^2.6.2"
|
| 288 |
+
},
|
| 289 |
+
"engines": {
|
| 290 |
+
"node": ">= 14.0.0"
|
| 291 |
+
}
|
| 292 |
+
},
|
| 293 |
+
"node_modules/@aws-sdk/nested-clients": {
|
| 294 |
+
"version": "3.997.44",
|
| 295 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.44.tgz",
|
| 296 |
+
"integrity": "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw==",
|
| 297 |
+
"license": "Apache-2.0",
|
| 298 |
+
"dependencies": {
|
| 299 |
+
"@aws-sdk/core": "^3.977.9",
|
| 300 |
+
"@aws-sdk/signature-v4-multi-region": "^3.996.46",
|
| 301 |
+
"@aws-sdk/types": "^3.974.5",
|
| 302 |
+
"@smithy/core": "^3.33.3",
|
| 303 |
+
"@smithy/fetch-http-handler": "^5.7.2",
|
| 304 |
+
"@smithy/node-http-handler": "^4.11.3",
|
| 305 |
+
"@smithy/types": "^4.17.2",
|
| 306 |
+
"tslib": "^2.6.2"
|
| 307 |
+
},
|
| 308 |
+
"engines": {
|
| 309 |
+
"node": ">=20.0.0"
|
| 310 |
+
}
|
| 311 |
+
},
|
| 312 |
+
"node_modules/@aws-sdk/signature-v4-multi-region": {
|
| 313 |
+
"version": "3.996.46",
|
| 314 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz",
|
| 315 |
+
"integrity": "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==",
|
| 316 |
+
"license": "Apache-2.0",
|
| 317 |
+
"dependencies": {
|
| 318 |
+
"@aws-sdk/types": "^3.974.5",
|
| 319 |
+
"@smithy/signature-v4": "^5.6.12",
|
| 320 |
+
"@smithy/types": "^4.17.2",
|
| 321 |
+
"tslib": "^2.6.2"
|
| 322 |
+
},
|
| 323 |
+
"engines": {
|
| 324 |
+
"node": ">=20.0.0"
|
| 325 |
+
}
|
| 326 |
+
},
|
| 327 |
+
"node_modules/@aws-sdk/token-providers": {
|
| 328 |
+
"version": "3.1127.0",
|
| 329 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1127.0.tgz",
|
| 330 |
+
"integrity": "sha512-Dv2TMWBshJ+tF6ahs2Sy5bh4Iabsd4GAQqVvE9XZmYmnoaVbpS2QKIKE/HRacc7bTtbjEEvP+laGzHvHlf1CiQ==",
|
| 331 |
+
"license": "Apache-2.0",
|
| 332 |
+
"dependencies": {
|
| 333 |
+
"@aws-sdk/core": "^3.977.9",
|
| 334 |
+
"@aws-sdk/nested-clients": "^3.997.44",
|
| 335 |
+
"@aws-sdk/types": "^3.974.5",
|
| 336 |
+
"@smithy/core": "^3.33.3",
|
| 337 |
+
"@smithy/types": "^4.17.2",
|
| 338 |
+
"tslib": "^2.6.2"
|
| 339 |
+
},
|
| 340 |
+
"engines": {
|
| 341 |
+
"node": ">=20.0.0"
|
| 342 |
+
}
|
| 343 |
+
},
|
| 344 |
+
"node_modules/@aws-sdk/types": {
|
| 345 |
+
"version": "3.974.5",
|
| 346 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.5.tgz",
|
| 347 |
+
"integrity": "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==",
|
| 348 |
+
"license": "Apache-2.0",
|
| 349 |
+
"dependencies": {
|
| 350 |
+
"@smithy/types": "^4.17.2",
|
| 351 |
+
"tslib": "^2.6.2"
|
| 352 |
+
},
|
| 353 |
+
"engines": {
|
| 354 |
+
"node": ">=20.0.0"
|
| 355 |
+
}
|
| 356 |
+
},
|
| 357 |
+
"node_modules/@aws-sdk/xml-builder": {
|
| 358 |
+
"version": "3.972.40",
|
| 359 |
+
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz",
|
| 360 |
+
"integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==",
|
| 361 |
+
"license": "Apache-2.0",
|
| 362 |
+
"dependencies": {
|
| 363 |
+
"@smithy/types": "^4.17.2",
|
| 364 |
+
"tslib": "^2.6.2"
|
| 365 |
+
},
|
| 366 |
+
"engines": {
|
| 367 |
+
"node": ">=20.0.0"
|
| 368 |
+
}
|
| 369 |
+
},
|
| 370 |
+
"node_modules/@aws/lambda-invoke-store": {
|
| 371 |
+
"version": "0.3.0",
|
| 372 |
+
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz",
|
| 373 |
+
"integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==",
|
| 374 |
+
"license": "Apache-2.0",
|
| 375 |
+
"engines": {
|
| 376 |
+
"node": ">=18.0.0"
|
| 377 |
+
}
|
| 378 |
+
},
|
| 379 |
+
"node_modules/@babel/runtime": {
|
| 380 |
+
"version": "7.29.2",
|
| 381 |
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
| 382 |
+
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
| 383 |
+
"license": "MIT",
|
| 384 |
+
"engines": {
|
| 385 |
+
"node": ">=6.9.0"
|
| 386 |
+
}
|
| 387 |
+
},
|
| 388 |
+
"node_modules/@earendil-works/chord": {
|
| 389 |
+
"version": "0.85.1",
|
| 390 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.85.1.tgz",
|
| 391 |
+
"license": "MIT",
|
| 392 |
+
"dependencies": {
|
| 393 |
+
"esbuild": "0.28.2"
|
| 394 |
+
},
|
| 395 |
+
"engines": {
|
| 396 |
+
"node": ">=22.19.0"
|
| 397 |
+
}
|
| 398 |
+
},
|
| 399 |
+
"node_modules/@earendil-works/pi-agent-core": {
|
| 400 |
+
"version": "0.85.1",
|
| 401 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.85.1.tgz",
|
| 402 |
+
"license": "MIT",
|
| 403 |
+
"dependencies": {
|
| 404 |
+
"@earendil-works/chord": "^0.85.1",
|
| 405 |
+
"@earendil-works/pi-ai": "^0.85.1",
|
| 406 |
+
"@earendil-works/pi-telemetry": "^0.85.1",
|
| 407 |
+
"diff": "8.0.4",
|
| 408 |
+
"ignore": "7.0.8",
|
| 409 |
+
"typebox": "1.3.27",
|
| 410 |
+
"yaml": "2.9.0"
|
| 411 |
+
},
|
| 412 |
+
"engines": {
|
| 413 |
+
"node": ">=22.19.0"
|
| 414 |
+
}
|
| 415 |
+
},
|
| 416 |
+
"node_modules/@earendil-works/pi-ai": {
|
| 417 |
+
"version": "0.85.1",
|
| 418 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.85.1.tgz",
|
| 419 |
+
"license": "MIT",
|
| 420 |
+
"dependencies": {
|
| 421 |
+
"@anthropic-ai/sdk": "0.124.0",
|
| 422 |
+
"@aws-sdk/client-bedrock-runtime": "3.1127.0",
|
| 423 |
+
"@earendil-works/pi-telemetry": "^0.85.1",
|
| 424 |
+
"@google/genai": "2.21.0",
|
| 425 |
+
"@smithy/node-http-handler": "4.12.1",
|
| 426 |
+
"http-proxy-agent": "9.1.0",
|
| 427 |
+
"https-proxy-agent": "9.1.0",
|
| 428 |
+
"openai": "6.40.0",
|
| 429 |
+
"partial-json": "0.1.7",
|
| 430 |
+
"typebox": "1.3.27"
|
| 431 |
+
},
|
| 432 |
+
"bin": {
|
| 433 |
+
"pi-ai": "dist/cli.js"
|
| 434 |
+
},
|
| 435 |
+
"engines": {
|
| 436 |
+
"node": ">=22.19.0"
|
| 437 |
+
}
|
| 438 |
+
},
|
| 439 |
+
"node_modules/@earendil-works/pi-ai/node_modules/agent-base": {
|
| 440 |
+
"version": "9.0.0",
|
| 441 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
| 442 |
+
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
| 443 |
+
"license": "MIT",
|
| 444 |
+
"engines": {
|
| 445 |
+
"node": ">= 20"
|
| 446 |
+
}
|
| 447 |
+
},
|
| 448 |
+
"node_modules/@earendil-works/pi-ai/node_modules/https-proxy-agent": {
|
| 449 |
+
"version": "9.1.0",
|
| 450 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz",
|
| 451 |
+
"integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==",
|
| 452 |
+
"license": "MIT",
|
| 453 |
+
"dependencies": {
|
| 454 |
+
"agent-base": "9.0.0",
|
| 455 |
+
"debug": "^4.3.4",
|
| 456 |
+
"proxy-agent-negotiate": "1.1.0"
|
| 457 |
+
},
|
| 458 |
+
"engines": {
|
| 459 |
+
"node": ">= 20"
|
| 460 |
+
}
|
| 461 |
+
},
|
| 462 |
+
"node_modules/@earendil-works/pi-coding-agent": {
|
| 463 |
+
"version": "0.85.1",
|
| 464 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz",
|
| 465 |
+
"license": "MIT",
|
| 466 |
+
"dependencies": {
|
| 467 |
+
"@earendil-works/chord": "^0.85.1",
|
| 468 |
+
"@earendil-works/pi-agent-core": "^0.85.1",
|
| 469 |
+
"@earendil-works/pi-ai": "^0.85.1",
|
| 470 |
+
"@earendil-works/pi-tui": "^0.85.1",
|
| 471 |
+
"@silvia-odwyer/photon-node": "0.3.4",
|
| 472 |
+
"chalk": "6.0.0",
|
| 473 |
+
"cross-spawn": "7.0.6",
|
| 474 |
+
"diff": "8.0.4",
|
| 475 |
+
"grok-mermaid": "0.2.3",
|
| 476 |
+
"highlight.js": "10.7.3",
|
| 477 |
+
"hosted-git-info": "9.0.3",
|
| 478 |
+
"ignore": "7.0.8",
|
| 479 |
+
"jiti": "2.7.0",
|
| 480 |
+
"minimatch": "10.2.6",
|
| 481 |
+
"proper-lockfile": "4.1.2",
|
| 482 |
+
"semver": "7.8.5",
|
| 483 |
+
"typebox": "1.3.27",
|
| 484 |
+
"undici": "8.10.2",
|
| 485 |
+
"yaml": "2.9.0"
|
| 486 |
+
},
|
| 487 |
+
"bin": {
|
| 488 |
+
"pi": "dist/bundle/cli.js"
|
| 489 |
+
},
|
| 490 |
+
"engines": {
|
| 491 |
+
"node": ">=22.19.0"
|
| 492 |
+
}
|
| 493 |
+
},
|
| 494 |
+
"node_modules/@earendil-works/pi-telemetry": {
|
| 495 |
+
"version": "0.85.1",
|
| 496 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.85.1.tgz",
|
| 497 |
+
"license": "MIT",
|
| 498 |
+
"engines": {
|
| 499 |
+
"node": ">=22.19.0"
|
| 500 |
+
}
|
| 501 |
+
},
|
| 502 |
+
"node_modules/@earendil-works/pi-tui": {
|
| 503 |
+
"version": "0.85.1",
|
| 504 |
+
"resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.85.1.tgz",
|
| 505 |
+
"license": "MIT",
|
| 506 |
+
"dependencies": {
|
| 507 |
+
"get-east-asian-width": "1.6.0",
|
| 508 |
+
"marked": "18.0.11"
|
| 509 |
+
},
|
| 510 |
+
"engines": {
|
| 511 |
+
"node": ">=22.19.0"
|
| 512 |
+
}
|
| 513 |
+
},
|
| 514 |
+
"node_modules/@esbuild/aix-ppc64": {
|
| 515 |
+
"version": "0.28.2",
|
| 516 |
+
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
|
| 517 |
+
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
|
| 518 |
+
"license": "MIT",
|
| 519 |
+
"engines": {
|
| 520 |
+
"node": ">=18"
|
| 521 |
+
},
|
| 522 |
+
"os": [
|
| 523 |
+
"aix"
|
| 524 |
+
],
|
| 525 |
+
"cpu": [
|
| 526 |
+
"ppc64"
|
| 527 |
+
],
|
| 528 |
+
"optional": true
|
| 529 |
+
},
|
| 530 |
+
"node_modules/@esbuild/android-arm": {
|
| 531 |
+
"version": "0.28.2",
|
| 532 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
|
| 533 |
+
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
|
| 534 |
+
"license": "MIT",
|
| 535 |
+
"engines": {
|
| 536 |
+
"node": ">=18"
|
| 537 |
+
},
|
| 538 |
+
"os": [
|
| 539 |
+
"android"
|
| 540 |
+
],
|
| 541 |
+
"cpu": [
|
| 542 |
+
"arm"
|
| 543 |
+
],
|
| 544 |
+
"optional": true
|
| 545 |
+
},
|
| 546 |
+
"node_modules/@esbuild/android-arm64": {
|
| 547 |
+
"version": "0.28.2",
|
| 548 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
|
| 549 |
+
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
|
| 550 |
+
"license": "MIT",
|
| 551 |
+
"engines": {
|
| 552 |
+
"node": ">=18"
|
| 553 |
+
},
|
| 554 |
+
"os": [
|
| 555 |
+
"android"
|
| 556 |
+
],
|
| 557 |
+
"cpu": [
|
| 558 |
+
"arm64"
|
| 559 |
+
],
|
| 560 |
+
"optional": true
|
| 561 |
+
},
|
| 562 |
+
"node_modules/@esbuild/android-x64": {
|
| 563 |
+
"version": "0.28.2",
|
| 564 |
+
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
|
| 565 |
+
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
|
| 566 |
+
"license": "MIT",
|
| 567 |
+
"engines": {
|
| 568 |
+
"node": ">=18"
|
| 569 |
+
},
|
| 570 |
+
"os": [
|
| 571 |
+
"android"
|
| 572 |
+
],
|
| 573 |
+
"cpu": [
|
| 574 |
+
"x64"
|
| 575 |
+
],
|
| 576 |
+
"optional": true
|
| 577 |
+
},
|
| 578 |
+
"node_modules/@esbuild/darwin-arm64": {
|
| 579 |
+
"version": "0.28.2",
|
| 580 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
|
| 581 |
+
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
|
| 582 |
+
"license": "MIT",
|
| 583 |
+
"engines": {
|
| 584 |
+
"node": ">=18"
|
| 585 |
+
},
|
| 586 |
+
"os": [
|
| 587 |
+
"darwin"
|
| 588 |
+
],
|
| 589 |
+
"cpu": [
|
| 590 |
+
"arm64"
|
| 591 |
+
],
|
| 592 |
+
"optional": true
|
| 593 |
+
},
|
| 594 |
+
"node_modules/@esbuild/darwin-x64": {
|
| 595 |
+
"version": "0.28.2",
|
| 596 |
+
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
|
| 597 |
+
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
|
| 598 |
+
"license": "MIT",
|
| 599 |
+
"engines": {
|
| 600 |
+
"node": ">=18"
|
| 601 |
+
},
|
| 602 |
+
"os": [
|
| 603 |
+
"darwin"
|
| 604 |
+
],
|
| 605 |
+
"cpu": [
|
| 606 |
+
"x64"
|
| 607 |
+
],
|
| 608 |
+
"optional": true
|
| 609 |
+
},
|
| 610 |
+
"node_modules/@esbuild/freebsd-arm64": {
|
| 611 |
+
"version": "0.28.2",
|
| 612 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
|
| 613 |
+
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
|
| 614 |
+
"license": "MIT",
|
| 615 |
+
"engines": {
|
| 616 |
+
"node": ">=18"
|
| 617 |
+
},
|
| 618 |
+
"os": [
|
| 619 |
+
"freebsd"
|
| 620 |
+
],
|
| 621 |
+
"cpu": [
|
| 622 |
+
"arm64"
|
| 623 |
+
],
|
| 624 |
+
"optional": true
|
| 625 |
+
},
|
| 626 |
+
"node_modules/@esbuild/freebsd-x64": {
|
| 627 |
+
"version": "0.28.2",
|
| 628 |
+
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
|
| 629 |
+
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
|
| 630 |
+
"license": "MIT",
|
| 631 |
+
"engines": {
|
| 632 |
+
"node": ">=18"
|
| 633 |
+
},
|
| 634 |
+
"os": [
|
| 635 |
+
"freebsd"
|
| 636 |
+
],
|
| 637 |
+
"cpu": [
|
| 638 |
+
"x64"
|
| 639 |
+
],
|
| 640 |
+
"optional": true
|
| 641 |
+
},
|
| 642 |
+
"node_modules/@esbuild/linux-arm": {
|
| 643 |
+
"version": "0.28.2",
|
| 644 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
|
| 645 |
+
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
|
| 646 |
+
"license": "MIT",
|
| 647 |
+
"engines": {
|
| 648 |
+
"node": ">=18"
|
| 649 |
+
},
|
| 650 |
+
"os": [
|
| 651 |
+
"linux"
|
| 652 |
+
],
|
| 653 |
+
"cpu": [
|
| 654 |
+
"arm"
|
| 655 |
+
],
|
| 656 |
+
"optional": true
|
| 657 |
+
},
|
| 658 |
+
"node_modules/@esbuild/linux-arm64": {
|
| 659 |
+
"version": "0.28.2",
|
| 660 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
|
| 661 |
+
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
|
| 662 |
+
"license": "MIT",
|
| 663 |
+
"engines": {
|
| 664 |
+
"node": ">=18"
|
| 665 |
+
},
|
| 666 |
+
"os": [
|
| 667 |
+
"linux"
|
| 668 |
+
],
|
| 669 |
+
"cpu": [
|
| 670 |
+
"arm64"
|
| 671 |
+
],
|
| 672 |
+
"optional": true
|
| 673 |
+
},
|
| 674 |
+
"node_modules/@esbuild/linux-ia32": {
|
| 675 |
+
"version": "0.28.2",
|
| 676 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
|
| 677 |
+
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
|
| 678 |
+
"license": "MIT",
|
| 679 |
+
"engines": {
|
| 680 |
+
"node": ">=18"
|
| 681 |
+
},
|
| 682 |
+
"os": [
|
| 683 |
+
"linux"
|
| 684 |
+
],
|
| 685 |
+
"cpu": [
|
| 686 |
+
"ia32"
|
| 687 |
+
],
|
| 688 |
+
"optional": true
|
| 689 |
+
},
|
| 690 |
+
"node_modules/@esbuild/linux-loong64": {
|
| 691 |
+
"version": "0.28.2",
|
| 692 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
|
| 693 |
+
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
|
| 694 |
+
"license": "MIT",
|
| 695 |
+
"engines": {
|
| 696 |
+
"node": ">=18"
|
| 697 |
+
},
|
| 698 |
+
"os": [
|
| 699 |
+
"linux"
|
| 700 |
+
],
|
| 701 |
+
"cpu": [
|
| 702 |
+
"loong64"
|
| 703 |
+
],
|
| 704 |
+
"optional": true
|
| 705 |
+
},
|
| 706 |
+
"node_modules/@esbuild/linux-mips64el": {
|
| 707 |
+
"version": "0.28.2",
|
| 708 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
|
| 709 |
+
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
|
| 710 |
+
"license": "MIT",
|
| 711 |
+
"engines": {
|
| 712 |
+
"node": ">=18"
|
| 713 |
+
},
|
| 714 |
+
"os": [
|
| 715 |
+
"linux"
|
| 716 |
+
],
|
| 717 |
+
"cpu": [
|
| 718 |
+
"mips64el"
|
| 719 |
+
],
|
| 720 |
+
"optional": true
|
| 721 |
+
},
|
| 722 |
+
"node_modules/@esbuild/linux-ppc64": {
|
| 723 |
+
"version": "0.28.2",
|
| 724 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
|
| 725 |
+
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
|
| 726 |
+
"license": "MIT",
|
| 727 |
+
"engines": {
|
| 728 |
+
"node": ">=18"
|
| 729 |
+
},
|
| 730 |
+
"os": [
|
| 731 |
+
"linux"
|
| 732 |
+
],
|
| 733 |
+
"cpu": [
|
| 734 |
+
"ppc64"
|
| 735 |
+
],
|
| 736 |
+
"optional": true
|
| 737 |
+
},
|
| 738 |
+
"node_modules/@esbuild/linux-riscv64": {
|
| 739 |
+
"version": "0.28.2",
|
| 740 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
|
| 741 |
+
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
|
| 742 |
+
"license": "MIT",
|
| 743 |
+
"engines": {
|
| 744 |
+
"node": ">=18"
|
| 745 |
+
},
|
| 746 |
+
"os": [
|
| 747 |
+
"linux"
|
| 748 |
+
],
|
| 749 |
+
"cpu": [
|
| 750 |
+
"riscv64"
|
| 751 |
+
],
|
| 752 |
+
"optional": true
|
| 753 |
+
},
|
| 754 |
+
"node_modules/@esbuild/linux-s390x": {
|
| 755 |
+
"version": "0.28.2",
|
| 756 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
|
| 757 |
+
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
|
| 758 |
+
"license": "MIT",
|
| 759 |
+
"engines": {
|
| 760 |
+
"node": ">=18"
|
| 761 |
+
},
|
| 762 |
+
"os": [
|
| 763 |
+
"linux"
|
| 764 |
+
],
|
| 765 |
+
"cpu": [
|
| 766 |
+
"s390x"
|
| 767 |
+
],
|
| 768 |
+
"optional": true
|
| 769 |
+
},
|
| 770 |
+
"node_modules/@esbuild/linux-x64": {
|
| 771 |
+
"version": "0.28.2",
|
| 772 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
|
| 773 |
+
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
|
| 774 |
+
"license": "MIT",
|
| 775 |
+
"engines": {
|
| 776 |
+
"node": ">=18"
|
| 777 |
+
},
|
| 778 |
+
"os": [
|
| 779 |
+
"linux"
|
| 780 |
+
],
|
| 781 |
+
"cpu": [
|
| 782 |
+
"x64"
|
| 783 |
+
],
|
| 784 |
+
"optional": true
|
| 785 |
+
},
|
| 786 |
+
"node_modules/@esbuild/netbsd-arm64": {
|
| 787 |
+
"version": "0.28.2",
|
| 788 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
|
| 789 |
+
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
|
| 790 |
+
"license": "MIT",
|
| 791 |
+
"engines": {
|
| 792 |
+
"node": ">=18"
|
| 793 |
+
},
|
| 794 |
+
"os": [
|
| 795 |
+
"netbsd"
|
| 796 |
+
],
|
| 797 |
+
"cpu": [
|
| 798 |
+
"arm64"
|
| 799 |
+
],
|
| 800 |
+
"optional": true
|
| 801 |
+
},
|
| 802 |
+
"node_modules/@esbuild/netbsd-x64": {
|
| 803 |
+
"version": "0.28.2",
|
| 804 |
+
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
|
| 805 |
+
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
|
| 806 |
+
"license": "MIT",
|
| 807 |
+
"engines": {
|
| 808 |
+
"node": ">=18"
|
| 809 |
+
},
|
| 810 |
+
"os": [
|
| 811 |
+
"netbsd"
|
| 812 |
+
],
|
| 813 |
+
"cpu": [
|
| 814 |
+
"x64"
|
| 815 |
+
],
|
| 816 |
+
"optional": true
|
| 817 |
+
},
|
| 818 |
+
"node_modules/@esbuild/openbsd-arm64": {
|
| 819 |
+
"version": "0.28.2",
|
| 820 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
|
| 821 |
+
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
|
| 822 |
+
"license": "MIT",
|
| 823 |
+
"engines": {
|
| 824 |
+
"node": ">=18"
|
| 825 |
+
},
|
| 826 |
+
"os": [
|
| 827 |
+
"openbsd"
|
| 828 |
+
],
|
| 829 |
+
"cpu": [
|
| 830 |
+
"arm64"
|
| 831 |
+
],
|
| 832 |
+
"optional": true
|
| 833 |
+
},
|
| 834 |
+
"node_modules/@esbuild/openbsd-x64": {
|
| 835 |
+
"version": "0.28.2",
|
| 836 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
|
| 837 |
+
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
|
| 838 |
+
"license": "MIT",
|
| 839 |
+
"engines": {
|
| 840 |
+
"node": ">=18"
|
| 841 |
+
},
|
| 842 |
+
"os": [
|
| 843 |
+
"openbsd"
|
| 844 |
+
],
|
| 845 |
+
"cpu": [
|
| 846 |
+
"x64"
|
| 847 |
+
],
|
| 848 |
+
"optional": true
|
| 849 |
+
},
|
| 850 |
+
"node_modules/@esbuild/openharmony-arm64": {
|
| 851 |
+
"version": "0.28.2",
|
| 852 |
+
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
|
| 853 |
+
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
|
| 854 |
+
"license": "MIT",
|
| 855 |
+
"engines": {
|
| 856 |
+
"node": ">=18"
|
| 857 |
+
},
|
| 858 |
+
"os": [
|
| 859 |
+
"openharmony"
|
| 860 |
+
],
|
| 861 |
+
"cpu": [
|
| 862 |
+
"arm64"
|
| 863 |
+
],
|
| 864 |
+
"optional": true
|
| 865 |
+
},
|
| 866 |
+
"node_modules/@esbuild/sunos-x64": {
|
| 867 |
+
"version": "0.28.2",
|
| 868 |
+
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
|
| 869 |
+
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
|
| 870 |
+
"license": "MIT",
|
| 871 |
+
"engines": {
|
| 872 |
+
"node": ">=18"
|
| 873 |
+
},
|
| 874 |
+
"os": [
|
| 875 |
+
"sunos"
|
| 876 |
+
],
|
| 877 |
+
"cpu": [
|
| 878 |
+
"x64"
|
| 879 |
+
],
|
| 880 |
+
"optional": true
|
| 881 |
+
},
|
| 882 |
+
"node_modules/@esbuild/win32-arm64": {
|
| 883 |
+
"version": "0.28.2",
|
| 884 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
|
| 885 |
+
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
|
| 886 |
+
"license": "MIT",
|
| 887 |
+
"engines": {
|
| 888 |
+
"node": ">=18"
|
| 889 |
+
},
|
| 890 |
+
"os": [
|
| 891 |
+
"win32"
|
| 892 |
+
],
|
| 893 |
+
"cpu": [
|
| 894 |
+
"arm64"
|
| 895 |
+
],
|
| 896 |
+
"optional": true
|
| 897 |
+
},
|
| 898 |
+
"node_modules/@esbuild/win32-ia32": {
|
| 899 |
+
"version": "0.28.2",
|
| 900 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
|
| 901 |
+
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
|
| 902 |
+
"license": "MIT",
|
| 903 |
+
"engines": {
|
| 904 |
+
"node": ">=18"
|
| 905 |
+
},
|
| 906 |
+
"os": [
|
| 907 |
+
"win32"
|
| 908 |
+
],
|
| 909 |
+
"cpu": [
|
| 910 |
+
"ia32"
|
| 911 |
+
],
|
| 912 |
+
"optional": true
|
| 913 |
+
},
|
| 914 |
+
"node_modules/@esbuild/win32-x64": {
|
| 915 |
+
"version": "0.28.2",
|
| 916 |
+
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
|
| 917 |
+
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
|
| 918 |
+
"license": "MIT",
|
| 919 |
+
"engines": {
|
| 920 |
+
"node": ">=18"
|
| 921 |
+
},
|
| 922 |
+
"os": [
|
| 923 |
+
"win32"
|
| 924 |
+
],
|
| 925 |
+
"cpu": [
|
| 926 |
+
"x64"
|
| 927 |
+
],
|
| 928 |
+
"optional": true
|
| 929 |
+
},
|
| 930 |
+
"node_modules/@google/genai": {
|
| 931 |
+
"version": "2.21.0",
|
| 932 |
+
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.21.0.tgz",
|
| 933 |
+
"integrity": "sha512-+PDtco2/Z0ONdzCGekCoCT+O1VJS9xJQNN4XzQpXG/t3El/SWWMkCWlFRO1KmivOHPa4Q0VjUYu1HBKCZ/v33Q==",
|
| 934 |
+
"license": "Apache-2.0",
|
| 935 |
+
"dependencies": {
|
| 936 |
+
"google-auth-library": "^10.3.0",
|
| 937 |
+
"p-retry": "^4.6.2",
|
| 938 |
+
"protobufjs": "^7.5.4",
|
| 939 |
+
"ws": "^8.18.0"
|
| 940 |
+
},
|
| 941 |
+
"peerDependencies": {
|
| 942 |
+
"@modelcontextprotocol/sdk": "^1.25.2"
|
| 943 |
+
},
|
| 944 |
+
"peerDependenciesMeta": {
|
| 945 |
+
"@modelcontextprotocol/sdk": {
|
| 946 |
+
"optional": true
|
| 947 |
+
}
|
| 948 |
+
},
|
| 949 |
+
"engines": {
|
| 950 |
+
"node": ">=20.0.0"
|
| 951 |
+
},
|
| 952 |
+
"hasInstallScript": true
|
| 953 |
+
},
|
| 954 |
+
"node_modules/@protobufjs/aspromise": {
|
| 955 |
+
"version": "1.1.2",
|
| 956 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
| 957 |
+
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
| 958 |
+
"license": "BSD-3-Clause"
|
| 959 |
+
},
|
| 960 |
+
"node_modules/@protobufjs/base64": {
|
| 961 |
+
"version": "1.1.2",
|
| 962 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
| 963 |
+
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
| 964 |
+
"license": "BSD-3-Clause"
|
| 965 |
+
},
|
| 966 |
+
"node_modules/@protobufjs/codegen": {
|
| 967 |
+
"version": "2.0.5",
|
| 968 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
| 969 |
+
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
| 970 |
+
"license": "BSD-3-Clause"
|
| 971 |
+
},
|
| 972 |
+
"node_modules/@protobufjs/eventemitter": {
|
| 973 |
+
"version": "1.1.1",
|
| 974 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
| 975 |
+
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
| 976 |
+
"license": "BSD-3-Clause"
|
| 977 |
+
},
|
| 978 |
+
"node_modules/@protobufjs/fetch": {
|
| 979 |
+
"version": "1.1.1",
|
| 980 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
| 981 |
+
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
| 982 |
+
"license": "BSD-3-Clause",
|
| 983 |
+
"dependencies": {
|
| 984 |
+
"@protobufjs/aspromise": "^1.1.1"
|
| 985 |
+
}
|
| 986 |
+
},
|
| 987 |
+
"node_modules/@protobufjs/float": {
|
| 988 |
+
"version": "1.0.2",
|
| 989 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
| 990 |
+
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
| 991 |
+
"license": "BSD-3-Clause"
|
| 992 |
+
},
|
| 993 |
+
"node_modules/@protobufjs/path": {
|
| 994 |
+
"version": "1.1.2",
|
| 995 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
| 996 |
+
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
| 997 |
+
"license": "BSD-3-Clause"
|
| 998 |
+
},
|
| 999 |
+
"node_modules/@protobufjs/pool": {
|
| 1000 |
+
"version": "1.1.0",
|
| 1001 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
| 1002 |
+
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
| 1003 |
+
"license": "BSD-3-Clause"
|
| 1004 |
+
},
|
| 1005 |
+
"node_modules/@protobufjs/utf8": {
|
| 1006 |
+
"version": "1.1.1",
|
| 1007 |
+
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
| 1008 |
+
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
|
| 1009 |
+
"license": "BSD-3-Clause"
|
| 1010 |
+
},
|
| 1011 |
+
"node_modules/@silvia-odwyer/photon-node": {
|
| 1012 |
+
"version": "0.3.4",
|
| 1013 |
+
"resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz",
|
| 1014 |
+
"integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==",
|
| 1015 |
+
"license": "Apache-2.0"
|
| 1016 |
+
},
|
| 1017 |
+
"node_modules/@smithy/core": {
|
| 1018 |
+
"version": "3.33.3",
|
| 1019 |
+
"resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.33.3.tgz",
|
| 1020 |
+
"integrity": "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg==",
|
| 1021 |
+
"license": "Apache-2.0",
|
| 1022 |
+
"dependencies": {
|
| 1023 |
+
"@smithy/types": "^4.17.2",
|
| 1024 |
+
"tslib": "^2.6.2"
|
| 1025 |
+
},
|
| 1026 |
+
"engines": {
|
| 1027 |
+
"node": ">=18.0.0"
|
| 1028 |
+
}
|
| 1029 |
+
},
|
| 1030 |
+
"node_modules/@smithy/credential-provider-imds": {
|
| 1031 |
+
"version": "4.5.2",
|
| 1032 |
+
"resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz",
|
| 1033 |
+
"integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==",
|
| 1034 |
+
"license": "Apache-2.0",
|
| 1035 |
+
"dependencies": {
|
| 1036 |
+
"@smithy/core": "^3.33.2",
|
| 1037 |
+
"@smithy/types": "^4.17.2",
|
| 1038 |
+
"tslib": "^2.6.2"
|
| 1039 |
+
},
|
| 1040 |
+
"engines": {
|
| 1041 |
+
"node": ">=18.0.0"
|
| 1042 |
+
}
|
| 1043 |
+
},
|
| 1044 |
+
"node_modules/@smithy/fetch-http-handler": {
|
| 1045 |
+
"version": "5.8.0",
|
| 1046 |
+
"resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz",
|
| 1047 |
+
"integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==",
|
| 1048 |
+
"license": "Apache-2.0",
|
| 1049 |
+
"dependencies": {
|
| 1050 |
+
"@smithy/core": "^3.33.3",
|
| 1051 |
+
"@smithy/types": "^4.18.0",
|
| 1052 |
+
"tslib": "^2.6.2"
|
| 1053 |
+
},
|
| 1054 |
+
"engines": {
|
| 1055 |
+
"node": ">=18.0.0"
|
| 1056 |
+
}
|
| 1057 |
+
},
|
| 1058 |
+
"node_modules/@smithy/node-http-handler": {
|
| 1059 |
+
"version": "4.12.1",
|
| 1060 |
+
"resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz",
|
| 1061 |
+
"integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==",
|
| 1062 |
+
"license": "Apache-2.0",
|
| 1063 |
+
"dependencies": {
|
| 1064 |
+
"@smithy/core": "^3.33.3",
|
| 1065 |
+
"@smithy/types": "^4.18.0",
|
| 1066 |
+
"tslib": "^2.6.2"
|
| 1067 |
+
},
|
| 1068 |
+
"engines": {
|
| 1069 |
+
"node": ">=18.0.0"
|
| 1070 |
+
}
|
| 1071 |
+
},
|
| 1072 |
+
"node_modules/@smithy/signature-v4": {
|
| 1073 |
+
"version": "5.7.3",
|
| 1074 |
+
"resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.3.tgz",
|
| 1075 |
+
"integrity": "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==",
|
| 1076 |
+
"license": "Apache-2.0",
|
| 1077 |
+
"dependencies": {
|
| 1078 |
+
"@smithy/core": "^3.33.3",
|
| 1079 |
+
"@smithy/types": "^4.17.2",
|
| 1080 |
+
"tslib": "^2.6.2"
|
| 1081 |
+
},
|
| 1082 |
+
"engines": {
|
| 1083 |
+
"node": ">=18.0.0"
|
| 1084 |
+
}
|
| 1085 |
+
},
|
| 1086 |
+
"node_modules/@smithy/types": {
|
| 1087 |
+
"version": "4.18.0",
|
| 1088 |
+
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.18.0.tgz",
|
| 1089 |
+
"integrity": "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==",
|
| 1090 |
+
"license": "Apache-2.0",
|
| 1091 |
+
"dependencies": {
|
| 1092 |
+
"tslib": "^2.6.2"
|
| 1093 |
+
},
|
| 1094 |
+
"engines": {
|
| 1095 |
+
"node": ">=18.0.0"
|
| 1096 |
+
}
|
| 1097 |
+
},
|
| 1098 |
+
"node_modules/@stablelib/base64": {
|
| 1099 |
+
"version": "1.0.1",
|
| 1100 |
+
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
|
| 1101 |
+
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
|
| 1102 |
+
"license": "MIT"
|
| 1103 |
+
},
|
| 1104 |
+
"node_modules/@types/node": {
|
| 1105 |
+
"version": "22.19.19",
|
| 1106 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
|
| 1107 |
+
"integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
|
| 1108 |
+
"license": "MIT",
|
| 1109 |
+
"dependencies": {
|
| 1110 |
+
"undici-types": "~6.21.0"
|
| 1111 |
+
}
|
| 1112 |
+
},
|
| 1113 |
+
"node_modules/agent-base": {
|
| 1114 |
+
"version": "7.1.4",
|
| 1115 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
| 1116 |
+
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
| 1117 |
+
"license": "MIT",
|
| 1118 |
+
"engines": {
|
| 1119 |
+
"node": ">= 14"
|
| 1120 |
+
}
|
| 1121 |
+
},
|
| 1122 |
+
"node_modules/balanced-match": {
|
| 1123 |
+
"version": "4.0.4",
|
| 1124 |
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
| 1125 |
+
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
| 1126 |
+
"license": "MIT",
|
| 1127 |
+
"engines": {
|
| 1128 |
+
"node": "18 || 20 || >=22"
|
| 1129 |
+
}
|
| 1130 |
+
},
|
| 1131 |
+
"node_modules/base64-js": {
|
| 1132 |
+
"version": "1.5.1",
|
| 1133 |
+
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
| 1134 |
+
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
| 1135 |
+
"license": "MIT",
|
| 1136 |
+
"funding": [
|
| 1137 |
+
{
|
| 1138 |
+
"type": "github",
|
| 1139 |
+
"url": "https://github.com/sponsors/feross"
|
| 1140 |
+
},
|
| 1141 |
+
{
|
| 1142 |
+
"type": "patreon",
|
| 1143 |
+
"url": "https://www.patreon.com/feross"
|
| 1144 |
+
},
|
| 1145 |
+
{
|
| 1146 |
+
"type": "consulting",
|
| 1147 |
+
"url": "https://feross.org/support"
|
| 1148 |
+
}
|
| 1149 |
+
]
|
| 1150 |
+
},
|
| 1151 |
+
"node_modules/bignumber.js": {
|
| 1152 |
+
"version": "9.3.1",
|
| 1153 |
+
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
| 1154 |
+
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
| 1155 |
+
"license": "MIT",
|
| 1156 |
+
"engines": {
|
| 1157 |
+
"node": "*"
|
| 1158 |
+
}
|
| 1159 |
+
},
|
| 1160 |
+
"node_modules/bowser": {
|
| 1161 |
+
"version": "2.14.1",
|
| 1162 |
+
"resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
|
| 1163 |
+
"integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
|
| 1164 |
+
"license": "MIT"
|
| 1165 |
+
},
|
| 1166 |
+
"node_modules/brace-expansion": {
|
| 1167 |
+
"version": "5.0.9",
|
| 1168 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
| 1169 |
+
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
| 1170 |
+
"license": "MIT",
|
| 1171 |
+
"dependencies": {
|
| 1172 |
+
"balanced-match": "^4.0.2"
|
| 1173 |
+
},
|
| 1174 |
+
"engines": {
|
| 1175 |
+
"node": "20 || >=22"
|
| 1176 |
+
}
|
| 1177 |
+
},
|
| 1178 |
+
"node_modules/buffer-equal-constant-time": {
|
| 1179 |
+
"version": "1.0.1",
|
| 1180 |
+
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
| 1181 |
+
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
| 1182 |
+
"license": "BSD-3-Clause"
|
| 1183 |
+
},
|
| 1184 |
+
"node_modules/chalk": {
|
| 1185 |
+
"version": "6.0.0",
|
| 1186 |
+
"resolved": "https://registry.npmjs.org/chalk/-/chalk-6.0.0.tgz",
|
| 1187 |
+
"integrity": "sha512-2uNTXIuTTxk7ciZgAU1BQcgnchcG0xXnrs6jzkQfj9SsRa9M2s5zE8WT96hS6KmG4MzWHSrvH43DF1m4XRkrFg==",
|
| 1188 |
+
"license": "MIT",
|
| 1189 |
+
"engines": {
|
| 1190 |
+
"node": ">=22"
|
| 1191 |
+
},
|
| 1192 |
+
"funding": {
|
| 1193 |
+
"url": "https://github.com/chalk/chalk?sponsor=1"
|
| 1194 |
+
}
|
| 1195 |
+
},
|
| 1196 |
+
"node_modules/cross-spawn": {
|
| 1197 |
+
"version": "7.0.6",
|
| 1198 |
+
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
| 1199 |
+
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
| 1200 |
+
"license": "MIT",
|
| 1201 |
+
"dependencies": {
|
| 1202 |
+
"path-key": "^3.1.0",
|
| 1203 |
+
"shebang-command": "^2.0.0",
|
| 1204 |
+
"which": "^2.0.1"
|
| 1205 |
+
},
|
| 1206 |
+
"engines": {
|
| 1207 |
+
"node": ">= 8"
|
| 1208 |
+
}
|
| 1209 |
+
},
|
| 1210 |
+
"node_modules/data-uri-to-buffer": {
|
| 1211 |
+
"version": "4.0.1",
|
| 1212 |
+
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
| 1213 |
+
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
| 1214 |
+
"license": "MIT",
|
| 1215 |
+
"engines": {
|
| 1216 |
+
"node": ">= 12"
|
| 1217 |
+
}
|
| 1218 |
+
},
|
| 1219 |
+
"node_modules/debug": {
|
| 1220 |
+
"version": "4.4.3",
|
| 1221 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1222 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1223 |
+
"license": "MIT",
|
| 1224 |
+
"dependencies": {
|
| 1225 |
+
"ms": "^2.1.3"
|
| 1226 |
+
},
|
| 1227 |
+
"peerDependenciesMeta": {
|
| 1228 |
+
"supports-color": {
|
| 1229 |
+
"optional": true
|
| 1230 |
+
}
|
| 1231 |
+
},
|
| 1232 |
+
"engines": {
|
| 1233 |
+
"node": ">=6.0"
|
| 1234 |
+
}
|
| 1235 |
+
},
|
| 1236 |
+
"node_modules/diff": {
|
| 1237 |
+
"version": "8.0.4",
|
| 1238 |
+
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
|
| 1239 |
+
"integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
|
| 1240 |
+
"license": "BSD-3-Clause",
|
| 1241 |
+
"engines": {
|
| 1242 |
+
"node": ">=0.3.1"
|
| 1243 |
+
}
|
| 1244 |
+
},
|
| 1245 |
+
"node_modules/ecdsa-sig-formatter": {
|
| 1246 |
+
"version": "1.0.11",
|
| 1247 |
+
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
| 1248 |
+
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
| 1249 |
+
"license": "Apache-2.0",
|
| 1250 |
+
"dependencies": {
|
| 1251 |
+
"safe-buffer": "^5.0.1"
|
| 1252 |
+
}
|
| 1253 |
+
},
|
| 1254 |
+
"node_modules/esbuild": {
|
| 1255 |
+
"version": "0.28.2",
|
| 1256 |
+
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
|
| 1257 |
+
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
|
| 1258 |
+
"license": "MIT",
|
| 1259 |
+
"optionalDependencies": {
|
| 1260 |
+
"@esbuild/aix-ppc64": "0.28.2",
|
| 1261 |
+
"@esbuild/android-arm": "0.28.2",
|
| 1262 |
+
"@esbuild/android-arm64": "0.28.2",
|
| 1263 |
+
"@esbuild/android-x64": "0.28.2",
|
| 1264 |
+
"@esbuild/darwin-arm64": "0.28.2",
|
| 1265 |
+
"@esbuild/darwin-x64": "0.28.2",
|
| 1266 |
+
"@esbuild/freebsd-arm64": "0.28.2",
|
| 1267 |
+
"@esbuild/freebsd-x64": "0.28.2",
|
| 1268 |
+
"@esbuild/linux-arm": "0.28.2",
|
| 1269 |
+
"@esbuild/linux-arm64": "0.28.2",
|
| 1270 |
+
"@esbuild/linux-ia32": "0.28.2",
|
| 1271 |
+
"@esbuild/linux-loong64": "0.28.2",
|
| 1272 |
+
"@esbuild/linux-mips64el": "0.28.2",
|
| 1273 |
+
"@esbuild/linux-ppc64": "0.28.2",
|
| 1274 |
+
"@esbuild/linux-riscv64": "0.28.2",
|
| 1275 |
+
"@esbuild/linux-s390x": "0.28.2",
|
| 1276 |
+
"@esbuild/linux-x64": "0.28.2",
|
| 1277 |
+
"@esbuild/netbsd-arm64": "0.28.2",
|
| 1278 |
+
"@esbuild/netbsd-x64": "0.28.2",
|
| 1279 |
+
"@esbuild/openbsd-arm64": "0.28.2",
|
| 1280 |
+
"@esbuild/openbsd-x64": "0.28.2",
|
| 1281 |
+
"@esbuild/openharmony-arm64": "0.28.2",
|
| 1282 |
+
"@esbuild/sunos-x64": "0.28.2",
|
| 1283 |
+
"@esbuild/win32-arm64": "0.28.2",
|
| 1284 |
+
"@esbuild/win32-ia32": "0.28.2",
|
| 1285 |
+
"@esbuild/win32-x64": "0.28.2"
|
| 1286 |
+
},
|
| 1287 |
+
"bin": {
|
| 1288 |
+
"esbuild": "bin/esbuild"
|
| 1289 |
+
},
|
| 1290 |
+
"engines": {
|
| 1291 |
+
"node": ">=18"
|
| 1292 |
+
},
|
| 1293 |
+
"hasInstallScript": true
|
| 1294 |
+
},
|
| 1295 |
+
"node_modules/extend": {
|
| 1296 |
+
"version": "3.0.2",
|
| 1297 |
+
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
| 1298 |
+
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
| 1299 |
+
"license": "MIT"
|
| 1300 |
+
},
|
| 1301 |
+
"node_modules/fast-sha256": {
|
| 1302 |
+
"version": "1.3.0",
|
| 1303 |
+
"resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz",
|
| 1304 |
+
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
|
| 1305 |
+
"license": "Unlicense"
|
| 1306 |
+
},
|
| 1307 |
+
"node_modules/fetch-blob": {
|
| 1308 |
+
"version": "3.2.0",
|
| 1309 |
+
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
| 1310 |
+
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
| 1311 |
+
"license": "MIT",
|
| 1312 |
+
"dependencies": {
|
| 1313 |
+
"node-domexception": "^1.0.0",
|
| 1314 |
+
"web-streams-polyfill": "^3.0.3"
|
| 1315 |
+
},
|
| 1316 |
+
"engines": {
|
| 1317 |
+
"node": "^12.20 || >= 14.13"
|
| 1318 |
+
},
|
| 1319 |
+
"funding": [
|
| 1320 |
+
{
|
| 1321 |
+
"type": "github",
|
| 1322 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 1323 |
+
},
|
| 1324 |
+
{
|
| 1325 |
+
"type": "paypal",
|
| 1326 |
+
"url": "https://paypal.me/jimmywarting"
|
| 1327 |
+
}
|
| 1328 |
+
]
|
| 1329 |
+
},
|
| 1330 |
+
"node_modules/formdata-polyfill": {
|
| 1331 |
+
"version": "4.0.10",
|
| 1332 |
+
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
| 1333 |
+
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
| 1334 |
+
"license": "MIT",
|
| 1335 |
+
"dependencies": {
|
| 1336 |
+
"fetch-blob": "^3.1.2"
|
| 1337 |
+
},
|
| 1338 |
+
"engines": {
|
| 1339 |
+
"node": ">=12.20.0"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
"node_modules/gaxios": {
|
| 1343 |
+
"version": "7.1.4",
|
| 1344 |
+
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
|
| 1345 |
+
"integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==",
|
| 1346 |
+
"license": "Apache-2.0",
|
| 1347 |
+
"dependencies": {
|
| 1348 |
+
"extend": "^3.0.2",
|
| 1349 |
+
"https-proxy-agent": "^7.0.1",
|
| 1350 |
+
"node-fetch": "^3.3.2"
|
| 1351 |
+
},
|
| 1352 |
+
"engines": {
|
| 1353 |
+
"node": ">=18"
|
| 1354 |
+
}
|
| 1355 |
+
},
|
| 1356 |
+
"node_modules/gcp-metadata": {
|
| 1357 |
+
"version": "8.1.2",
|
| 1358 |
+
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
| 1359 |
+
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
| 1360 |
+
"license": "Apache-2.0",
|
| 1361 |
+
"dependencies": {
|
| 1362 |
+
"gaxios": "^7.0.0",
|
| 1363 |
+
"google-logging-utils": "^1.0.0",
|
| 1364 |
+
"json-bigint": "^1.0.0"
|
| 1365 |
+
},
|
| 1366 |
+
"engines": {
|
| 1367 |
+
"node": ">=18"
|
| 1368 |
+
}
|
| 1369 |
+
},
|
| 1370 |
+
"node_modules/get-east-asian-width": {
|
| 1371 |
+
"version": "1.6.0",
|
| 1372 |
+
"resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
|
| 1373 |
+
"integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
|
| 1374 |
+
"license": "MIT",
|
| 1375 |
+
"engines": {
|
| 1376 |
+
"node": ">=18"
|
| 1377 |
+
},
|
| 1378 |
+
"funding": {
|
| 1379 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1380 |
+
}
|
| 1381 |
+
},
|
| 1382 |
+
"node_modules/google-auth-library": {
|
| 1383 |
+
"version": "10.6.2",
|
| 1384 |
+
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
|
| 1385 |
+
"integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
|
| 1386 |
+
"license": "Apache-2.0",
|
| 1387 |
+
"dependencies": {
|
| 1388 |
+
"base64-js": "^1.3.0",
|
| 1389 |
+
"ecdsa-sig-formatter": "^1.0.11",
|
| 1390 |
+
"gaxios": "^7.1.4",
|
| 1391 |
+
"gcp-metadata": "8.1.2",
|
| 1392 |
+
"google-logging-utils": "1.1.3",
|
| 1393 |
+
"jws": "^4.0.0"
|
| 1394 |
+
},
|
| 1395 |
+
"engines": {
|
| 1396 |
+
"node": ">=18"
|
| 1397 |
+
}
|
| 1398 |
+
},
|
| 1399 |
+
"node_modules/google-logging-utils": {
|
| 1400 |
+
"version": "1.1.3",
|
| 1401 |
+
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
| 1402 |
+
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
| 1403 |
+
"license": "Apache-2.0",
|
| 1404 |
+
"engines": {
|
| 1405 |
+
"node": ">=14"
|
| 1406 |
+
}
|
| 1407 |
+
},
|
| 1408 |
+
"node_modules/graceful-fs": {
|
| 1409 |
+
"version": "4.2.11",
|
| 1410 |
+
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
| 1411 |
+
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
| 1412 |
+
"license": "ISC"
|
| 1413 |
+
},
|
| 1414 |
+
"node_modules/grok-mermaid": {
|
| 1415 |
+
"version": "0.2.3",
|
| 1416 |
+
"resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.3.tgz",
|
| 1417 |
+
"integrity": "sha512-/4KopAbsjvuRP9MdPtlDjOHUmUVEohOX73JNcsWpzAtFxh+bq5+Dhb6gzvRieLDwIPQIR3/vy8V1NNTuz4Zsmg==",
|
| 1418 |
+
"license": "Apache-2.0",
|
| 1419 |
+
"engines": {
|
| 1420 |
+
"node": ">=18"
|
| 1421 |
+
}
|
| 1422 |
+
},
|
| 1423 |
+
"node_modules/highlight.js": {
|
| 1424 |
+
"version": "10.7.3",
|
| 1425 |
+
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
| 1426 |
+
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
|
| 1427 |
+
"license": "BSD-3-Clause",
|
| 1428 |
+
"engines": {
|
| 1429 |
+
"node": "*"
|
| 1430 |
+
}
|
| 1431 |
+
},
|
| 1432 |
+
"node_modules/hosted-git-info": {
|
| 1433 |
+
"version": "9.0.3",
|
| 1434 |
+
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz",
|
| 1435 |
+
"integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==",
|
| 1436 |
+
"license": "ISC",
|
| 1437 |
+
"dependencies": {
|
| 1438 |
+
"lru-cache": "^11.1.0"
|
| 1439 |
+
},
|
| 1440 |
+
"engines": {
|
| 1441 |
+
"node": "^20.17.0 || >=22.9.0"
|
| 1442 |
+
}
|
| 1443 |
+
},
|
| 1444 |
+
"node_modules/http-proxy-agent": {
|
| 1445 |
+
"version": "9.1.0",
|
| 1446 |
+
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz",
|
| 1447 |
+
"integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==",
|
| 1448 |
+
"license": "MIT",
|
| 1449 |
+
"dependencies": {
|
| 1450 |
+
"agent-base": "9.0.0",
|
| 1451 |
+
"debug": "^4.3.4",
|
| 1452 |
+
"proxy-agent-negotiate": "1.1.0"
|
| 1453 |
+
},
|
| 1454 |
+
"engines": {
|
| 1455 |
+
"node": ">= 20"
|
| 1456 |
+
}
|
| 1457 |
+
},
|
| 1458 |
+
"node_modules/http-proxy-agent/node_modules/agent-base": {
|
| 1459 |
+
"version": "9.0.0",
|
| 1460 |
+
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz",
|
| 1461 |
+
"integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==",
|
| 1462 |
+
"license": "MIT",
|
| 1463 |
+
"engines": {
|
| 1464 |
+
"node": ">= 20"
|
| 1465 |
+
}
|
| 1466 |
+
},
|
| 1467 |
+
"node_modules/https-proxy-agent": {
|
| 1468 |
+
"version": "7.0.6",
|
| 1469 |
+
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
| 1470 |
+
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
| 1471 |
+
"license": "MIT",
|
| 1472 |
+
"dependencies": {
|
| 1473 |
+
"agent-base": "^7.1.2",
|
| 1474 |
+
"debug": "4"
|
| 1475 |
+
},
|
| 1476 |
+
"engines": {
|
| 1477 |
+
"node": ">= 14"
|
| 1478 |
+
}
|
| 1479 |
+
},
|
| 1480 |
+
"node_modules/ignore": {
|
| 1481 |
+
"version": "7.0.8",
|
| 1482 |
+
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.8.tgz",
|
| 1483 |
+
"integrity": "sha512-YYNsSlXBjMk92SKnkwvB5LOVSa6OznlFUGcsvrFgNJbJCd0M1XKeFVRc8ZByeCqz32FivYNHJVooLmdqrmvp/Q==",
|
| 1484 |
+
"license": "MIT",
|
| 1485 |
+
"engines": {
|
| 1486 |
+
"node": ">= 4"
|
| 1487 |
+
}
|
| 1488 |
+
},
|
| 1489 |
+
"node_modules/isexe": {
|
| 1490 |
+
"version": "2.0.0",
|
| 1491 |
+
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
| 1492 |
+
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
| 1493 |
+
"license": "ISC"
|
| 1494 |
+
},
|
| 1495 |
+
"node_modules/jiti": {
|
| 1496 |
+
"version": "2.7.0",
|
| 1497 |
+
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
|
| 1498 |
+
"integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
|
| 1499 |
+
"license": "MIT",
|
| 1500 |
+
"bin": {
|
| 1501 |
+
"jiti": "lib/jiti-cli.mjs"
|
| 1502 |
+
}
|
| 1503 |
+
},
|
| 1504 |
+
"node_modules/json-bigint": {
|
| 1505 |
+
"version": "1.0.0",
|
| 1506 |
+
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
| 1507 |
+
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
| 1508 |
+
"license": "MIT",
|
| 1509 |
+
"dependencies": {
|
| 1510 |
+
"bignumber.js": "^9.0.0"
|
| 1511 |
+
}
|
| 1512 |
+
},
|
| 1513 |
+
"node_modules/json-schema-to-ts": {
|
| 1514 |
+
"version": "3.1.1",
|
| 1515 |
+
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
|
| 1516 |
+
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
|
| 1517 |
+
"license": "MIT",
|
| 1518 |
+
"dependencies": {
|
| 1519 |
+
"@babel/runtime": "^7.18.3",
|
| 1520 |
+
"ts-algebra": "^2.0.0"
|
| 1521 |
+
},
|
| 1522 |
+
"engines": {
|
| 1523 |
+
"node": ">=16"
|
| 1524 |
+
}
|
| 1525 |
+
},
|
| 1526 |
+
"node_modules/jwa": {
|
| 1527 |
+
"version": "2.0.1",
|
| 1528 |
+
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
| 1529 |
+
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
| 1530 |
+
"license": "MIT",
|
| 1531 |
+
"dependencies": {
|
| 1532 |
+
"buffer-equal-constant-time": "^1.0.1",
|
| 1533 |
+
"ecdsa-sig-formatter": "1.0.11",
|
| 1534 |
+
"safe-buffer": "^5.0.1"
|
| 1535 |
+
}
|
| 1536 |
+
},
|
| 1537 |
+
"node_modules/jws": {
|
| 1538 |
+
"version": "4.0.1",
|
| 1539 |
+
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
| 1540 |
+
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
| 1541 |
+
"license": "MIT",
|
| 1542 |
+
"dependencies": {
|
| 1543 |
+
"jwa": "^2.0.1",
|
| 1544 |
+
"safe-buffer": "^5.0.1"
|
| 1545 |
+
}
|
| 1546 |
+
},
|
| 1547 |
+
"node_modules/long": {
|
| 1548 |
+
"version": "5.3.2",
|
| 1549 |
+
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
| 1550 |
+
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
| 1551 |
+
"license": "Apache-2.0"
|
| 1552 |
+
},
|
| 1553 |
+
"node_modules/lru-cache": {
|
| 1554 |
+
"version": "11.4.0",
|
| 1555 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz",
|
| 1556 |
+
"integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==",
|
| 1557 |
+
"license": "BlueOak-1.0.0",
|
| 1558 |
+
"engines": {
|
| 1559 |
+
"node": "20 || >=22"
|
| 1560 |
+
}
|
| 1561 |
+
},
|
| 1562 |
+
"node_modules/marked": {
|
| 1563 |
+
"version": "18.0.11",
|
| 1564 |
+
"resolved": "https://registry.npmjs.org/marked/-/marked-18.0.11.tgz",
|
| 1565 |
+
"integrity": "sha512-HnslJfsZkRPBDJRHvVtAaWlZHEpSu7u8LgQuJCELjRKuWR+hpq4A7sLq3p8HaI9ypVoXDXxV34CsQJEe1+J5Aw==",
|
| 1566 |
+
"license": "MIT",
|
| 1567 |
+
"bin": {
|
| 1568 |
+
"marked": "bin/marked.js"
|
| 1569 |
+
},
|
| 1570 |
+
"engines": {
|
| 1571 |
+
"node": ">= 20"
|
| 1572 |
+
}
|
| 1573 |
+
},
|
| 1574 |
+
"node_modules/minimatch": {
|
| 1575 |
+
"version": "10.2.6",
|
| 1576 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
| 1577 |
+
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
| 1578 |
+
"license": "BlueOak-1.0.0",
|
| 1579 |
+
"dependencies": {
|
| 1580 |
+
"brace-expansion": "^5.0.8"
|
| 1581 |
+
},
|
| 1582 |
+
"engines": {
|
| 1583 |
+
"node": "18 || 20 || >=22"
|
| 1584 |
+
},
|
| 1585 |
+
"funding": {
|
| 1586 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 1587 |
+
}
|
| 1588 |
+
},
|
| 1589 |
+
"node_modules/ms": {
|
| 1590 |
+
"version": "2.1.3",
|
| 1591 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 1592 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 1593 |
+
"license": "MIT"
|
| 1594 |
+
},
|
| 1595 |
+
"node_modules/node-domexception": {
|
| 1596 |
+
"version": "1.0.0",
|
| 1597 |
+
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
| 1598 |
+
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
| 1599 |
+
"license": "MIT",
|
| 1600 |
+
"engines": {
|
| 1601 |
+
"node": ">=10.5.0"
|
| 1602 |
+
},
|
| 1603 |
+
"deprecated": "Use your platform's native DOMException instead",
|
| 1604 |
+
"funding": [
|
| 1605 |
+
{
|
| 1606 |
+
"type": "github",
|
| 1607 |
+
"url": "https://github.com/sponsors/jimmywarting"
|
| 1608 |
+
},
|
| 1609 |
+
{
|
| 1610 |
+
"type": "github",
|
| 1611 |
+
"url": "https://paypal.me/jimmywarting"
|
| 1612 |
+
}
|
| 1613 |
+
]
|
| 1614 |
+
},
|
| 1615 |
+
"node_modules/node-fetch": {
|
| 1616 |
+
"version": "3.3.2",
|
| 1617 |
+
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
| 1618 |
+
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
| 1619 |
+
"license": "MIT",
|
| 1620 |
+
"dependencies": {
|
| 1621 |
+
"data-uri-to-buffer": "^4.0.0",
|
| 1622 |
+
"fetch-blob": "^3.1.4",
|
| 1623 |
+
"formdata-polyfill": "^4.0.10"
|
| 1624 |
+
},
|
| 1625 |
+
"engines": {
|
| 1626 |
+
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
| 1627 |
+
},
|
| 1628 |
+
"funding": {
|
| 1629 |
+
"type": "opencollective",
|
| 1630 |
+
"url": "https://opencollective.com/node-fetch"
|
| 1631 |
+
}
|
| 1632 |
+
},
|
| 1633 |
+
"node_modules/openai": {
|
| 1634 |
+
"version": "6.40.0",
|
| 1635 |
+
"resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz",
|
| 1636 |
+
"integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==",
|
| 1637 |
+
"license": "Apache-2.0",
|
| 1638 |
+
"peerDependencies": {
|
| 1639 |
+
"ws": "^8.18.0",
|
| 1640 |
+
"zod": "^3.25 || ^4.0"
|
| 1641 |
+
},
|
| 1642 |
+
"peerDependenciesMeta": {
|
| 1643 |
+
"ws": {
|
| 1644 |
+
"optional": true
|
| 1645 |
+
},
|
| 1646 |
+
"zod": {
|
| 1647 |
+
"optional": true
|
| 1648 |
+
}
|
| 1649 |
+
}
|
| 1650 |
+
},
|
| 1651 |
+
"node_modules/p-retry": {
|
| 1652 |
+
"version": "4.6.2",
|
| 1653 |
+
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
| 1654 |
+
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
| 1655 |
+
"license": "MIT",
|
| 1656 |
+
"dependencies": {
|
| 1657 |
+
"@types/retry": "0.12.0",
|
| 1658 |
+
"retry": "^0.13.1"
|
| 1659 |
+
},
|
| 1660 |
+
"engines": {
|
| 1661 |
+
"node": ">=8"
|
| 1662 |
+
}
|
| 1663 |
+
},
|
| 1664 |
+
"node_modules/p-retry/node_modules/@types/retry": {
|
| 1665 |
+
"version": "0.12.0",
|
| 1666 |
+
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
| 1667 |
+
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
| 1668 |
+
"license": "MIT"
|
| 1669 |
+
},
|
| 1670 |
+
"node_modules/partial-json": {
|
| 1671 |
+
"version": "0.1.7",
|
| 1672 |
+
"resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz",
|
| 1673 |
+
"integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==",
|
| 1674 |
+
"license": "MIT"
|
| 1675 |
+
},
|
| 1676 |
+
"node_modules/path-key": {
|
| 1677 |
+
"version": "3.1.1",
|
| 1678 |
+
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
| 1679 |
+
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
| 1680 |
+
"license": "MIT",
|
| 1681 |
+
"engines": {
|
| 1682 |
+
"node": ">=8"
|
| 1683 |
+
}
|
| 1684 |
+
},
|
| 1685 |
+
"node_modules/proper-lockfile": {
|
| 1686 |
+
"version": "4.1.2",
|
| 1687 |
+
"resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz",
|
| 1688 |
+
"integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==",
|
| 1689 |
+
"license": "MIT",
|
| 1690 |
+
"dependencies": {
|
| 1691 |
+
"graceful-fs": "^4.2.4",
|
| 1692 |
+
"retry": "^0.12.0",
|
| 1693 |
+
"signal-exit": "^3.0.2"
|
| 1694 |
+
}
|
| 1695 |
+
},
|
| 1696 |
+
"node_modules/proper-lockfile/node_modules/retry": {
|
| 1697 |
+
"version": "0.12.0",
|
| 1698 |
+
"resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
|
| 1699 |
+
"integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
|
| 1700 |
+
"license": "MIT",
|
| 1701 |
+
"engines": {
|
| 1702 |
+
"node": ">= 4"
|
| 1703 |
+
}
|
| 1704 |
+
},
|
| 1705 |
+
"node_modules/protobufjs": {
|
| 1706 |
+
"version": "7.6.6",
|
| 1707 |
+
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz",
|
| 1708 |
+
"integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==",
|
| 1709 |
+
"license": "BSD-3-Clause",
|
| 1710 |
+
"dependencies": {
|
| 1711 |
+
"@protobufjs/aspromise": "^1.1.2",
|
| 1712 |
+
"@protobufjs/base64": "^1.1.2",
|
| 1713 |
+
"@protobufjs/codegen": "^2.0.5",
|
| 1714 |
+
"@protobufjs/eventemitter": "^1.1.1",
|
| 1715 |
+
"@protobufjs/fetch": "^1.1.1",
|
| 1716 |
+
"@protobufjs/float": "^1.0.2",
|
| 1717 |
+
"@protobufjs/path": "^1.1.2",
|
| 1718 |
+
"@protobufjs/pool": "^1.1.0",
|
| 1719 |
+
"@protobufjs/utf8": "^1.1.1",
|
| 1720 |
+
"@types/node": ">=13.7.0",
|
| 1721 |
+
"long": "^5.3.2"
|
| 1722 |
+
},
|
| 1723 |
+
"engines": {
|
| 1724 |
+
"node": ">=12.0.0"
|
| 1725 |
+
},
|
| 1726 |
+
"hasInstallScript": true
|
| 1727 |
+
},
|
| 1728 |
+
"node_modules/proxy-agent-negotiate": {
|
| 1729 |
+
"version": "1.1.0",
|
| 1730 |
+
"resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz",
|
| 1731 |
+
"integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==",
|
| 1732 |
+
"license": "MIT",
|
| 1733 |
+
"peerDependencies": {
|
| 1734 |
+
"kerberos": "^2.0.0"
|
| 1735 |
+
},
|
| 1736 |
+
"peerDependenciesMeta": {
|
| 1737 |
+
"kerberos": {
|
| 1738 |
+
"optional": true
|
| 1739 |
+
}
|
| 1740 |
+
},
|
| 1741 |
+
"engines": {
|
| 1742 |
+
"node": ">= 20"
|
| 1743 |
+
}
|
| 1744 |
+
},
|
| 1745 |
+
"node_modules/retry": {
|
| 1746 |
+
"version": "0.13.1",
|
| 1747 |
+
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
| 1748 |
+
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
| 1749 |
+
"license": "MIT",
|
| 1750 |
+
"engines": {
|
| 1751 |
+
"node": ">= 4"
|
| 1752 |
+
}
|
| 1753 |
+
},
|
| 1754 |
+
"node_modules/safe-buffer": {
|
| 1755 |
+
"version": "5.2.1",
|
| 1756 |
+
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
| 1757 |
+
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
| 1758 |
+
"license": "MIT",
|
| 1759 |
+
"funding": [
|
| 1760 |
+
{
|
| 1761 |
+
"type": "github",
|
| 1762 |
+
"url": "https://github.com/sponsors/feross"
|
| 1763 |
+
},
|
| 1764 |
+
{
|
| 1765 |
+
"type": "patreon",
|
| 1766 |
+
"url": "https://www.patreon.com/feross"
|
| 1767 |
+
},
|
| 1768 |
+
{
|
| 1769 |
+
"type": "consulting",
|
| 1770 |
+
"url": "https://feross.org/support"
|
| 1771 |
+
}
|
| 1772 |
+
]
|
| 1773 |
+
},
|
| 1774 |
+
"node_modules/semver": {
|
| 1775 |
+
"version": "7.8.5",
|
| 1776 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
| 1777 |
+
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
| 1778 |
+
"license": "ISC",
|
| 1779 |
+
"bin": {
|
| 1780 |
+
"semver": "bin/semver.js"
|
| 1781 |
+
},
|
| 1782 |
+
"engines": {
|
| 1783 |
+
"node": ">=10"
|
| 1784 |
+
}
|
| 1785 |
+
},
|
| 1786 |
+
"node_modules/shebang-command": {
|
| 1787 |
+
"version": "2.0.0",
|
| 1788 |
+
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
| 1789 |
+
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
| 1790 |
+
"license": "MIT",
|
| 1791 |
+
"dependencies": {
|
| 1792 |
+
"shebang-regex": "^3.0.0"
|
| 1793 |
+
},
|
| 1794 |
+
"engines": {
|
| 1795 |
+
"node": ">=8"
|
| 1796 |
+
}
|
| 1797 |
+
},
|
| 1798 |
+
"node_modules/shebang-regex": {
|
| 1799 |
+
"version": "3.0.0",
|
| 1800 |
+
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
| 1801 |
+
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
| 1802 |
+
"license": "MIT",
|
| 1803 |
+
"engines": {
|
| 1804 |
+
"node": ">=8"
|
| 1805 |
+
}
|
| 1806 |
+
},
|
| 1807 |
+
"node_modules/signal-exit": {
|
| 1808 |
+
"version": "3.0.7",
|
| 1809 |
+
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
|
| 1810 |
+
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
| 1811 |
+
"license": "ISC"
|
| 1812 |
+
},
|
| 1813 |
+
"node_modules/standardwebhooks": {
|
| 1814 |
+
"version": "1.1.1",
|
| 1815 |
+
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz",
|
| 1816 |
+
"integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==",
|
| 1817 |
+
"license": "MIT",
|
| 1818 |
+
"dependencies": {
|
| 1819 |
+
"@stablelib/base64": "^1.0.0",
|
| 1820 |
+
"fast-sha256": "^1.3.0"
|
| 1821 |
+
}
|
| 1822 |
+
},
|
| 1823 |
+
"node_modules/ts-algebra": {
|
| 1824 |
+
"version": "2.0.0",
|
| 1825 |
+
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
|
| 1826 |
+
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
|
| 1827 |
+
"license": "MIT"
|
| 1828 |
+
},
|
| 1829 |
+
"node_modules/tslib": {
|
| 1830 |
+
"version": "2.8.1",
|
| 1831 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 1832 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 1833 |
+
"license": "0BSD"
|
| 1834 |
+
},
|
| 1835 |
+
"node_modules/typebox": {
|
| 1836 |
+
"version": "1.3.27",
|
| 1837 |
+
"resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.27.tgz",
|
| 1838 |
+
"integrity": "sha512-zu+jc1pcy4UiNThxikUr36f0Rybk9PEeCg/NE6adeWr/SKsdNO4EzZHYRDlv2YCVAfj3Odq3dESSo/jNyoBXzA==",
|
| 1839 |
+
"license": "MIT"
|
| 1840 |
+
},
|
| 1841 |
+
"node_modules/undici": {
|
| 1842 |
+
"version": "8.10.2",
|
| 1843 |
+
"resolved": "https://registry.npmjs.org/undici/-/undici-8.10.2.tgz",
|
| 1844 |
+
"integrity": "sha512-/y4/bH9YNU5hi9NIrpOuvGXFcxrj3CMrV+/AYpowAYTpHn8gX/XPFjNy766FPoYY0miQhdW977JFWKGNhBdwyQ==",
|
| 1845 |
+
"license": "MIT",
|
| 1846 |
+
"engines": {
|
| 1847 |
+
"node": ">=22.19.0"
|
| 1848 |
+
}
|
| 1849 |
+
},
|
| 1850 |
+
"node_modules/undici-types": {
|
| 1851 |
+
"version": "6.21.0",
|
| 1852 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
| 1853 |
+
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
| 1854 |
+
"license": "MIT"
|
| 1855 |
+
},
|
| 1856 |
+
"node_modules/web-streams-polyfill": {
|
| 1857 |
+
"version": "3.3.3",
|
| 1858 |
+
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
| 1859 |
+
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
| 1860 |
+
"license": "MIT",
|
| 1861 |
+
"engines": {
|
| 1862 |
+
"node": ">= 8"
|
| 1863 |
+
}
|
| 1864 |
+
},
|
| 1865 |
+
"node_modules/which": {
|
| 1866 |
+
"version": "2.0.2",
|
| 1867 |
+
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
| 1868 |
+
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
| 1869 |
+
"license": "ISC",
|
| 1870 |
+
"dependencies": {
|
| 1871 |
+
"isexe": "^2.0.0"
|
| 1872 |
+
},
|
| 1873 |
+
"bin": {
|
| 1874 |
+
"node-which": "bin/node-which"
|
| 1875 |
+
},
|
| 1876 |
+
"engines": {
|
| 1877 |
+
"node": ">= 8"
|
| 1878 |
+
}
|
| 1879 |
+
},
|
| 1880 |
+
"node_modules/ws": {
|
| 1881 |
+
"version": "8.21.0",
|
| 1882 |
+
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
| 1883 |
+
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
| 1884 |
+
"license": "MIT",
|
| 1885 |
+
"peerDependencies": {
|
| 1886 |
+
"bufferutil": "^4.0.1",
|
| 1887 |
+
"utf-8-validate": ">=5.0.2"
|
| 1888 |
+
},
|
| 1889 |
+
"peerDependenciesMeta": {
|
| 1890 |
+
"bufferutil": {
|
| 1891 |
+
"optional": true
|
| 1892 |
+
},
|
| 1893 |
+
"utf-8-validate": {
|
| 1894 |
+
"optional": true
|
| 1895 |
+
}
|
| 1896 |
+
},
|
| 1897 |
+
"engines": {
|
| 1898 |
+
"node": ">=10.0.0"
|
| 1899 |
+
}
|
| 1900 |
+
},
|
| 1901 |
+
"node_modules/yaml": {
|
| 1902 |
+
"version": "2.9.0",
|
| 1903 |
+
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
|
| 1904 |
+
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
|
| 1905 |
+
"license": "ISC",
|
| 1906 |
+
"bin": {
|
| 1907 |
+
"yaml": "bin.mjs"
|
| 1908 |
+
},
|
| 1909 |
+
"engines": {
|
| 1910 |
+
"node": ">= 14.6"
|
| 1911 |
+
},
|
| 1912 |
+
"funding": {
|
| 1913 |
+
"url": "https://github.com/sponsors/eemeli"
|
| 1914 |
+
}
|
| 1915 |
+
}
|
| 1916 |
+
}
|
| 1917 |
+
}
|
packages/coding-agent/install-lock/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@earendil-works/pi-coding-agent-install",
|
| 3 |
+
"version": "0.85.1",
|
| 4 |
+
"private": true,
|
| 5 |
+
"description": "Lockfile root used by the Pi installer and updater.",
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"@earendil-works/pi-coding-agent": "0.85.1"
|
| 8 |
+
},
|
| 9 |
+
"overrides": {
|
| 10 |
+
"protobufjs": "7.6.6"
|
| 11 |
+
},
|
| 12 |
+
"engines": {
|
| 13 |
+
"node": ">=22.19.0"
|
| 14 |
+
}
|
| 15 |
+
}
|
packages/coding-agent/src/core/compaction/branch-summarization.ts
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Branch summarization for tree navigation.
|
| 3 |
+
*
|
| 4 |
+
* When navigating to a different point in the session tree, this generates
|
| 5 |
+
* a summary of the branch being left so context isn't lost.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type { AgentMessage, StreamFn } from "@earendil-works/pi-agent-core";
|
| 9 |
+
import type { RetryCallbacks, RetryPolicy } from "@earendil-works/pi-ai";
|
| 10 |
+
import { contentText } from "@earendil-works/pi-ai";
|
| 11 |
+
import type { Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat";
|
| 12 |
+
import {
|
| 13 |
+
convertToLlm,
|
| 14 |
+
createBranchSummaryMessage,
|
| 15 |
+
createCompactionSummaryMessage,
|
| 16 |
+
createCustomMessage,
|
| 17 |
+
} from "../messages.ts";
|
| 18 |
+
import type { ReadonlySessionManager, SessionEntry } from "../session-manager.ts";
|
| 19 |
+
import { completeSummarization, estimateTokens, getSummarizationFailure } from "./compaction.ts";
|
| 20 |
+
import {
|
| 21 |
+
computeFileLists,
|
| 22 |
+
createFileOps,
|
| 23 |
+
extractFileOpsFromMessage,
|
| 24 |
+
type FileOperations,
|
| 25 |
+
formatFileOperations,
|
| 26 |
+
SUMMARIZATION_SYSTEM_PROMPT,
|
| 27 |
+
serializeConversation,
|
| 28 |
+
} from "./utils.ts";
|
| 29 |
+
|
| 30 |
+
// ============================================================================
|
| 31 |
+
// Types
|
| 32 |
+
// ============================================================================
|
| 33 |
+
|
| 34 |
+
export interface BranchSummaryResult {
|
| 35 |
+
summary?: string;
|
| 36 |
+
usage?: Usage;
|
| 37 |
+
readFiles?: string[];
|
| 38 |
+
modifiedFiles?: string[];
|
| 39 |
+
aborted?: boolean;
|
| 40 |
+
error?: string;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/** Details stored in BranchSummaryEntry.details for file tracking */
|
| 44 |
+
export interface BranchSummaryDetails {
|
| 45 |
+
readFiles: string[];
|
| 46 |
+
modifiedFiles: string[];
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export type { FileOperations } from "./utils.ts";
|
| 50 |
+
|
| 51 |
+
export interface BranchPreparation {
|
| 52 |
+
/** Messages extracted for summarization, in chronological order */
|
| 53 |
+
messages: AgentMessage[];
|
| 54 |
+
/** File operations extracted from tool calls */
|
| 55 |
+
fileOps: FileOperations;
|
| 56 |
+
/** Total estimated tokens in messages */
|
| 57 |
+
totalTokens: number;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export interface CollectEntriesResult {
|
| 61 |
+
/** Entries to summarize, in chronological order */
|
| 62 |
+
entries: SessionEntry[];
|
| 63 |
+
/** Common ancestor between old and new position, if any */
|
| 64 |
+
commonAncestorId: string | null;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface GenerateBranchSummaryOptions {
|
| 68 |
+
/** Model to use for summarization */
|
| 69 |
+
model: Model<any>;
|
| 70 |
+
/** API key for the model */
|
| 71 |
+
apiKey?: string;
|
| 72 |
+
/** Request headers for the model */
|
| 73 |
+
headers?: Record<string, string>;
|
| 74 |
+
/** Provider-scoped environment values for the model */
|
| 75 |
+
env?: Record<string, string>;
|
| 76 |
+
/** Abort signal for cancellation */
|
| 77 |
+
signal: AbortSignal;
|
| 78 |
+
/** Optional custom instructions for summarization */
|
| 79 |
+
customInstructions?: string;
|
| 80 |
+
/** If true, customInstructions replaces the default prompt instead of being appended */
|
| 81 |
+
replaceInstructions?: boolean;
|
| 82 |
+
/** Tokens reserved when selecting branch history (default 16384) */
|
| 83 |
+
reserveTokens?: number;
|
| 84 |
+
/** Optional session stream function. Used to preserve SDK request behavior without mutating agent state. */
|
| 85 |
+
streamFn?: StreamFn;
|
| 86 |
+
/** Retry policy for transient summarization errors. Reuses coding-agent's `settings.retry`. */
|
| 87 |
+
retry?: RetryPolicy;
|
| 88 |
+
/** Optional callbacks for retry reporting (e.g. TUI retry indicators). */
|
| 89 |
+
callbacks?: RetryCallbacks;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ============================================================================
|
| 93 |
+
// Entry Collection
|
| 94 |
+
// ============================================================================
|
| 95 |
+
|
| 96 |
+
/**
|
| 97 |
+
* Collect entries that should be summarized when navigating from one position to another.
|
| 98 |
+
*
|
| 99 |
+
* Walks from oldLeafId back to the common ancestor with targetId, collecting entries
|
| 100 |
+
* along the way. Does NOT stop at compaction boundaries - those are included and their
|
| 101 |
+
* summaries become context.
|
| 102 |
+
*
|
| 103 |
+
* @param session - Session manager (read-only access)
|
| 104 |
+
* @param oldLeafId - Current position (where we're navigating from)
|
| 105 |
+
* @param targetId - Target position (where we're navigating to)
|
| 106 |
+
* @returns Entries to summarize and the common ancestor
|
| 107 |
+
*/
|
| 108 |
+
export function collectEntriesForBranchSummary(
|
| 109 |
+
session: ReadonlySessionManager,
|
| 110 |
+
oldLeafId: string | null,
|
| 111 |
+
targetId: string,
|
| 112 |
+
): CollectEntriesResult {
|
| 113 |
+
// If no old position, nothing to summarize
|
| 114 |
+
if (!oldLeafId) {
|
| 115 |
+
return { entries: [], commonAncestorId: null };
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Find common ancestor (deepest node that's on both paths)
|
| 119 |
+
const oldPath = new Set(session.getBranch(oldLeafId).map((e) => e.id));
|
| 120 |
+
const targetPath = session.getBranch(targetId);
|
| 121 |
+
|
| 122 |
+
// targetPath is root-first, so iterate backwards to find deepest common ancestor
|
| 123 |
+
let commonAncestorId: string | null = null;
|
| 124 |
+
for (let i = targetPath.length - 1; i >= 0; i--) {
|
| 125 |
+
if (oldPath.has(targetPath[i].id)) {
|
| 126 |
+
commonAncestorId = targetPath[i].id;
|
| 127 |
+
break;
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
// Collect entries from old leaf back to common ancestor
|
| 132 |
+
const entries: SessionEntry[] = [];
|
| 133 |
+
let current: string | null = oldLeafId;
|
| 134 |
+
|
| 135 |
+
while (current && current !== commonAncestorId) {
|
| 136 |
+
const entry = session.getEntry(current);
|
| 137 |
+
if (!entry) break;
|
| 138 |
+
entries.push(entry);
|
| 139 |
+
current = entry.parentId;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Reverse to get chronological order
|
| 143 |
+
entries.reverse();
|
| 144 |
+
|
| 145 |
+
return { entries, commonAncestorId };
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
// ============================================================================
|
| 149 |
+
// Entry to Message Conversion
|
| 150 |
+
// ============================================================================
|
| 151 |
+
|
| 152 |
+
/**
|
| 153 |
+
* Extract AgentMessage from a session entry.
|
| 154 |
+
* Similar to getMessageFromEntry in compaction.ts but also handles compaction entries.
|
| 155 |
+
*/
|
| 156 |
+
function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
| 157 |
+
switch (entry.type) {
|
| 158 |
+
case "message":
|
| 159 |
+
// Skip tool results - context is in assistant's tool call
|
| 160 |
+
if (entry.message.role === "toolResult") return undefined;
|
| 161 |
+
return entry.message;
|
| 162 |
+
|
| 163 |
+
case "custom_message":
|
| 164 |
+
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
|
| 165 |
+
|
| 166 |
+
case "branch_summary":
|
| 167 |
+
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
| 168 |
+
|
| 169 |
+
case "compaction":
|
| 170 |
+
return createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
|
| 171 |
+
|
| 172 |
+
// These don't contribute to conversation content
|
| 173 |
+
case "thinking_level_change":
|
| 174 |
+
case "model_change":
|
| 175 |
+
case "custom":
|
| 176 |
+
case "label":
|
| 177 |
+
case "session_info":
|
| 178 |
+
return undefined;
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/**
|
| 183 |
+
* Prepare entries for summarization with token budget.
|
| 184 |
+
*
|
| 185 |
+
* Walks entries from NEWEST to OLDEST, adding messages until we hit the token budget.
|
| 186 |
+
* This ensures we keep the most recent context when the branch is too long.
|
| 187 |
+
*
|
| 188 |
+
* Also collects file operations from:
|
| 189 |
+
* - Tool calls in assistant messages
|
| 190 |
+
* - Existing branch_summary entries' details (for cumulative tracking)
|
| 191 |
+
*
|
| 192 |
+
* @param entries - Entries in chronological order
|
| 193 |
+
* @param tokenBudget - Maximum tokens to include (0 = no limit)
|
| 194 |
+
*/
|
| 195 |
+
export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: number = 0): BranchPreparation {
|
| 196 |
+
const messages: AgentMessage[] = [];
|
| 197 |
+
const fileOps = createFileOps();
|
| 198 |
+
let totalTokens = 0;
|
| 199 |
+
|
| 200 |
+
// First pass: collect file ops from ALL entries (even if they don't fit in token budget)
|
| 201 |
+
// This ensures we capture cumulative file tracking from nested branch summaries
|
| 202 |
+
// Only extract from pi-generated summaries (fromHook !== true), not extension-generated ones
|
| 203 |
+
for (const entry of entries) {
|
| 204 |
+
if (entry.type === "branch_summary" && !entry.fromHook && entry.details) {
|
| 205 |
+
const details = entry.details as BranchSummaryDetails;
|
| 206 |
+
if (Array.isArray(details.readFiles)) {
|
| 207 |
+
for (const f of details.readFiles) fileOps.read.add(f);
|
| 208 |
+
}
|
| 209 |
+
if (Array.isArray(details.modifiedFiles)) {
|
| 210 |
+
// Modified files go into both edited and written for proper deduplication
|
| 211 |
+
for (const f of details.modifiedFiles) {
|
| 212 |
+
fileOps.edited.add(f);
|
| 213 |
+
}
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
// Second pass: walk from newest to oldest, adding messages until token budget
|
| 219 |
+
for (let i = entries.length - 1; i >= 0; i--) {
|
| 220 |
+
const entry = entries[i];
|
| 221 |
+
const message = getMessageFromEntry(entry);
|
| 222 |
+
if (!message) continue;
|
| 223 |
+
|
| 224 |
+
// Extract file ops from assistant messages (tool calls)
|
| 225 |
+
extractFileOpsFromMessage(message, fileOps);
|
| 226 |
+
|
| 227 |
+
const tokens = estimateTokens(message);
|
| 228 |
+
|
| 229 |
+
// Check budget before adding
|
| 230 |
+
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
|
| 231 |
+
// If this is a summary entry, try to fit it anyway as it's important context
|
| 232 |
+
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
| 233 |
+
if (totalTokens < tokenBudget * 0.9) {
|
| 234 |
+
messages.unshift(message);
|
| 235 |
+
totalTokens += tokens;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
// Stop - we've hit the budget
|
| 239 |
+
break;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
messages.unshift(message);
|
| 243 |
+
totalTokens += tokens;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
return { messages, fileOps, totalTokens };
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
// ============================================================================
|
| 250 |
+
// Summary Generation
|
| 251 |
+
// ============================================================================
|
| 252 |
+
|
| 253 |
+
const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
|
| 254 |
+
Summary of that exploration:
|
| 255 |
+
|
| 256 |
+
`;
|
| 257 |
+
|
| 258 |
+
const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
|
| 259 |
+
|
| 260 |
+
Use this EXACT format:
|
| 261 |
+
|
| 262 |
+
## Goal
|
| 263 |
+
[What was the user trying to accomplish in this branch?]
|
| 264 |
+
|
| 265 |
+
## Constraints & Preferences
|
| 266 |
+
- [Any constraints, preferences, or requirements mentioned]
|
| 267 |
+
- [Or "(none)" if none were mentioned]
|
| 268 |
+
|
| 269 |
+
## Progress
|
| 270 |
+
### Done
|
| 271 |
+
- [x] [Completed tasks/changes]
|
| 272 |
+
|
| 273 |
+
### In Progress
|
| 274 |
+
- [ ] [Work that was started but not finished]
|
| 275 |
+
|
| 276 |
+
### Blocked
|
| 277 |
+
- [Issues preventing progress, if any]
|
| 278 |
+
|
| 279 |
+
## Key Decisions
|
| 280 |
+
- **[Decision]**: [Brief rationale]
|
| 281 |
+
|
| 282 |
+
## Next Steps
|
| 283 |
+
1. [What should happen next to continue this work]
|
| 284 |
+
|
| 285 |
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
| 286 |
+
|
| 287 |
+
/**
|
| 288 |
+
* Generate a summary of abandoned branch entries.
|
| 289 |
+
*
|
| 290 |
+
* @param entries - Session entries to summarize (chronological order)
|
| 291 |
+
* @param options - Generation options
|
| 292 |
+
*/
|
| 293 |
+
export async function generateBranchSummary(
|
| 294 |
+
entries: SessionEntry[],
|
| 295 |
+
options: GenerateBranchSummaryOptions,
|
| 296 |
+
): Promise<BranchSummaryResult> {
|
| 297 |
+
const {
|
| 298 |
+
model,
|
| 299 |
+
apiKey,
|
| 300 |
+
headers,
|
| 301 |
+
env,
|
| 302 |
+
signal,
|
| 303 |
+
customInstructions,
|
| 304 |
+
replaceInstructions,
|
| 305 |
+
reserveTokens = 16384,
|
| 306 |
+
streamFn,
|
| 307 |
+
retry,
|
| 308 |
+
callbacks,
|
| 309 |
+
} = options;
|
| 310 |
+
|
| 311 |
+
// Token budget = context window minus reserved space for prompt + response
|
| 312 |
+
const contextWindow = model.contextWindow || 128000;
|
| 313 |
+
const tokenBudget = contextWindow - reserveTokens;
|
| 314 |
+
|
| 315 |
+
const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);
|
| 316 |
+
|
| 317 |
+
if (messages.length === 0) {
|
| 318 |
+
return { summary: "No content to summarize" };
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Transform to LLM-compatible messages, then serialize to text
|
| 322 |
+
// Serialization prevents the model from treating it as a conversation to continue
|
| 323 |
+
const llmMessages = convertToLlm(messages);
|
| 324 |
+
const conversationText = serializeConversation(llmMessages);
|
| 325 |
+
|
| 326 |
+
// Build prompt
|
| 327 |
+
let instructions: string;
|
| 328 |
+
if (replaceInstructions && customInstructions) {
|
| 329 |
+
instructions = customInstructions;
|
| 330 |
+
} else if (customInstructions) {
|
| 331 |
+
instructions = `${BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: ${customInstructions}`;
|
| 332 |
+
} else {
|
| 333 |
+
instructions = BRANCH_SUMMARY_PROMPT;
|
| 334 |
+
}
|
| 335 |
+
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${instructions}`;
|
| 336 |
+
|
| 337 |
+
const summarizationMessages = [
|
| 338 |
+
{
|
| 339 |
+
role: "user" as const,
|
| 340 |
+
content: [{ type: "text" as const, text: promptText }],
|
| 341 |
+
timestamp: Date.now(),
|
| 342 |
+
},
|
| 343 |
+
];
|
| 344 |
+
|
| 345 |
+
const maxTokens = Math.min(4096, model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
| 346 |
+
|
| 347 |
+
// Call LLM for summarization. Prefer the session stream function so SDK
|
| 348 |
+
// request behavior (timeouts, retries, attribution headers) stays consistent
|
| 349 |
+
// without running through agent state/events. Retried via completeSummarization
|
| 350 |
+
// so transient stream drops reuse the configured retry policy.
|
| 351 |
+
const context = { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages };
|
| 352 |
+
const requestOptions: SimpleStreamOptions = { apiKey, headers, env, signal, maxTokens };
|
| 353 |
+
const response = await completeSummarization(model, context, requestOptions, streamFn, retry, callbacks);
|
| 354 |
+
|
| 355 |
+
// Check if aborted or errored
|
| 356 |
+
if (response.stopReason === "aborted") {
|
| 357 |
+
return { aborted: true };
|
| 358 |
+
}
|
| 359 |
+
const failure = getSummarizationFailure(response, "Branch summarization");
|
| 360 |
+
if (failure) {
|
| 361 |
+
return { error: failure };
|
| 362 |
+
}
|
| 363 |
+
if (response.content.some((block) => block.type === "toolCall")) {
|
| 364 |
+
return { error: "Branch summarization attempted to call a tool" };
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
let summary = contentText(response.content);
|
| 368 |
+
|
| 369 |
+
// Prepend preamble to provide context about the branch summary
|
| 370 |
+
summary = BRANCH_SUMMARY_PREAMBLE + summary;
|
| 371 |
+
|
| 372 |
+
// Compute file lists and append to summary
|
| 373 |
+
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
| 374 |
+
summary += formatFileOperations(readFiles, modifiedFiles);
|
| 375 |
+
|
| 376 |
+
return {
|
| 377 |
+
summary: summary || "No summary generated",
|
| 378 |
+
usage: response.usage,
|
| 379 |
+
readFiles,
|
| 380 |
+
modifiedFiles,
|
| 381 |
+
};
|
| 382 |
+
}
|
packages/coding-agent/src/core/compaction/compaction.ts
ADDED
|
@@ -0,0 +1,1012 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Context compaction for long sessions.
|
| 3 |
+
*
|
| 4 |
+
* Pure functions for compaction logic. The session manager handles I/O,
|
| 5 |
+
* and after compaction the session is reloaded.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type { AgentMessage, StreamFn, ThinkingLevel } from "@earendil-works/pi-agent-core";
|
| 9 |
+
import { contentText, type RetryCallbacks, type RetryPolicy, retryAssistantCall, uuidv7 } from "@earendil-works/pi-ai";
|
| 10 |
+
import type { AssistantMessage, Context, Model, SimpleStreamOptions, Usage } from "@earendil-works/pi-ai/compat";
|
| 11 |
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
| 12 |
+
import { convertToLlm } from "../messages.ts";
|
| 13 |
+
import {
|
| 14 |
+
buildSessionContext,
|
| 15 |
+
type CompactionEntry,
|
| 16 |
+
type SessionEntry,
|
| 17 |
+
sessionEntryToContextMessages,
|
| 18 |
+
} from "../session-manager.ts";
|
| 19 |
+
import {
|
| 20 |
+
computeFileLists,
|
| 21 |
+
createFileOps,
|
| 22 |
+
extractFileOpsFromMessage,
|
| 23 |
+
type FileOperations,
|
| 24 |
+
formatFileOperations,
|
| 25 |
+
SUMMARIZATION_SYSTEM_PROMPT,
|
| 26 |
+
serializeConversation,
|
| 27 |
+
} from "./utils.ts";
|
| 28 |
+
|
| 29 |
+
// ============================================================================
|
| 30 |
+
// File Operation Tracking
|
| 31 |
+
// ============================================================================
|
| 32 |
+
|
| 33 |
+
/** Details stored in CompactionEntry.details for file tracking */
|
| 34 |
+
export interface CompactionDetails {
|
| 35 |
+
readFiles: string[];
|
| 36 |
+
modifiedFiles: string[];
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Extract file operations from messages and previous compaction entries.
|
| 41 |
+
*/
|
| 42 |
+
function extractFileOperations(
|
| 43 |
+
messages: AgentMessage[],
|
| 44 |
+
entries: SessionEntry[],
|
| 45 |
+
prevCompactionIndex: number,
|
| 46 |
+
): FileOperations {
|
| 47 |
+
const fileOps = createFileOps();
|
| 48 |
+
|
| 49 |
+
// Collect from previous compaction's details (if pi-generated)
|
| 50 |
+
if (prevCompactionIndex >= 0) {
|
| 51 |
+
const prevCompaction = entries[prevCompactionIndex] as CompactionEntry;
|
| 52 |
+
if (!prevCompaction.fromHook && prevCompaction.details) {
|
| 53 |
+
// fromHook field kept for session file compatibility
|
| 54 |
+
const details = prevCompaction.details as CompactionDetails;
|
| 55 |
+
if (Array.isArray(details.readFiles)) {
|
| 56 |
+
for (const f of details.readFiles) fileOps.read.add(f);
|
| 57 |
+
}
|
| 58 |
+
if (Array.isArray(details.modifiedFiles)) {
|
| 59 |
+
for (const f of details.modifiedFiles) fileOps.edited.add(f);
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
// Extract from tool calls in messages
|
| 65 |
+
for (const msg of messages) {
|
| 66 |
+
extractFileOpsFromMessage(msg, fileOps);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
return fileOps;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// ============================================================================
|
| 73 |
+
// Message Extraction
|
| 74 |
+
// ============================================================================
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* Extract AgentMessage from an entry if it produces one.
|
| 78 |
+
* Returns undefined for entries that don't contribute to LLM context.
|
| 79 |
+
*/
|
| 80 |
+
function getMessageFromEntryForCompaction(entry: SessionEntry): AgentMessage | undefined {
|
| 81 |
+
if (entry.type === "compaction") {
|
| 82 |
+
return undefined;
|
| 83 |
+
}
|
| 84 |
+
return sessionEntryToContextMessages(entry)[0];
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
/** Result from compact() - SessionManager adds uuid/parentUuid when saving */
|
| 88 |
+
export interface CompactionResult<T = unknown> {
|
| 89 |
+
summary: string;
|
| 90 |
+
firstKeptEntryId: string;
|
| 91 |
+
tokensBefore: number;
|
| 92 |
+
estimatedTokensAfter?: number;
|
| 93 |
+
/** Usage from the LLM call(s) that generated this summary, if available */
|
| 94 |
+
usage?: Usage;
|
| 95 |
+
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
| 96 |
+
details?: T;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
function combineUsage(first: Usage, second: Usage): Usage {
|
| 100 |
+
return {
|
| 101 |
+
input: first.input + second.input,
|
| 102 |
+
output: first.output + second.output,
|
| 103 |
+
cacheRead: first.cacheRead + second.cacheRead,
|
| 104 |
+
cacheWrite: first.cacheWrite + second.cacheWrite,
|
| 105 |
+
...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
|
| 106 |
+
? { cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) }
|
| 107 |
+
: {}),
|
| 108 |
+
...(first.reasoning !== undefined || second.reasoning !== undefined
|
| 109 |
+
? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
|
| 110 |
+
: {}),
|
| 111 |
+
totalTokens: first.totalTokens + second.totalTokens,
|
| 112 |
+
cost: {
|
| 113 |
+
input: first.cost.input + second.cost.input,
|
| 114 |
+
output: first.cost.output + second.cost.output,
|
| 115 |
+
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
| 116 |
+
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
| 117 |
+
total: first.cost.total + second.cost.total,
|
| 118 |
+
},
|
| 119 |
+
};
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
// ============================================================================
|
| 123 |
+
// Types
|
| 124 |
+
// ============================================================================
|
| 125 |
+
|
| 126 |
+
export interface CompactionSettings {
|
| 127 |
+
enabled: boolean;
|
| 128 |
+
reserveTokens: number;
|
| 129 |
+
keepRecentTokens: number;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
export const DEFAULT_COMPACTION_SETTINGS: CompactionSettings = {
|
| 133 |
+
enabled: true,
|
| 134 |
+
reserveTokens: 16384,
|
| 135 |
+
keepRecentTokens: 20000,
|
| 136 |
+
};
|
| 137 |
+
|
| 138 |
+
// ============================================================================
|
| 139 |
+
// Token calculation
|
| 140 |
+
// ============================================================================
|
| 141 |
+
|
| 142 |
+
/**
|
| 143 |
+
* Calculate total context tokens from usage.
|
| 144 |
+
* Uses the native totalTokens field when available, falls back to computing from components.
|
| 145 |
+
*/
|
| 146 |
+
export function calculateContextTokens(usage: Usage): number {
|
| 147 |
+
return usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Get usage from an assistant message if available.
|
| 152 |
+
* Skips aborted, error, and all-zero usage messages as they don't have valid usage data.
|
| 153 |
+
*/
|
| 154 |
+
function getAssistantUsage(msg: AgentMessage): Usage | undefined {
|
| 155 |
+
if (msg.role === "assistant" && "usage" in msg) {
|
| 156 |
+
const assistantMsg = msg as AssistantMessage;
|
| 157 |
+
if (
|
| 158 |
+
assistantMsg.stopReason !== "aborted" &&
|
| 159 |
+
assistantMsg.stopReason !== "error" &&
|
| 160 |
+
assistantMsg.usage &&
|
| 161 |
+
calculateContextTokens(assistantMsg.usage) > 0
|
| 162 |
+
) {
|
| 163 |
+
return assistantMsg.usage;
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
return undefined;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
/**
|
| 170 |
+
* Find the last valid assistant message usage from session entries.
|
| 171 |
+
*/
|
| 172 |
+
export function getLastAssistantUsage(entries: SessionEntry[]): Usage | undefined {
|
| 173 |
+
for (let i = entries.length - 1; i >= 0; i--) {
|
| 174 |
+
const entry = entries[i];
|
| 175 |
+
if (entry.type === "message") {
|
| 176 |
+
const usage = getAssistantUsage(entry.message);
|
| 177 |
+
if (usage) return usage;
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
return undefined;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
export interface ContextUsageEstimate {
|
| 184 |
+
tokens: number;
|
| 185 |
+
usageTokens: number;
|
| 186 |
+
trailingTokens: number;
|
| 187 |
+
lastUsageIndex: number | null;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
function getLastAssistantUsageInfo(messages: AgentMessage[]): { usage: Usage; index: number } | undefined {
|
| 191 |
+
for (let i = messages.length - 1; i >= 0; i--) {
|
| 192 |
+
const usage = getAssistantUsage(messages[i]);
|
| 193 |
+
if (usage) return { usage, index: i };
|
| 194 |
+
}
|
| 195 |
+
return undefined;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/**
|
| 199 |
+
* Estimate context tokens from messages, using the last assistant usage when available.
|
| 200 |
+
* If there are messages after the last usage, estimate their tokens with estimateTokens.
|
| 201 |
+
*/
|
| 202 |
+
export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEstimate {
|
| 203 |
+
const usageInfo = getLastAssistantUsageInfo(messages);
|
| 204 |
+
|
| 205 |
+
if (!usageInfo) {
|
| 206 |
+
let estimated = 0;
|
| 207 |
+
for (const message of messages) {
|
| 208 |
+
estimated += estimateTokens(message);
|
| 209 |
+
}
|
| 210 |
+
return {
|
| 211 |
+
tokens: estimated,
|
| 212 |
+
usageTokens: 0,
|
| 213 |
+
trailingTokens: estimated,
|
| 214 |
+
lastUsageIndex: null,
|
| 215 |
+
};
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
const usageTokens = calculateContextTokens(usageInfo.usage);
|
| 219 |
+
let trailingTokens = 0;
|
| 220 |
+
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
| 221 |
+
trailingTokens += estimateTokens(messages[i]);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return {
|
| 225 |
+
tokens: usageTokens + trailingTokens,
|
| 226 |
+
usageTokens,
|
| 227 |
+
trailingTokens,
|
| 228 |
+
lastUsageIndex: usageInfo.index,
|
| 229 |
+
};
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Check if compaction should trigger based on context usage.
|
| 234 |
+
*/
|
| 235 |
+
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
|
| 236 |
+
if (!settings.enabled) return false;
|
| 237 |
+
return contextTokens > contextWindow - settings.reserveTokens;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
// ============================================================================
|
| 241 |
+
// Cut point detection
|
| 242 |
+
// ============================================================================
|
| 243 |
+
|
| 244 |
+
const ESTIMATED_IMAGE_CHARS = 4800;
|
| 245 |
+
|
| 246 |
+
function estimateTextAndImageContentChars(content: string | Array<{ type: string; text?: string }>): number {
|
| 247 |
+
if (typeof content === "string") {
|
| 248 |
+
return content.length;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
let chars = 0;
|
| 252 |
+
for (const block of content) {
|
| 253 |
+
if (block.type === "text" && block.text) {
|
| 254 |
+
chars += block.text.length;
|
| 255 |
+
} else if (block.type === "image") {
|
| 256 |
+
chars += ESTIMATED_IMAGE_CHARS;
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
return chars;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/**
|
| 263 |
+
* Estimate token count for a message using chars/4 heuristic.
|
| 264 |
+
* This is conservative (overestimates tokens).
|
| 265 |
+
*/
|
| 266 |
+
export function estimateTokens(message: AgentMessage): number {
|
| 267 |
+
let chars = 0;
|
| 268 |
+
|
| 269 |
+
switch (message.role) {
|
| 270 |
+
case "user": {
|
| 271 |
+
chars = estimateTextAndImageContentChars(
|
| 272 |
+
(message as { content: string | Array<{ type: string; text?: string }> }).content,
|
| 273 |
+
);
|
| 274 |
+
return Math.ceil(chars / 4);
|
| 275 |
+
}
|
| 276 |
+
case "assistant": {
|
| 277 |
+
const assistant = message as AssistantMessage;
|
| 278 |
+
for (const block of assistant.content) {
|
| 279 |
+
if (block.type === "text") {
|
| 280 |
+
chars += block.text.length;
|
| 281 |
+
} else if (block.type === "thinking") {
|
| 282 |
+
chars += block.thinking.length;
|
| 283 |
+
} else if (block.type === "toolCall") {
|
| 284 |
+
chars += block.name.length + JSON.stringify(block.arguments).length;
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
return Math.ceil(chars / 4);
|
| 288 |
+
}
|
| 289 |
+
case "custom":
|
| 290 |
+
case "toolResult": {
|
| 291 |
+
chars = estimateTextAndImageContentChars(message.content);
|
| 292 |
+
return Math.ceil(chars / 4);
|
| 293 |
+
}
|
| 294 |
+
case "bashExecution": {
|
| 295 |
+
chars = message.command.length + message.output.length;
|
| 296 |
+
return Math.ceil(chars / 4);
|
| 297 |
+
}
|
| 298 |
+
case "branchSummary":
|
| 299 |
+
case "compactionSummary": {
|
| 300 |
+
chars = message.summary.length;
|
| 301 |
+
return Math.ceil(chars / 4);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
return 0;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
function isCutPointMessage(message: AgentMessage): boolean {
|
| 309 |
+
switch (message.role) {
|
| 310 |
+
case "user":
|
| 311 |
+
case "assistant":
|
| 312 |
+
case "bashExecution":
|
| 313 |
+
case "custom":
|
| 314 |
+
case "branchSummary":
|
| 315 |
+
case "compactionSummary":
|
| 316 |
+
return true;
|
| 317 |
+
case "toolResult":
|
| 318 |
+
return false;
|
| 319 |
+
}
|
| 320 |
+
return false;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
function isTurnStartMessage(message: AgentMessage): boolean {
|
| 324 |
+
switch (message.role) {
|
| 325 |
+
case "user":
|
| 326 |
+
case "bashExecution":
|
| 327 |
+
case "custom":
|
| 328 |
+
case "branchSummary":
|
| 329 |
+
case "compactionSummary":
|
| 330 |
+
return true;
|
| 331 |
+
case "assistant":
|
| 332 |
+
case "toolResult":
|
| 333 |
+
return false;
|
| 334 |
+
}
|
| 335 |
+
return false;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function isTurnStartEntry(entry: SessionEntry): boolean {
|
| 339 |
+
if (entry.type === "compaction") {
|
| 340 |
+
return false;
|
| 341 |
+
}
|
| 342 |
+
return sessionEntryToContextMessages(entry).some(isTurnStartMessage);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
/**
|
| 346 |
+
* Find valid cut points: indices of context-visible user-like or assistant messages.
|
| 347 |
+
* Never cut at tool results (they must follow their tool call).
|
| 348 |
+
* When we cut at an assistant message with tool calls, its tool results follow it
|
| 349 |
+
* and will be kept.
|
| 350 |
+
*/
|
| 351 |
+
function findValidCutPoints(entries: SessionEntry[], startIndex: number, endIndex: number): number[] {
|
| 352 |
+
const cutPoints: number[] = [];
|
| 353 |
+
for (let i = startIndex; i < endIndex; i++) {
|
| 354 |
+
const entry = entries[i];
|
| 355 |
+
if (entry.type === "compaction") {
|
| 356 |
+
continue;
|
| 357 |
+
}
|
| 358 |
+
if (sessionEntryToContextMessages(entry).some(isCutPointMessage)) {
|
| 359 |
+
cutPoints.push(i);
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
return cutPoints;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/**
|
| 366 |
+
* Find the context-visible user-role message that starts the turn containing the given entry index.
|
| 367 |
+
* Returns -1 if no turn start found before the index.
|
| 368 |
+
*/
|
| 369 |
+
export function findTurnStartIndex(entries: SessionEntry[], entryIndex: number, startIndex: number): number {
|
| 370 |
+
for (let i = entryIndex; i >= startIndex; i--) {
|
| 371 |
+
if (isTurnStartEntry(entries[i])) {
|
| 372 |
+
return i;
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
return -1;
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
export interface CutPointResult {
|
| 379 |
+
/** Index of first entry to keep */
|
| 380 |
+
firstKeptEntryIndex: number;
|
| 381 |
+
/** Index of user message that starts the turn being split, or -1 if not splitting */
|
| 382 |
+
turnStartIndex: number;
|
| 383 |
+
/** Whether this cut splits a turn (cut point is not a user message) */
|
| 384 |
+
isSplitTurn: boolean;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
/**
|
| 388 |
+
* Find the cut point in session entries that keeps approximately `keepRecentTokens`.
|
| 389 |
+
*
|
| 390 |
+
* Algorithm: Walk backwards from newest, accumulating estimated message sizes.
|
| 391 |
+
* Stop when we've accumulated >= keepRecentTokens. Cut at that point.
|
| 392 |
+
*
|
| 393 |
+
* Can cut at user OR assistant messages (never tool results). When cutting at an
|
| 394 |
+
* assistant message with tool calls, its tool results come after and will be kept.
|
| 395 |
+
*
|
| 396 |
+
* Returns CutPointResult with:
|
| 397 |
+
* - firstKeptEntryIndex: the entry index to start keeping from
|
| 398 |
+
* - turnStartIndex: if cutting mid-turn, the user message that started that turn
|
| 399 |
+
* - isSplitTurn: whether we're cutting in the middle of a turn
|
| 400 |
+
*
|
| 401 |
+
* Only considers entries between `startIndex` and `endIndex` (exclusive).
|
| 402 |
+
*/
|
| 403 |
+
export function findCutPoint(
|
| 404 |
+
entries: SessionEntry[],
|
| 405 |
+
startIndex: number,
|
| 406 |
+
endIndex: number,
|
| 407 |
+
keepRecentTokens: number,
|
| 408 |
+
): CutPointResult {
|
| 409 |
+
const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
|
| 410 |
+
|
| 411 |
+
if (cutPoints.length === 0) {
|
| 412 |
+
return { firstKeptEntryIndex: startIndex, turnStartIndex: -1, isSplitTurn: false };
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// Walk backwards from newest, accumulating estimated message sizes
|
| 416 |
+
let accumulatedTokens = 0;
|
| 417 |
+
let cutIndex = cutPoints[0]; // Default: keep from first message (not header)
|
| 418 |
+
|
| 419 |
+
for (let i = endIndex - 1; i >= startIndex; i--) {
|
| 420 |
+
const entry = entries[i];
|
| 421 |
+
const messageTokens = sessionEntryToContextMessages(entry).reduce(
|
| 422 |
+
(sum, message) => sum + estimateTokens(message),
|
| 423 |
+
0,
|
| 424 |
+
);
|
| 425 |
+
if (messageTokens === 0) continue;
|
| 426 |
+
accumulatedTokens += messageTokens;
|
| 427 |
+
|
| 428 |
+
// Check if we've exceeded the budget
|
| 429 |
+
if (accumulatedTokens >= keepRecentTokens) {
|
| 430 |
+
// Find the closest valid cut point at or after this entry
|
| 431 |
+
for (let c = 0; c < cutPoints.length; c++) {
|
| 432 |
+
if (cutPoints[c] >= i) {
|
| 433 |
+
cutIndex = cutPoints[c];
|
| 434 |
+
break;
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
break;
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
// Scan backwards from cutIndex to include adjacent metadata entries that do not affect context.
|
| 442 |
+
while (cutIndex > startIndex) {
|
| 443 |
+
const prevEntry = entries[cutIndex - 1];
|
| 444 |
+
// Stop at compaction boundaries or context-visible entries.
|
| 445 |
+
if (prevEntry.type === "compaction" || sessionEntryToContextMessages(prevEntry).length > 0) {
|
| 446 |
+
break;
|
| 447 |
+
}
|
| 448 |
+
cutIndex--;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// Determine if this is a split turn
|
| 452 |
+
const cutEntry = entries[cutIndex];
|
| 453 |
+
const startsTurn = isTurnStartEntry(cutEntry);
|
| 454 |
+
const turnStartIndex = startsTurn ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
|
| 455 |
+
|
| 456 |
+
return {
|
| 457 |
+
firstKeptEntryIndex: cutIndex,
|
| 458 |
+
turnStartIndex,
|
| 459 |
+
isSplitTurn: !startsTurn && turnStartIndex !== -1,
|
| 460 |
+
};
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
// ============================================================================
|
| 464 |
+
// Summarization
|
| 465 |
+
// ============================================================================
|
| 466 |
+
|
| 467 |
+
const SUMMARIZATION_PROMPT = `The messages above are a conversation to summarize. Create a structured context checkpoint summary that another LLM will use to continue the work.
|
| 468 |
+
|
| 469 |
+
Use this EXACT format:
|
| 470 |
+
|
| 471 |
+
## Goal
|
| 472 |
+
[What is the user trying to accomplish? Can be multiple items if the session covers different tasks.]
|
| 473 |
+
|
| 474 |
+
## Constraints & Preferences
|
| 475 |
+
- [Any constraints, preferences, or requirements mentioned by user]
|
| 476 |
+
- [Or "(none)" if none were mentioned]
|
| 477 |
+
|
| 478 |
+
## Progress
|
| 479 |
+
### Done
|
| 480 |
+
- [x] [Completed tasks/changes]
|
| 481 |
+
|
| 482 |
+
### In Progress
|
| 483 |
+
- [ ] [Current work]
|
| 484 |
+
|
| 485 |
+
### Blocked
|
| 486 |
+
- [Issues preventing progress, if any]
|
| 487 |
+
|
| 488 |
+
## Key Decisions
|
| 489 |
+
- **[Decision]**: [Brief rationale]
|
| 490 |
+
|
| 491 |
+
## Next Steps
|
| 492 |
+
1. [Ordered list of what should happen next]
|
| 493 |
+
|
| 494 |
+
## Critical Context
|
| 495 |
+
- [Any data, examples, or references needed to continue]
|
| 496 |
+
- [Or "(none)" if not applicable]
|
| 497 |
+
|
| 498 |
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
| 499 |
+
|
| 500 |
+
const UPDATE_SUMMARIZATION_INSTRUCTIONS = `Update the existing structured summary with new information. RULES:
|
| 501 |
+
- PRESERVE all existing information from the previous summary
|
| 502 |
+
- ADD new progress, decisions, and context from the new messages
|
| 503 |
+
- UPDATE the Progress section: move items from "In Progress" to "Done" when completed
|
| 504 |
+
- UPDATE "Next Steps" based on what was accomplished
|
| 505 |
+
- PRESERVE exact file paths, function names, and error messages
|
| 506 |
+
- If something is no longer relevant, you may remove it
|
| 507 |
+
|
| 508 |
+
Use this EXACT format:
|
| 509 |
+
|
| 510 |
+
## Goal
|
| 511 |
+
[Preserve existing goals, add new ones if the task expanded]
|
| 512 |
+
|
| 513 |
+
## Constraints & Preferences
|
| 514 |
+
- [Preserve existing, add new ones discovered]
|
| 515 |
+
|
| 516 |
+
## Progress
|
| 517 |
+
### Done
|
| 518 |
+
- [x] [Include previously done items AND newly completed items]
|
| 519 |
+
|
| 520 |
+
### In Progress
|
| 521 |
+
- [ ] [Current work - update based on progress]
|
| 522 |
+
|
| 523 |
+
### Blocked
|
| 524 |
+
- [Current blockers - remove if resolved]
|
| 525 |
+
|
| 526 |
+
## Key Decisions
|
| 527 |
+
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
| 528 |
+
|
| 529 |
+
## Next Steps
|
| 530 |
+
1. [Update based on current state]
|
| 531 |
+
|
| 532 |
+
## Critical Context
|
| 533 |
+
- [Preserve important context, add new if needed]
|
| 534 |
+
|
| 535 |
+
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
| 536 |
+
|
| 537 |
+
const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
|
| 538 |
+
|
| 539 |
+
${UPDATE_SUMMARIZATION_INSTRUCTIONS}`;
|
| 540 |
+
|
| 541 |
+
/**
|
| 542 |
+
* Returns an error message when a summarization response cannot safely be persisted.
|
| 543 |
+
* A length stop contains partial text and must not become a session checkpoint.
|
| 544 |
+
*/
|
| 545 |
+
export function getSummarizationFailure(response: AssistantMessage, label: string): string | undefined {
|
| 546 |
+
if (response.stopReason === "error") {
|
| 547 |
+
return `${label} failed: ${response.errorMessage || "Unknown error"}`;
|
| 548 |
+
}
|
| 549 |
+
if (response.stopReason === "length") {
|
| 550 |
+
return `${label} failed: generation hit the token cap and the summary is incomplete`;
|
| 551 |
+
}
|
| 552 |
+
return undefined;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
function createSummarizationOptions(
|
| 556 |
+
model: Model<any>,
|
| 557 |
+
maxTokens: number,
|
| 558 |
+
apiKey: string | undefined,
|
| 559 |
+
headers: Record<string, string> | undefined,
|
| 560 |
+
env: Record<string, string> | undefined,
|
| 561 |
+
signal: AbortSignal | undefined,
|
| 562 |
+
thinkingLevel: ThinkingLevel | undefined,
|
| 563 |
+
sessionId: string | undefined,
|
| 564 |
+
): SimpleStreamOptions {
|
| 565 |
+
const options: SimpleStreamOptions = { maxTokens, signal, apiKey, headers, env, sessionId };
|
| 566 |
+
if (model.reasoning && thinkingLevel && thinkingLevel !== "off") {
|
| 567 |
+
options.reasoning = thinkingLevel;
|
| 568 |
+
}
|
| 569 |
+
return options;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
/**
|
| 573 |
+
* Shared choke point for every compaction/branch-summary summarization call. Wraps the
|
| 574 |
+
* single LLM call in {@link retryAssistantCall} so transient stream drops (e.g.
|
| 575 |
+
* `terminated`, socket close) honor the configured retry policy instead of failing
|
| 576 |
+
* the whole compaction on the first attempt. Deterministic errors and aborts return
|
| 577 |
+
* immediately (see {@link retryAssistantCall}).
|
| 578 |
+
*/
|
| 579 |
+
export async function completeSummarization(
|
| 580 |
+
model: Model<any>,
|
| 581 |
+
context: Context,
|
| 582 |
+
options: SimpleStreamOptions,
|
| 583 |
+
streamFn?: StreamFn,
|
| 584 |
+
retry?: RetryPolicy,
|
| 585 |
+
callbacks?: RetryCallbacks,
|
| 586 |
+
): Promise<AssistantMessage> {
|
| 587 |
+
// Avoid cache writes for one-off summaries. Reuse caller-supplied routing when available;
|
| 588 |
+
// callers without a session ID, including branch summaries, receive a fresh routing ID.
|
| 589 |
+
const requestOptions: SimpleStreamOptions = {
|
| 590 |
+
...options,
|
| 591 |
+
cacheRetention: "none",
|
| 592 |
+
sessionId: options.sessionId ?? uuidv7(),
|
| 593 |
+
};
|
| 594 |
+
const produce = async (): Promise<AssistantMessage> =>
|
| 595 |
+
streamFn
|
| 596 |
+
? (await streamFn(model, context, requestOptions)).result()
|
| 597 |
+
: completeSimple(model, context, requestOptions);
|
| 598 |
+
return retryAssistantCall(produce, retry, requestOptions.signal, callbacks);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
/**
|
| 602 |
+
* Generate a summary of the conversation using the LLM.
|
| 603 |
+
* If previousSummary is provided, uses the update prompt to merge.
|
| 604 |
+
*/
|
| 605 |
+
export async function generateSummary(
|
| 606 |
+
currentMessages: AgentMessage[],
|
| 607 |
+
model: Model<any>,
|
| 608 |
+
reserveTokens: number,
|
| 609 |
+
apiKey: string | undefined,
|
| 610 |
+
headers?: Record<string, string>,
|
| 611 |
+
signal?: AbortSignal,
|
| 612 |
+
customInstructions?: string,
|
| 613 |
+
previousSummary?: string,
|
| 614 |
+
thinkingLevel?: ThinkingLevel,
|
| 615 |
+
streamFn?: StreamFn,
|
| 616 |
+
env?: Record<string, string>,
|
| 617 |
+
retry?: RetryPolicy,
|
| 618 |
+
callbacks?: RetryCallbacks,
|
| 619 |
+
sessionId?: string,
|
| 620 |
+
): Promise<string> {
|
| 621 |
+
return (
|
| 622 |
+
await generateSummaryWithUsage(
|
| 623 |
+
currentMessages,
|
| 624 |
+
model,
|
| 625 |
+
reserveTokens,
|
| 626 |
+
apiKey,
|
| 627 |
+
headers,
|
| 628 |
+
signal,
|
| 629 |
+
customInstructions,
|
| 630 |
+
previousSummary,
|
| 631 |
+
thinkingLevel,
|
| 632 |
+
streamFn,
|
| 633 |
+
env,
|
| 634 |
+
retry,
|
| 635 |
+
callbacks,
|
| 636 |
+
sessionId,
|
| 637 |
+
)
|
| 638 |
+
).text;
|
| 639 |
+
}
|
| 640 |
+
|
| 641 |
+
/** Build the provider context for a standalone summary request. */
|
| 642 |
+
function buildSummarizationContext(promptText: string): Context {
|
| 643 |
+
return {
|
| 644 |
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
| 645 |
+
messages: [
|
| 646 |
+
{
|
| 647 |
+
role: "user",
|
| 648 |
+
content: [{ type: "text", text: promptText }],
|
| 649 |
+
timestamp: Date.now(),
|
| 650 |
+
},
|
| 651 |
+
],
|
| 652 |
+
};
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
/** Generate or update a conversation summary and return its provider usage. */
|
| 656 |
+
export async function generateSummaryWithUsage(
|
| 657 |
+
currentMessages: AgentMessage[],
|
| 658 |
+
model: Model<any>,
|
| 659 |
+
reserveTokens: number,
|
| 660 |
+
apiKey: string | undefined,
|
| 661 |
+
headers?: Record<string, string>,
|
| 662 |
+
signal?: AbortSignal,
|
| 663 |
+
customInstructions?: string,
|
| 664 |
+
previousSummary?: string,
|
| 665 |
+
thinkingLevel?: ThinkingLevel,
|
| 666 |
+
streamFn?: StreamFn,
|
| 667 |
+
env?: Record<string, string>,
|
| 668 |
+
retry?: RetryPolicy,
|
| 669 |
+
callbacks?: RetryCallbacks,
|
| 670 |
+
sessionId?: string,
|
| 671 |
+
): Promise<{ text: string; usage: Usage }> {
|
| 672 |
+
const maxTokens = Math.min(
|
| 673 |
+
Math.floor(0.8 * reserveTokens),
|
| 674 |
+
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
| 675 |
+
);
|
| 676 |
+
|
| 677 |
+
// Use update prompt if we have a previous summary, otherwise initial prompt
|
| 678 |
+
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
| 679 |
+
if (customInstructions) {
|
| 680 |
+
basePrompt = `${basePrompt}\n\nAdditional focus: ${customInstructions}`;
|
| 681 |
+
}
|
| 682 |
+
|
| 683 |
+
// Serialize conversation to text so model doesn't try to continue it
|
| 684 |
+
// Convert to LLM messages first (handles custom types like bashExecution, custom, etc.)
|
| 685 |
+
const llmMessages = convertToLlm(currentMessages);
|
| 686 |
+
const conversationText = serializeConversation(llmMessages);
|
| 687 |
+
|
| 688 |
+
// Build the prompt with conversation wrapped in tags
|
| 689 |
+
let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;
|
| 690 |
+
if (previousSummary) {
|
| 691 |
+
promptText += `<previous-summary>\n${previousSummary}\n</previous-summary>\n\n`;
|
| 692 |
+
}
|
| 693 |
+
promptText += basePrompt;
|
| 694 |
+
|
| 695 |
+
const completionOptions = createSummarizationOptions(
|
| 696 |
+
model,
|
| 697 |
+
maxTokens,
|
| 698 |
+
apiKey,
|
| 699 |
+
headers,
|
| 700 |
+
env,
|
| 701 |
+
signal,
|
| 702 |
+
thinkingLevel,
|
| 703 |
+
sessionId,
|
| 704 |
+
);
|
| 705 |
+
|
| 706 |
+
const response = await completeSummarization(
|
| 707 |
+
model,
|
| 708 |
+
buildSummarizationContext(promptText),
|
| 709 |
+
completionOptions,
|
| 710 |
+
streamFn,
|
| 711 |
+
retry,
|
| 712 |
+
callbacks,
|
| 713 |
+
);
|
| 714 |
+
|
| 715 |
+
const failure = getSummarizationFailure(response, "Summarization");
|
| 716 |
+
if (failure) {
|
| 717 |
+
throw new Error(failure);
|
| 718 |
+
}
|
| 719 |
+
if (response.content.some((block) => block.type === "toolCall")) {
|
| 720 |
+
throw new Error("Summarization attempted to call a tool");
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
const textContent = contentText(response.content);
|
| 724 |
+
|
| 725 |
+
return { text: textContent, usage: response.usage };
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// ============================================================================
|
| 729 |
+
// Compaction Preparation (for extensions)
|
| 730 |
+
// ============================================================================
|
| 731 |
+
|
| 732 |
+
export interface CompactionPreparation {
|
| 733 |
+
/** UUID of first entry to keep */
|
| 734 |
+
firstKeptEntryId: string;
|
| 735 |
+
/** Messages that will be summarized and discarded */
|
| 736 |
+
messagesToSummarize: AgentMessage[];
|
| 737 |
+
/** Messages that will be turned into turn prefix summary (if splitting) */
|
| 738 |
+
turnPrefixMessages: AgentMessage[];
|
| 739 |
+
/** Whether this is a split turn (cut point in middle of turn) */
|
| 740 |
+
isSplitTurn: boolean;
|
| 741 |
+
tokensBefore: number;
|
| 742 |
+
/** Summary from previous compaction, for iterative update */
|
| 743 |
+
previousSummary?: string;
|
| 744 |
+
/** File operations extracted from messagesToSummarize */
|
| 745 |
+
fileOps: FileOperations;
|
| 746 |
+
/** Compaction settions from settings.jsonl */
|
| 747 |
+
settings: CompactionSettings;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
export function prepareCompaction(
|
| 751 |
+
pathEntries: SessionEntry[],
|
| 752 |
+
settings: CompactionSettings,
|
| 753 |
+
): CompactionPreparation | undefined {
|
| 754 |
+
if (pathEntries.length > 0 && pathEntries[pathEntries.length - 1].type === "compaction") {
|
| 755 |
+
return undefined;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
let prevCompactionIndex = -1;
|
| 759 |
+
for (let i = pathEntries.length - 1; i >= 0; i--) {
|
| 760 |
+
if (pathEntries[i].type === "compaction") {
|
| 761 |
+
prevCompactionIndex = i;
|
| 762 |
+
break;
|
| 763 |
+
}
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
let previousSummary: string | undefined;
|
| 767 |
+
let boundaryStart = 0;
|
| 768 |
+
if (prevCompactionIndex >= 0) {
|
| 769 |
+
const prevCompaction = pathEntries[prevCompactionIndex] as CompactionEntry;
|
| 770 |
+
previousSummary = prevCompaction.summary;
|
| 771 |
+
const firstKeptEntryIndex = pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId);
|
| 772 |
+
boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
|
| 773 |
+
}
|
| 774 |
+
const boundaryEnd = pathEntries.length;
|
| 775 |
+
|
| 776 |
+
const tokensBefore = estimateContextTokens(buildSessionContext(pathEntries).messages).tokens;
|
| 777 |
+
|
| 778 |
+
const cutPoint = findCutPoint(pathEntries, boundaryStart, boundaryEnd, settings.keepRecentTokens);
|
| 779 |
+
|
| 780 |
+
// Get UUID of first kept entry
|
| 781 |
+
const firstKeptEntry = pathEntries[cutPoint.firstKeptEntryIndex];
|
| 782 |
+
if (!firstKeptEntry?.id) {
|
| 783 |
+
return undefined; // Session needs migration
|
| 784 |
+
}
|
| 785 |
+
const firstKeptEntryId = firstKeptEntry.id;
|
| 786 |
+
|
| 787 |
+
const historyEnd = cutPoint.isSplitTurn ? cutPoint.turnStartIndex : cutPoint.firstKeptEntryIndex;
|
| 788 |
+
|
| 789 |
+
// Messages to summarize (will be discarded after summary)
|
| 790 |
+
const messagesToSummarize: AgentMessage[] = [];
|
| 791 |
+
for (let i = boundaryStart; i < historyEnd; i++) {
|
| 792 |
+
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
| 793 |
+
if (msg) messagesToSummarize.push(msg);
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
// Messages for turn prefix summary (if splitting a turn)
|
| 797 |
+
const turnPrefixMessages: AgentMessage[] = [];
|
| 798 |
+
if (cutPoint.isSplitTurn) {
|
| 799 |
+
for (let i = cutPoint.turnStartIndex; i < cutPoint.firstKeptEntryIndex; i++) {
|
| 800 |
+
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
| 801 |
+
if (msg) turnPrefixMessages.push(msg);
|
| 802 |
+
}
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
if (messagesToSummarize.length === 0 && turnPrefixMessages.length === 0) {
|
| 806 |
+
return undefined;
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
// Extract file operations from messages and previous compaction
|
| 810 |
+
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
| 811 |
+
|
| 812 |
+
// Also extract file ops from turn prefix if splitting
|
| 813 |
+
if (cutPoint.isSplitTurn) {
|
| 814 |
+
for (const msg of turnPrefixMessages) {
|
| 815 |
+
extractFileOpsFromMessage(msg, fileOps);
|
| 816 |
+
}
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
return {
|
| 820 |
+
firstKeptEntryId,
|
| 821 |
+
messagesToSummarize,
|
| 822 |
+
turnPrefixMessages,
|
| 823 |
+
isSplitTurn: cutPoint.isSplitTurn,
|
| 824 |
+
tokensBefore,
|
| 825 |
+
previousSummary,
|
| 826 |
+
fileOps,
|
| 827 |
+
settings,
|
| 828 |
+
};
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
// ============================================================================
|
| 832 |
+
// Main compaction function
|
| 833 |
+
// ============================================================================
|
| 834 |
+
|
| 835 |
+
const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
|
| 836 |
+
|
| 837 |
+
Summarize the prefix to provide context for the retained suffix:
|
| 838 |
+
|
| 839 |
+
## Original Request
|
| 840 |
+
[What did the user ask for in this turn?]
|
| 841 |
+
|
| 842 |
+
## Early Progress
|
| 843 |
+
- [Key decisions and work done in the prefix]
|
| 844 |
+
|
| 845 |
+
## Context for Suffix
|
| 846 |
+
- [Information needed to understand the retained recent work]
|
| 847 |
+
|
| 848 |
+
Be concise. Focus on what's needed to understand the kept suffix.`;
|
| 849 |
+
|
| 850 |
+
/**
|
| 851 |
+
* Generate summaries for compaction using prepared data.
|
| 852 |
+
* Returns CompactionResult - SessionManager adds uuid/parentUuid when saving.
|
| 853 |
+
*
|
| 854 |
+
* @param preparation - Pre-calculated preparation from prepareCompaction()
|
| 855 |
+
* @param customInstructions - Optional custom focus for the summary
|
| 856 |
+
* @param sessionId - Optional routing session ID forwarded without enabling prompt caching
|
| 857 |
+
*/
|
| 858 |
+
export async function compact(
|
| 859 |
+
preparation: CompactionPreparation,
|
| 860 |
+
model: Model<any>,
|
| 861 |
+
apiKey: string | undefined,
|
| 862 |
+
headers?: Record<string, string>,
|
| 863 |
+
customInstructions?: string,
|
| 864 |
+
signal?: AbortSignal,
|
| 865 |
+
thinkingLevel?: ThinkingLevel,
|
| 866 |
+
streamFn?: StreamFn,
|
| 867 |
+
env?: Record<string, string>,
|
| 868 |
+
retry?: RetryPolicy,
|
| 869 |
+
callbacks?: RetryCallbacks,
|
| 870 |
+
sessionId?: string,
|
| 871 |
+
): Promise<CompactionResult> {
|
| 872 |
+
const {
|
| 873 |
+
firstKeptEntryId,
|
| 874 |
+
messagesToSummarize,
|
| 875 |
+
turnPrefixMessages,
|
| 876 |
+
isSplitTurn,
|
| 877 |
+
tokensBefore,
|
| 878 |
+
previousSummary,
|
| 879 |
+
fileOps,
|
| 880 |
+
settings,
|
| 881 |
+
} = preparation;
|
| 882 |
+
|
| 883 |
+
// Generate summaries and merge into one
|
| 884 |
+
let summary: string;
|
| 885 |
+
let summaryUsage: Usage;
|
| 886 |
+
|
| 887 |
+
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
| 888 |
+
let historyText = "No prior history.";
|
| 889 |
+
let historyUsage: Usage | undefined;
|
| 890 |
+
if (messagesToSummarize.length > 0) {
|
| 891 |
+
const historyResult = await generateSummaryWithUsage(
|
| 892 |
+
messagesToSummarize,
|
| 893 |
+
model,
|
| 894 |
+
settings.reserveTokens,
|
| 895 |
+
apiKey,
|
| 896 |
+
headers,
|
| 897 |
+
signal,
|
| 898 |
+
customInstructions,
|
| 899 |
+
previousSummary,
|
| 900 |
+
thinkingLevel,
|
| 901 |
+
streamFn,
|
| 902 |
+
env,
|
| 903 |
+
retry,
|
| 904 |
+
callbacks,
|
| 905 |
+
sessionId,
|
| 906 |
+
);
|
| 907 |
+
historyText = historyResult.text;
|
| 908 |
+
historyUsage = historyResult.usage;
|
| 909 |
+
}
|
| 910 |
+
const turnPrefixResult = await generateTurnPrefixSummary(
|
| 911 |
+
turnPrefixMessages,
|
| 912 |
+
model,
|
| 913 |
+
settings.reserveTokens,
|
| 914 |
+
apiKey,
|
| 915 |
+
headers,
|
| 916 |
+
env,
|
| 917 |
+
signal,
|
| 918 |
+
thinkingLevel,
|
| 919 |
+
streamFn,
|
| 920 |
+
retry,
|
| 921 |
+
callbacks,
|
| 922 |
+
sessionId,
|
| 923 |
+
);
|
| 924 |
+
// Merge into single summary
|
| 925 |
+
summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.text}`;
|
| 926 |
+
summaryUsage = historyUsage ? combineUsage(historyUsage, turnPrefixResult.usage) : turnPrefixResult.usage;
|
| 927 |
+
} else {
|
| 928 |
+
// Just generate history summary
|
| 929 |
+
const result = await generateSummaryWithUsage(
|
| 930 |
+
messagesToSummarize,
|
| 931 |
+
model,
|
| 932 |
+
settings.reserveTokens,
|
| 933 |
+
apiKey,
|
| 934 |
+
headers,
|
| 935 |
+
signal,
|
| 936 |
+
customInstructions,
|
| 937 |
+
previousSummary,
|
| 938 |
+
thinkingLevel,
|
| 939 |
+
streamFn,
|
| 940 |
+
env,
|
| 941 |
+
retry,
|
| 942 |
+
callbacks,
|
| 943 |
+
sessionId,
|
| 944 |
+
);
|
| 945 |
+
summary = result.text;
|
| 946 |
+
summaryUsage = result.usage;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
// Compute file lists and append to summary
|
| 950 |
+
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
| 951 |
+
summary += formatFileOperations(readFiles, modifiedFiles);
|
| 952 |
+
|
| 953 |
+
if (!firstKeptEntryId) {
|
| 954 |
+
throw new Error("First kept entry has no UUID - session may need migration");
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
return {
|
| 958 |
+
summary,
|
| 959 |
+
firstKeptEntryId,
|
| 960 |
+
tokensBefore,
|
| 961 |
+
usage: summaryUsage,
|
| 962 |
+
details: { readFiles, modifiedFiles } as CompactionDetails,
|
| 963 |
+
};
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
/**
|
| 967 |
+
* Generate a summary for a turn prefix (when splitting a turn).
|
| 968 |
+
*/
|
| 969 |
+
async function generateTurnPrefixSummary(
|
| 970 |
+
messages: AgentMessage[],
|
| 971 |
+
model: Model<any>,
|
| 972 |
+
reserveTokens: number,
|
| 973 |
+
apiKey: string | undefined,
|
| 974 |
+
headers?: Record<string, string>,
|
| 975 |
+
env?: Record<string, string>,
|
| 976 |
+
signal?: AbortSignal,
|
| 977 |
+
thinkingLevel?: ThinkingLevel,
|
| 978 |
+
streamFn?: StreamFn,
|
| 979 |
+
retry?: RetryPolicy,
|
| 980 |
+
callbacks?: RetryCallbacks,
|
| 981 |
+
sessionId?: string,
|
| 982 |
+
): Promise<{ text: string; usage: Usage }> {
|
| 983 |
+
const maxTokens = Math.min(
|
| 984 |
+
Math.floor(0.5 * reserveTokens),
|
| 985 |
+
model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY,
|
| 986 |
+
); // Smaller budget for turn prefix
|
| 987 |
+
const llmMessages = convertToLlm(messages);
|
| 988 |
+
const conversationText = serializeConversation(llmMessages);
|
| 989 |
+
const promptText = `<conversation>\n${conversationText}\n</conversation>\n\n${TURN_PREFIX_SUMMARIZATION_PROMPT}`;
|
| 990 |
+
|
| 991 |
+
const response = await completeSummarization(
|
| 992 |
+
model,
|
| 993 |
+
buildSummarizationContext(promptText),
|
| 994 |
+
createSummarizationOptions(model, maxTokens, apiKey, headers, env, signal, thinkingLevel, sessionId),
|
| 995 |
+
streamFn,
|
| 996 |
+
retry,
|
| 997 |
+
callbacks,
|
| 998 |
+
);
|
| 999 |
+
|
| 1000 |
+
const failure = getSummarizationFailure(response, "Turn prefix summarization");
|
| 1001 |
+
if (failure) {
|
| 1002 |
+
throw new Error(failure);
|
| 1003 |
+
}
|
| 1004 |
+
if (response.content.some((block) => block.type === "toolCall")) {
|
| 1005 |
+
throw new Error("Turn prefix summarization attempted to call a tool");
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
return {
|
| 1009 |
+
text: contentText(response.content),
|
| 1010 |
+
usage: response.usage,
|
| 1011 |
+
};
|
| 1012 |
+
}
|
packages/coding-agent/src/core/compaction/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Compaction and summarization utilities.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export * from "./branch-summarization.ts";
|
| 6 |
+
export * from "./compaction.ts";
|
| 7 |
+
export * from "./utils.ts";
|
packages/coding-agent/src/core/compaction/utils.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared utilities for compaction and branch summarization.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
| 6 |
+
import { contentText, type Message } from "@earendil-works/pi-ai";
|
| 7 |
+
|
| 8 |
+
// ============================================================================
|
| 9 |
+
// File Operation Tracking
|
| 10 |
+
// ============================================================================
|
| 11 |
+
|
| 12 |
+
export interface FileOperations {
|
| 13 |
+
read: Set<string>;
|
| 14 |
+
written: Set<string>;
|
| 15 |
+
edited: Set<string>;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function createFileOps(): FileOperations {
|
| 19 |
+
return {
|
| 20 |
+
read: new Set(),
|
| 21 |
+
written: new Set(),
|
| 22 |
+
edited: new Set(),
|
| 23 |
+
};
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
/**
|
| 27 |
+
* Extract file operations from tool calls in an assistant message.
|
| 28 |
+
*/
|
| 29 |
+
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
|
| 30 |
+
if (message.role !== "assistant") return;
|
| 31 |
+
if (!("content" in message) || !Array.isArray(message.content)) return;
|
| 32 |
+
|
| 33 |
+
for (const block of message.content) {
|
| 34 |
+
if (typeof block !== "object" || block === null) continue;
|
| 35 |
+
if (!("type" in block) || block.type !== "toolCall") continue;
|
| 36 |
+
if (!("arguments" in block) || !("name" in block)) continue;
|
| 37 |
+
|
| 38 |
+
const args = block.arguments as Record<string, unknown> | undefined;
|
| 39 |
+
if (!args) continue;
|
| 40 |
+
|
| 41 |
+
const path = typeof args.path === "string" ? args.path : undefined;
|
| 42 |
+
if (!path) continue;
|
| 43 |
+
|
| 44 |
+
switch (block.name) {
|
| 45 |
+
case "read":
|
| 46 |
+
fileOps.read.add(path);
|
| 47 |
+
break;
|
| 48 |
+
case "write":
|
| 49 |
+
fileOps.written.add(path);
|
| 50 |
+
break;
|
| 51 |
+
case "edit":
|
| 52 |
+
fileOps.edited.add(path);
|
| 53 |
+
break;
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Compute final file lists from file operations.
|
| 60 |
+
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
| 61 |
+
*/
|
| 62 |
+
export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
|
| 63 |
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
| 64 |
+
const readOnly = [...fileOps.read].filter((f) => !modified.has(f)).sort();
|
| 65 |
+
const modifiedFiles = [...modified].sort();
|
| 66 |
+
return { readFiles: readOnly, modifiedFiles };
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Format file operations as XML tags for summary.
|
| 71 |
+
*/
|
| 72 |
+
export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
|
| 73 |
+
const sections: string[] = [];
|
| 74 |
+
if (readFiles.length > 0) {
|
| 75 |
+
sections.push(`<read-files>\n${readFiles.join("\n")}\n</read-files>`);
|
| 76 |
+
}
|
| 77 |
+
if (modifiedFiles.length > 0) {
|
| 78 |
+
sections.push(`<modified-files>\n${modifiedFiles.join("\n")}\n</modified-files>`);
|
| 79 |
+
}
|
| 80 |
+
if (sections.length === 0) return "";
|
| 81 |
+
return `\n\n${sections.join("\n\n")}`;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
// ============================================================================
|
| 85 |
+
// Message Serialization
|
| 86 |
+
// ============================================================================
|
| 87 |
+
|
| 88 |
+
/** Maximum characters for a tool result in serialized summaries. */
|
| 89 |
+
const TOOL_RESULT_MAX_CHARS = 2000;
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Truncate text to a maximum character length for summarization.
|
| 93 |
+
* Keeps the beginning and appends a truncation marker.
|
| 94 |
+
*/
|
| 95 |
+
function truncateForSummary(text: string, maxChars: number): string {
|
| 96 |
+
if (text.length <= maxChars) return text;
|
| 97 |
+
const truncatedChars = text.length - maxChars;
|
| 98 |
+
return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
/**
|
| 102 |
+
* Serialize LLM messages to text for summarization.
|
| 103 |
+
* This prevents the model from treating it as a conversation to continue.
|
| 104 |
+
* Call convertToLlm() first to handle custom message types.
|
| 105 |
+
*
|
| 106 |
+
* Tool results are truncated to keep the summarization request within
|
| 107 |
+
* reasonable token budgets. Full content is not needed for summarization.
|
| 108 |
+
*/
|
| 109 |
+
export function serializeConversation(messages: Message[]): string {
|
| 110 |
+
const parts: string[] = [];
|
| 111 |
+
|
| 112 |
+
for (const msg of messages) {
|
| 113 |
+
if (msg.role === "user") {
|
| 114 |
+
const content = contentText(msg.content, "");
|
| 115 |
+
if (content) parts.push(`[User]: ${content}`);
|
| 116 |
+
} else if (msg.role === "assistant") {
|
| 117 |
+
const thinkingParts: string[] = [];
|
| 118 |
+
const toolCalls: string[] = [];
|
| 119 |
+
|
| 120 |
+
for (const block of msg.content) {
|
| 121 |
+
if (block.type === "thinking") {
|
| 122 |
+
thinkingParts.push(block.thinking);
|
| 123 |
+
} else if (block.type === "toolCall") {
|
| 124 |
+
const args = block.arguments as Record<string, unknown>;
|
| 125 |
+
const argsStr = Object.entries(args)
|
| 126 |
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
| 127 |
+
.join(", ");
|
| 128 |
+
toolCalls.push(`${block.name}(${argsStr})`);
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if (thinkingParts.length > 0) {
|
| 133 |
+
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
|
| 134 |
+
}
|
| 135 |
+
if (msg.content.some((block) => block.type === "text")) {
|
| 136 |
+
parts.push(`[Assistant]: ${contentText(msg.content)}`);
|
| 137 |
+
}
|
| 138 |
+
if (toolCalls.length > 0) {
|
| 139 |
+
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
| 140 |
+
}
|
| 141 |
+
} else if (msg.role === "toolResult") {
|
| 142 |
+
const content = contentText(msg.content, "");
|
| 143 |
+
if (content) {
|
| 144 |
+
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
return parts.join("\n\n");
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// ============================================================================
|
| 153 |
+
// Summarization System Prompt
|
| 154 |
+
// ============================================================================
|
| 155 |
+
|
| 156 |
+
export const SUMMARIZATION_SYSTEM_PROMPT = `You are a context summarization assistant. Your task is to read a conversation between a user and an AI assistant, then produce a structured summary following the exact format specified.
|
| 157 |
+
|
| 158 |
+
Do NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.`;
|
packages/coding-agent/src/core/export-html/ansi-to-html.ts
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* ANSI escape code to HTML converter.
|
| 3 |
+
*
|
| 4 |
+
* Converts terminal ANSI color/style codes to HTML with inline styles.
|
| 5 |
+
* Supports:
|
| 6 |
+
* - Standard foreground colors (30-37) and bright variants (90-97)
|
| 7 |
+
* - Standard background colors (40-47) and bright variants (100-107)
|
| 8 |
+
* - 256-color palette (38;5;N and 48;5;N)
|
| 9 |
+
* - RGB true color (38;2;R;G;B and 48;2;R;G;B)
|
| 10 |
+
* - Text styles: bold (1), dim (2), italic (3), underline (4)
|
| 11 |
+
* - Reset (0)
|
| 12 |
+
*/
|
| 13 |
+
|
| 14 |
+
// Standard ANSI color palette (0-15)
|
| 15 |
+
const ANSI_COLORS = [
|
| 16 |
+
"#000000", // 0: black
|
| 17 |
+
"#800000", // 1: red
|
| 18 |
+
"#008000", // 2: green
|
| 19 |
+
"#808000", // 3: yellow
|
| 20 |
+
"#000080", // 4: blue
|
| 21 |
+
"#800080", // 5: magenta
|
| 22 |
+
"#008080", // 6: cyan
|
| 23 |
+
"#c0c0c0", // 7: white
|
| 24 |
+
"#808080", // 8: bright black
|
| 25 |
+
"#ff0000", // 9: bright red
|
| 26 |
+
"#00ff00", // 10: bright green
|
| 27 |
+
"#ffff00", // 11: bright yellow
|
| 28 |
+
"#0000ff", // 12: bright blue
|
| 29 |
+
"#ff00ff", // 13: bright magenta
|
| 30 |
+
"#00ffff", // 14: bright cyan
|
| 31 |
+
"#ffffff", // 15: bright white
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
/**
|
| 35 |
+
* Convert 256-color index to hex.
|
| 36 |
+
*/
|
| 37 |
+
function color256ToHex(index: number): string {
|
| 38 |
+
// Standard colors (0-15)
|
| 39 |
+
if (index < 16) {
|
| 40 |
+
return ANSI_COLORS[index];
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
// Color cube (16-231): 6x6x6 = 216 colors
|
| 44 |
+
if (index < 232) {
|
| 45 |
+
const cubeIndex = index - 16;
|
| 46 |
+
const r = Math.floor(cubeIndex / 36);
|
| 47 |
+
const g = Math.floor((cubeIndex % 36) / 6);
|
| 48 |
+
const b = cubeIndex % 6;
|
| 49 |
+
const toComponent = (n: number) => (n === 0 ? 0 : 55 + n * 40);
|
| 50 |
+
const toHex = (n: number) => toComponent(n).toString(16).padStart(2, "0");
|
| 51 |
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Grayscale (232-255): 24 shades
|
| 55 |
+
const gray = 8 + (index - 232) * 10;
|
| 56 |
+
const grayHex = gray.toString(16).padStart(2, "0");
|
| 57 |
+
return `#${grayHex}${grayHex}${grayHex}`;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/**
|
| 61 |
+
* Escape HTML special characters.
|
| 62 |
+
*/
|
| 63 |
+
function escapeHtml(text: string): string {
|
| 64 |
+
return text
|
| 65 |
+
.replace(/&/g, "&")
|
| 66 |
+
.replace(/</g, "<")
|
| 67 |
+
.replace(/>/g, ">")
|
| 68 |
+
.replace(/"/g, """)
|
| 69 |
+
.replace(/'/g, "'");
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
interface TextStyle {
|
| 73 |
+
fg: string | null;
|
| 74 |
+
bg: string | null;
|
| 75 |
+
bold: boolean;
|
| 76 |
+
dim: boolean;
|
| 77 |
+
italic: boolean;
|
| 78 |
+
underline: boolean;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function createEmptyStyle(): TextStyle {
|
| 82 |
+
return {
|
| 83 |
+
fg: null,
|
| 84 |
+
bg: null,
|
| 85 |
+
bold: false,
|
| 86 |
+
dim: false,
|
| 87 |
+
italic: false,
|
| 88 |
+
underline: false,
|
| 89 |
+
};
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
function styleToInlineCSS(style: TextStyle): string {
|
| 93 |
+
const parts: string[] = [];
|
| 94 |
+
if (style.fg) parts.push(`color:${style.fg}`);
|
| 95 |
+
if (style.bg) parts.push(`background-color:${style.bg}`);
|
| 96 |
+
if (style.bold) parts.push("font-weight:bold");
|
| 97 |
+
if (style.dim) parts.push("opacity:0.6");
|
| 98 |
+
if (style.italic) parts.push("font-style:italic");
|
| 99 |
+
if (style.underline) parts.push("text-decoration:underline");
|
| 100 |
+
return parts.join(";");
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function hasStyle(style: TextStyle): boolean {
|
| 104 |
+
return style.fg !== null || style.bg !== null || style.bold || style.dim || style.italic || style.underline;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
/**
|
| 108 |
+
* Parse ANSI SGR (Select Graphic Rendition) codes and update style.
|
| 109 |
+
*/
|
| 110 |
+
function applySgrCode(params: number[], style: TextStyle): void {
|
| 111 |
+
let i = 0;
|
| 112 |
+
while (i < params.length) {
|
| 113 |
+
const code = params[i];
|
| 114 |
+
|
| 115 |
+
if (code === 0) {
|
| 116 |
+
// Reset all
|
| 117 |
+
style.fg = null;
|
| 118 |
+
style.bg = null;
|
| 119 |
+
style.bold = false;
|
| 120 |
+
style.dim = false;
|
| 121 |
+
style.italic = false;
|
| 122 |
+
style.underline = false;
|
| 123 |
+
} else if (code === 1) {
|
| 124 |
+
style.bold = true;
|
| 125 |
+
} else if (code === 2) {
|
| 126 |
+
style.dim = true;
|
| 127 |
+
} else if (code === 3) {
|
| 128 |
+
style.italic = true;
|
| 129 |
+
} else if (code === 4) {
|
| 130 |
+
style.underline = true;
|
| 131 |
+
} else if (code === 22) {
|
| 132 |
+
// Reset bold/dim
|
| 133 |
+
style.bold = false;
|
| 134 |
+
style.dim = false;
|
| 135 |
+
} else if (code === 23) {
|
| 136 |
+
style.italic = false;
|
| 137 |
+
} else if (code === 24) {
|
| 138 |
+
style.underline = false;
|
| 139 |
+
} else if (code >= 30 && code <= 37) {
|
| 140 |
+
// Standard foreground colors
|
| 141 |
+
style.fg = ANSI_COLORS[code - 30];
|
| 142 |
+
} else if (code === 38) {
|
| 143 |
+
// Extended foreground color
|
| 144 |
+
if (params[i + 1] === 5 && params.length > i + 2) {
|
| 145 |
+
// 256-color: 38;5;N
|
| 146 |
+
style.fg = color256ToHex(params[i + 2]);
|
| 147 |
+
i += 2;
|
| 148 |
+
} else if (params[i + 1] === 2 && params.length > i + 4) {
|
| 149 |
+
// RGB: 38;2;R;G;B
|
| 150 |
+
const r = params[i + 2];
|
| 151 |
+
const g = params[i + 3];
|
| 152 |
+
const b = params[i + 4];
|
| 153 |
+
style.fg = `rgb(${r},${g},${b})`;
|
| 154 |
+
i += 4;
|
| 155 |
+
}
|
| 156 |
+
} else if (code === 39) {
|
| 157 |
+
// Default foreground
|
| 158 |
+
style.fg = null;
|
| 159 |
+
} else if (code >= 40 && code <= 47) {
|
| 160 |
+
// Standard background colors
|
| 161 |
+
style.bg = ANSI_COLORS[code - 40];
|
| 162 |
+
} else if (code === 48) {
|
| 163 |
+
// Extended background color
|
| 164 |
+
if (params[i + 1] === 5 && params.length > i + 2) {
|
| 165 |
+
// 256-color: 48;5;N
|
| 166 |
+
style.bg = color256ToHex(params[i + 2]);
|
| 167 |
+
i += 2;
|
| 168 |
+
} else if (params[i + 1] === 2 && params.length > i + 4) {
|
| 169 |
+
// RGB: 48;2;R;G;B
|
| 170 |
+
const r = params[i + 2];
|
| 171 |
+
const g = params[i + 3];
|
| 172 |
+
const b = params[i + 4];
|
| 173 |
+
style.bg = `rgb(${r},${g},${b})`;
|
| 174 |
+
i += 4;
|
| 175 |
+
}
|
| 176 |
+
} else if (code === 49) {
|
| 177 |
+
// Default background
|
| 178 |
+
style.bg = null;
|
| 179 |
+
} else if (code >= 90 && code <= 97) {
|
| 180 |
+
// Bright foreground colors
|
| 181 |
+
style.fg = ANSI_COLORS[code - 90 + 8];
|
| 182 |
+
} else if (code >= 100 && code <= 107) {
|
| 183 |
+
// Bright background colors
|
| 184 |
+
style.bg = ANSI_COLORS[code - 100 + 8];
|
| 185 |
+
}
|
| 186 |
+
// Ignore unrecognized codes
|
| 187 |
+
|
| 188 |
+
i++;
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
// Match ANSI escape sequences: ESC[ followed by params and ending with 'm'
|
| 193 |
+
const ANSI_REGEX = /\x1b\[([\d;]*)m/g;
|
| 194 |
+
|
| 195 |
+
/**
|
| 196 |
+
* Convert ANSI-escaped text to HTML with inline styles.
|
| 197 |
+
*/
|
| 198 |
+
export function ansiToHtml(text: string): string {
|
| 199 |
+
const style = createEmptyStyle();
|
| 200 |
+
let result = "";
|
| 201 |
+
let lastIndex = 0;
|
| 202 |
+
let inSpan = false;
|
| 203 |
+
|
| 204 |
+
// Reset regex state
|
| 205 |
+
ANSI_REGEX.lastIndex = 0;
|
| 206 |
+
|
| 207 |
+
let match = ANSI_REGEX.exec(text);
|
| 208 |
+
while (match !== null) {
|
| 209 |
+
// Add text before this escape sequence
|
| 210 |
+
const beforeText = text.slice(lastIndex, match.index);
|
| 211 |
+
if (beforeText) {
|
| 212 |
+
result += escapeHtml(beforeText);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
// Parse SGR parameters
|
| 216 |
+
const paramStr = match[1];
|
| 217 |
+
const params = paramStr ? paramStr.split(";").map((p) => parseInt(p, 10) || 0) : [0];
|
| 218 |
+
|
| 219 |
+
// Close existing span if we have one
|
| 220 |
+
if (inSpan) {
|
| 221 |
+
result += "</span>";
|
| 222 |
+
inSpan = false;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
// Apply the codes
|
| 226 |
+
applySgrCode(params, style);
|
| 227 |
+
|
| 228 |
+
// Open new span if we have any styling
|
| 229 |
+
if (hasStyle(style)) {
|
| 230 |
+
result += `<span style="${styleToInlineCSS(style)}">`;
|
| 231 |
+
inSpan = true;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
lastIndex = match.index + match[0].length;
|
| 235 |
+
match = ANSI_REGEX.exec(text);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
// Add remaining text
|
| 239 |
+
const remainingText = text.slice(lastIndex);
|
| 240 |
+
if (remainingText) {
|
| 241 |
+
result += escapeHtml(remainingText);
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
// Close any open span
|
| 245 |
+
if (inSpan) {
|
| 246 |
+
result += "</span>";
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
return result;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Convert array of ANSI-escaped lines to HTML.
|
| 254 |
+
* Each line is wrapped in a div element.
|
| 255 |
+
*/
|
| 256 |
+
export function ansiLinesToHtml(lines: string[]): string {
|
| 257 |
+
return lines.map((line) => `<div class="ansi-line">${ansiToHtml(line) || " "}</div>`).join("");
|
| 258 |
+
}
|
packages/coding-agent/src/core/export-html/index.ts
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentState } from "@earendil-works/pi-agent-core";
|
| 2 |
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
| 3 |
+
import { basename, join } from "path";
|
| 4 |
+
import { APP_NAME, getExportTemplateDir } from "../../config.ts";
|
| 5 |
+
import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme.ts";
|
| 6 |
+
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
| 7 |
+
import type { ToolDefinition } from "../extensions/types.ts";
|
| 8 |
+
import type { SessionEntry } from "../session-manager.ts";
|
| 9 |
+
import { SessionManager } from "../session-manager.ts";
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Interface for rendering custom tools to HTML.
|
| 13 |
+
* Used by agent-session to pre-render extension tool output.
|
| 14 |
+
*/
|
| 15 |
+
export interface ToolHtmlRenderer {
|
| 16 |
+
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
|
| 17 |
+
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;
|
| 18 |
+
/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */
|
| 19 |
+
renderResult(
|
| 20 |
+
toolCallId: string,
|
| 21 |
+
toolName: string,
|
| 22 |
+
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
|
| 23 |
+
details: unknown,
|
| 24 |
+
isError: boolean,
|
| 25 |
+
): { collapsed?: string; expanded?: string } | undefined;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/** Pre-rendered HTML for a custom tool call and result */
|
| 29 |
+
interface RenderedToolHtml {
|
| 30 |
+
callHtml?: string;
|
| 31 |
+
resultHtmlCollapsed?: string;
|
| 32 |
+
resultHtmlExpanded?: string;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export interface ExportOptions {
|
| 36 |
+
outputPath?: string;
|
| 37 |
+
themeName?: string;
|
| 38 |
+
/** Optional tool renderer for custom tools */
|
| 39 |
+
toolRenderer?: ToolHtmlRenderer;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */
|
| 43 |
+
function parseColor(color: string): { r: number; g: number; b: number } | undefined {
|
| 44 |
+
const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
|
| 45 |
+
if (hexMatch) {
|
| 46 |
+
return {
|
| 47 |
+
r: Number.parseInt(hexMatch[1], 16),
|
| 48 |
+
g: Number.parseInt(hexMatch[2], 16),
|
| 49 |
+
b: Number.parseInt(hexMatch[3], 16),
|
| 50 |
+
};
|
| 51 |
+
}
|
| 52 |
+
const rgbMatch = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
| 53 |
+
if (rgbMatch) {
|
| 54 |
+
return {
|
| 55 |
+
r: Number.parseInt(rgbMatch[1], 10),
|
| 56 |
+
g: Number.parseInt(rgbMatch[2], 10),
|
| 57 |
+
b: Number.parseInt(rgbMatch[3], 10),
|
| 58 |
+
};
|
| 59 |
+
}
|
| 60 |
+
return undefined;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/** Calculate relative luminance of a color (0-1, higher = lighter). */
|
| 64 |
+
function getLuminance(r: number, g: number, b: number): number {
|
| 65 |
+
const toLinear = (c: number) => {
|
| 66 |
+
const s = c / 255;
|
| 67 |
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
| 68 |
+
};
|
| 69 |
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */
|
| 73 |
+
function adjustBrightness(color: string, factor: number): string {
|
| 74 |
+
const parsed = parseColor(color);
|
| 75 |
+
if (!parsed) return color;
|
| 76 |
+
const adjust = (c: number) => Math.min(255, Math.max(0, Math.round(c * factor)));
|
| 77 |
+
return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
/** Derive export background colors from a base color (e.g., userMessageBg). */
|
| 81 |
+
function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string; infoBg: string } {
|
| 82 |
+
const parsed = parseColor(baseColor);
|
| 83 |
+
if (!parsed) {
|
| 84 |
+
return {
|
| 85 |
+
pageBg: "rgb(24, 24, 30)",
|
| 86 |
+
cardBg: "rgb(30, 30, 36)",
|
| 87 |
+
infoBg: "rgb(60, 55, 40)",
|
| 88 |
+
};
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
const luminance = getLuminance(parsed.r, parsed.g, parsed.b);
|
| 92 |
+
const isLight = luminance > 0.5;
|
| 93 |
+
|
| 94 |
+
if (isLight) {
|
| 95 |
+
return {
|
| 96 |
+
pageBg: adjustBrightness(baseColor, 0.96),
|
| 97 |
+
cardBg: baseColor,
|
| 98 |
+
infoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,
|
| 99 |
+
};
|
| 100 |
+
}
|
| 101 |
+
return {
|
| 102 |
+
pageBg: adjustBrightness(baseColor, 0.7),
|
| 103 |
+
cardBg: adjustBrightness(baseColor, 0.85),
|
| 104 |
+
infoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,
|
| 105 |
+
};
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/**
|
| 109 |
+
* Generate CSS custom property declarations from theme colors.
|
| 110 |
+
*/
|
| 111 |
+
function generateThemeVars(themeName?: string): string {
|
| 112 |
+
const colors = getResolvedThemeColors(themeName);
|
| 113 |
+
const lines: string[] = [];
|
| 114 |
+
for (const [key, value] of Object.entries(colors)) {
|
| 115 |
+
lines.push(`--${key}: ${value};`);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
// Use explicit theme export colors if available, otherwise derive from userMessageBg
|
| 119 |
+
const themeExport = getThemeExportColors(themeName);
|
| 120 |
+
const userMessageBg = colors.userMessageBg || "#343541";
|
| 121 |
+
const derivedColors = deriveExportColors(userMessageBg);
|
| 122 |
+
|
| 123 |
+
lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);
|
| 124 |
+
lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);
|
| 125 |
+
lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);
|
| 126 |
+
|
| 127 |
+
return lines.join("\n ");
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
interface SessionData {
|
| 131 |
+
header: ReturnType<SessionManager["getHeader"]>;
|
| 132 |
+
entries: ReturnType<SessionManager["getEntries"]>;
|
| 133 |
+
leafId: string | null;
|
| 134 |
+
systemPrompt?: string;
|
| 135 |
+
tools?: Array<Pick<ToolDefinition, "name" | "description" | "parameters">>;
|
| 136 |
+
/** Pre-rendered HTML for custom tool calls/results, keyed by tool call ID */
|
| 137 |
+
renderedTools?: Record<string, RenderedToolHtml>;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/**
|
| 141 |
+
* Core HTML generation logic shared by both export functions.
|
| 142 |
+
*/
|
| 143 |
+
function generateHtml(sessionData: SessionData, themeName?: string): string {
|
| 144 |
+
const templateDir = getExportTemplateDir();
|
| 145 |
+
const template = readFileSync(join(templateDir, "template.html"), "utf-8");
|
| 146 |
+
const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
|
| 147 |
+
const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8");
|
| 148 |
+
const markedJs = readFileSync(join(templateDir, "vendor", "marked.min.js"), "utf-8");
|
| 149 |
+
const hljsJs = readFileSync(join(templateDir, "vendor", "highlight.min.js"), "utf-8");
|
| 150 |
+
|
| 151 |
+
const themeVars = generateThemeVars(themeName);
|
| 152 |
+
const colors = getResolvedThemeColors(themeName);
|
| 153 |
+
const themeExport = getThemeExportColors(themeName);
|
| 154 |
+
const derivedExportColors = deriveExportColors(colors.userMessageBg || "#343541");
|
| 155 |
+
const bodyBg = themeExport.pageBg ?? derivedExportColors.pageBg;
|
| 156 |
+
const containerBg = themeExport.cardBg ?? derivedExportColors.cardBg;
|
| 157 |
+
const infoBg = themeExport.infoBg ?? derivedExportColors.infoBg;
|
| 158 |
+
|
| 159 |
+
// Base64 encode session data to avoid escaping issues
|
| 160 |
+
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString("base64");
|
| 161 |
+
|
| 162 |
+
// Build the CSS with theme variables injected
|
| 163 |
+
const css = templateCss
|
| 164 |
+
.replace("{{THEME_VARS}}", themeVars)
|
| 165 |
+
.replace("{{BODY_BG}}", bodyBg)
|
| 166 |
+
.replace("{{CONTAINER_BG}}", containerBg)
|
| 167 |
+
.replace("{{INFO_BG}}", infoBg);
|
| 168 |
+
|
| 169 |
+
return template
|
| 170 |
+
.replace("{{CSS}}", css)
|
| 171 |
+
.replace("{{JS}}", templateJs)
|
| 172 |
+
.replace("{{SESSION_DATA}}", sessionDataBase64)
|
| 173 |
+
.replace("{{MARKED_JS}}", markedJs)
|
| 174 |
+
.replace("{{HIGHLIGHT_JS}}", hljsJs);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/** Tools rendered directly by the HTML template (not pre-rendered via TUI→ANSI→HTML pipeline) */
|
| 178 |
+
const TEMPLATE_RENDERED_TOOLS = new Set(["bash", "read", "write", "edit", "ls"]);
|
| 179 |
+
|
| 180 |
+
/**
|
| 181 |
+
* Pre-render custom tools to HTML using their TUI renderers.
|
| 182 |
+
*/
|
| 183 |
+
function preRenderCustomTools(
|
| 184 |
+
entries: SessionEntry[],
|
| 185 |
+
toolRenderer: ToolHtmlRenderer,
|
| 186 |
+
): Record<string, RenderedToolHtml> {
|
| 187 |
+
const renderedTools: Record<string, RenderedToolHtml> = {};
|
| 188 |
+
|
| 189 |
+
for (const entry of entries) {
|
| 190 |
+
if (entry.type !== "message") continue;
|
| 191 |
+
const msg = entry.message;
|
| 192 |
+
|
| 193 |
+
// Find tool calls in assistant messages
|
| 194 |
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
| 195 |
+
for (const block of msg.content) {
|
| 196 |
+
if (block.type === "toolCall" && !TEMPLATE_RENDERED_TOOLS.has(block.name)) {
|
| 197 |
+
const callHtml = toolRenderer.renderCall(block.id, block.name, block.arguments);
|
| 198 |
+
if (callHtml) {
|
| 199 |
+
renderedTools[block.id] = { callHtml };
|
| 200 |
+
}
|
| 201 |
+
}
|
| 202 |
+
}
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
// Find tool results
|
| 206 |
+
if (msg.role === "toolResult" && msg.toolCallId) {
|
| 207 |
+
const toolName = msg.toolName || "";
|
| 208 |
+
// Only render if we have a pre-rendered call OR it's not template-rendered
|
| 209 |
+
const existing = renderedTools[msg.toolCallId];
|
| 210 |
+
if (existing || !TEMPLATE_RENDERED_TOOLS.has(toolName)) {
|
| 211 |
+
const rendered = toolRenderer.renderResult(
|
| 212 |
+
msg.toolCallId,
|
| 213 |
+
toolName,
|
| 214 |
+
msg.content,
|
| 215 |
+
msg.details,
|
| 216 |
+
msg.isError || false,
|
| 217 |
+
);
|
| 218 |
+
if (rendered) {
|
| 219 |
+
renderedTools[msg.toolCallId] = {
|
| 220 |
+
...existing,
|
| 221 |
+
resultHtmlCollapsed: rendered.collapsed,
|
| 222 |
+
resultHtmlExpanded: rendered.expanded,
|
| 223 |
+
};
|
| 224 |
+
}
|
| 225 |
+
}
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
return renderedTools;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Export session to HTML using SessionManager and AgentState.
|
| 234 |
+
* Used by TUI's /export command.
|
| 235 |
+
*/
|
| 236 |
+
export async function exportSessionToHtml(
|
| 237 |
+
sm: SessionManager,
|
| 238 |
+
state?: AgentState,
|
| 239 |
+
options?: ExportOptions | string,
|
| 240 |
+
): Promise<string> {
|
| 241 |
+
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
| 242 |
+
|
| 243 |
+
const sessionFile = sm.getSessionFile();
|
| 244 |
+
if (!sessionFile) {
|
| 245 |
+
throw new Error("Cannot export in-memory session to HTML");
|
| 246 |
+
}
|
| 247 |
+
if (!existsSync(sessionFile)) {
|
| 248 |
+
throw new Error("Nothing to export yet - start a conversation first");
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
const entries = sm.getEntries();
|
| 252 |
+
|
| 253 |
+
// Pre-render custom tools if a tool renderer is provided
|
| 254 |
+
let renderedTools: Record<string, RenderedToolHtml> | undefined;
|
| 255 |
+
if (opts.toolRenderer) {
|
| 256 |
+
renderedTools = preRenderCustomTools(entries, opts.toolRenderer);
|
| 257 |
+
// Only include if we actually rendered something
|
| 258 |
+
if (Object.keys(renderedTools).length === 0) {
|
| 259 |
+
renderedTools = undefined;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
const sessionData: SessionData = {
|
| 264 |
+
header: sm.getHeader(),
|
| 265 |
+
entries,
|
| 266 |
+
leafId: sm.getLeafId(),
|
| 267 |
+
systemPrompt: state?.systemPrompt,
|
| 268 |
+
tools: state?.tools?.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })),
|
| 269 |
+
renderedTools,
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
const html = generateHtml(sessionData, opts.themeName);
|
| 273 |
+
|
| 274 |
+
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
| 275 |
+
if (!outputPath) {
|
| 276 |
+
const sessionBasename = basename(sessionFile, ".jsonl");
|
| 277 |
+
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
writeFileSync(outputPath, html, "utf8");
|
| 281 |
+
return outputPath;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/**
|
| 285 |
+
* Export session file to HTML (standalone, without AgentState).
|
| 286 |
+
* Used by CLI for exporting arbitrary session files.
|
| 287 |
+
*/
|
| 288 |
+
export async function exportFromFile(inputPath: string, options?: ExportOptions | string): Promise<string> {
|
| 289 |
+
const opts: ExportOptions = typeof options === "string" ? { outputPath: options } : options || {};
|
| 290 |
+
const resolvedInputPath = resolvePath(inputPath);
|
| 291 |
+
|
| 292 |
+
if (!existsSync(resolvedInputPath)) {
|
| 293 |
+
throw new Error(`File not found: ${resolvedInputPath}`);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
const sm = SessionManager.open(resolvedInputPath);
|
| 297 |
+
|
| 298 |
+
const sessionData: SessionData = {
|
| 299 |
+
header: sm.getHeader(),
|
| 300 |
+
entries: sm.getEntries(),
|
| 301 |
+
leafId: sm.getLeafId(),
|
| 302 |
+
systemPrompt: undefined,
|
| 303 |
+
tools: undefined,
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
const html = generateHtml(sessionData, opts.themeName);
|
| 307 |
+
|
| 308 |
+
let outputPath = opts.outputPath ? normalizePath(opts.outputPath) : undefined;
|
| 309 |
+
if (!outputPath) {
|
| 310 |
+
const inputBasename = basename(resolvedInputPath, ".jsonl");
|
| 311 |
+
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
writeFileSync(outputPath, html, "utf8");
|
| 315 |
+
return outputPath;
|
| 316 |
+
}
|
packages/coding-agent/src/core/export-html/template.css
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
{{THEME_VARS}}
|
| 3 |
+
--body-bg: {{BODY_BG}};
|
| 4 |
+
--container-bg: {{CONTAINER_BG}};
|
| 5 |
+
--info-bg: {{INFO_BG}};
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
| 9 |
+
|
| 10 |
+
:root {
|
| 11 |
+
--line-height: 18px; /* 12px font * 1.5 */
|
| 12 |
+
--sidebar-width: 400px;
|
| 13 |
+
--sidebar-min-width: 240px;
|
| 14 |
+
--sidebar-max-width: 840px;
|
| 15 |
+
--sidebar-resizer-width: 6px;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
| 20 |
+
font-size: 12px;
|
| 21 |
+
line-height: var(--line-height);
|
| 22 |
+
color: var(--text);
|
| 23 |
+
background: var(--body-bg);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body.sidebar-resizing {
|
| 27 |
+
cursor: col-resize;
|
| 28 |
+
user-select: none;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
#app {
|
| 32 |
+
display: flex;
|
| 33 |
+
min-height: 100vh;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Sidebar */
|
| 37 |
+
#sidebar {
|
| 38 |
+
width: var(--sidebar-width);
|
| 39 |
+
min-width: var(--sidebar-width);
|
| 40 |
+
max-width: var(--sidebar-width);
|
| 41 |
+
background: var(--container-bg);
|
| 42 |
+
flex-shrink: 0;
|
| 43 |
+
display: flex;
|
| 44 |
+
flex-direction: column;
|
| 45 |
+
position: sticky;
|
| 46 |
+
top: 0;
|
| 47 |
+
height: 100vh;
|
| 48 |
+
border-right: 1px solid var(--dim);
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
#sidebar-resizer {
|
| 52 |
+
width: var(--sidebar-resizer-width);
|
| 53 |
+
flex-shrink: 0;
|
| 54 |
+
position: sticky;
|
| 55 |
+
top: 0;
|
| 56 |
+
height: 100vh;
|
| 57 |
+
cursor: col-resize;
|
| 58 |
+
touch-action: none;
|
| 59 |
+
background: transparent;
|
| 60 |
+
border-right: 1px solid transparent;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
#sidebar-resizer:hover,
|
| 64 |
+
body.sidebar-resizing #sidebar-resizer {
|
| 65 |
+
background: var(--selectedBg);
|
| 66 |
+
border-right-color: var(--dim);
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.sidebar-header {
|
| 70 |
+
padding: 8px 12px;
|
| 71 |
+
flex-shrink: 0;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.sidebar-controls {
|
| 75 |
+
padding: 8px 8px 4px 8px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.sidebar-search {
|
| 79 |
+
width: 100%;
|
| 80 |
+
box-sizing: border-box;
|
| 81 |
+
padding: 4px 8px;
|
| 82 |
+
font-size: 11px;
|
| 83 |
+
font-family: inherit;
|
| 84 |
+
background: var(--body-bg);
|
| 85 |
+
color: var(--text);
|
| 86 |
+
border: 1px solid var(--dim);
|
| 87 |
+
border-radius: 3px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.sidebar-filters {
|
| 91 |
+
display: flex;
|
| 92 |
+
padding: 4px 8px 8px 8px;
|
| 93 |
+
gap: 4px;
|
| 94 |
+
align-items: center;
|
| 95 |
+
flex-wrap: wrap;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.sidebar-search:focus {
|
| 99 |
+
outline: none;
|
| 100 |
+
border-color: var(--accent);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.sidebar-search::placeholder {
|
| 104 |
+
color: var(--muted);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.filter-btn {
|
| 108 |
+
padding: 3px 8px;
|
| 109 |
+
font-size: 10px;
|
| 110 |
+
font-family: inherit;
|
| 111 |
+
background: transparent;
|
| 112 |
+
color: var(--muted);
|
| 113 |
+
border: 1px solid var(--dim);
|
| 114 |
+
border-radius: 3px;
|
| 115 |
+
cursor: pointer;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.filter-btn:hover {
|
| 119 |
+
color: var(--text);
|
| 120 |
+
border-color: var(--text);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.filter-btn.active {
|
| 124 |
+
background: var(--accent);
|
| 125 |
+
color: var(--body-bg);
|
| 126 |
+
border-color: var(--accent);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.sidebar-close {
|
| 130 |
+
display: none;
|
| 131 |
+
padding: 3px 8px;
|
| 132 |
+
font-size: 12px;
|
| 133 |
+
font-family: inherit;
|
| 134 |
+
background: transparent;
|
| 135 |
+
color: var(--muted);
|
| 136 |
+
border: 1px solid var(--dim);
|
| 137 |
+
border-radius: 3px;
|
| 138 |
+
cursor: pointer;
|
| 139 |
+
margin-left: auto;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.sidebar-close:hover {
|
| 143 |
+
color: var(--text);
|
| 144 |
+
border-color: var(--text);
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.tree-container {
|
| 148 |
+
flex: 1;
|
| 149 |
+
overflow: auto;
|
| 150 |
+
padding: 4px 0;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.tree-node {
|
| 154 |
+
padding: 0 8px;
|
| 155 |
+
cursor: pointer;
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: baseline;
|
| 158 |
+
font-size: 11px;
|
| 159 |
+
line-height: 13px;
|
| 160 |
+
white-space: nowrap;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.tree-node:hover {
|
| 164 |
+
background: var(--selectedBg);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.tree-node.active {
|
| 168 |
+
background: var(--selectedBg);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.tree-node.active .tree-content {
|
| 172 |
+
font-weight: bold;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.tree-node.in-path {
|
| 176 |
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.tree-node:not(.in-path) {
|
| 180 |
+
opacity: 0.5;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.tree-node:not(.in-path):hover {
|
| 184 |
+
opacity: 1;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.tree-prefix {
|
| 188 |
+
color: var(--muted);
|
| 189 |
+
flex-shrink: 0;
|
| 190 |
+
font-family: monospace;
|
| 191 |
+
white-space: pre;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.tree-marker {
|
| 195 |
+
color: var(--accent);
|
| 196 |
+
flex-shrink: 0;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.tree-content {
|
| 200 |
+
color: var(--text);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.tree-role-user {
|
| 204 |
+
color: var(--accent);
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
.tree-role-skill {
|
| 208 |
+
color: var(--customMessageLabel);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.tree-role-assistant {
|
| 212 |
+
color: var(--success);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.tree-role-tool {
|
| 216 |
+
color: var(--muted);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.tree-muted {
|
| 220 |
+
color: var(--muted);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.tree-error {
|
| 224 |
+
color: var(--error);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.tree-compaction {
|
| 228 |
+
color: var(--borderAccent);
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
.tree-branch-summary {
|
| 232 |
+
color: var(--warning);
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.tree-custom-message {
|
| 236 |
+
color: var(--customMessageLabel);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.tree-status {
|
| 240 |
+
padding: 4px 12px;
|
| 241 |
+
font-size: 10px;
|
| 242 |
+
color: var(--muted);
|
| 243 |
+
flex-shrink: 0;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* Main content */
|
| 247 |
+
#content {
|
| 248 |
+
flex: 1;
|
| 249 |
+
min-width: 0;
|
| 250 |
+
overflow-y: auto;
|
| 251 |
+
padding: var(--line-height) calc(var(--line-height) * 2);
|
| 252 |
+
display: flex;
|
| 253 |
+
flex-direction: column;
|
| 254 |
+
align-items: center;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
#content > * {
|
| 258 |
+
width: 100%;
|
| 259 |
+
max-width: 800px;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
/* Help bar */
|
| 263 |
+
.help-bar {
|
| 264 |
+
font-size: 11px;
|
| 265 |
+
color: var(--warning);
|
| 266 |
+
margin-bottom: var(--line-height);
|
| 267 |
+
display: flex;
|
| 268 |
+
align-items: center;
|
| 269 |
+
justify-content: space-between;
|
| 270 |
+
flex-wrap: wrap;
|
| 271 |
+
gap: 12px;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.help-hint {
|
| 275 |
+
flex: 1 1 240px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.help-actions {
|
| 279 |
+
display: flex;
|
| 280 |
+
align-items: center;
|
| 281 |
+
flex-wrap: wrap;
|
| 282 |
+
gap: 8px;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.header-toggle-btn,
|
| 286 |
+
.download-json-btn {
|
| 287 |
+
font-size: 10px;
|
| 288 |
+
padding: 2px 8px;
|
| 289 |
+
background: var(--container-bg);
|
| 290 |
+
border: 1px solid var(--border);
|
| 291 |
+
border-radius: 3px;
|
| 292 |
+
color: var(--text);
|
| 293 |
+
cursor: pointer;
|
| 294 |
+
font-family: inherit;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.header-toggle-btn:hover,
|
| 298 |
+
.download-json-btn:hover {
|
| 299 |
+
background: var(--hover);
|
| 300 |
+
border-color: var(--borderAccent);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
/* Header */
|
| 304 |
+
.header {
|
| 305 |
+
background: var(--container-bg);
|
| 306 |
+
border-radius: 4px;
|
| 307 |
+
padding: var(--line-height);
|
| 308 |
+
margin-bottom: var(--line-height);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.header h1 {
|
| 312 |
+
font-size: 12px;
|
| 313 |
+
font-weight: bold;
|
| 314 |
+
color: var(--borderAccent);
|
| 315 |
+
margin-bottom: var(--line-height);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.header-info {
|
| 319 |
+
display: flex;
|
| 320 |
+
flex-direction: column;
|
| 321 |
+
gap: 0;
|
| 322 |
+
font-size: 11px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.info-item {
|
| 326 |
+
color: var(--dim);
|
| 327 |
+
display: flex;
|
| 328 |
+
align-items: baseline;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.info-label {
|
| 332 |
+
font-weight: 600;
|
| 333 |
+
margin-right: 8px;
|
| 334 |
+
min-width: 100px;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.info-value {
|
| 338 |
+
color: var(--text);
|
| 339 |
+
flex: 1;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
/* Messages */
|
| 343 |
+
#messages {
|
| 344 |
+
display: flex;
|
| 345 |
+
flex-direction: column;
|
| 346 |
+
gap: var(--line-height);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.message-timestamp {
|
| 350 |
+
font-size: 10px;
|
| 351 |
+
color: var(--dim);
|
| 352 |
+
opacity: 0.8;
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
.user-message {
|
| 356 |
+
background: var(--userMessageBg);
|
| 357 |
+
color: var(--userMessageText);
|
| 358 |
+
padding: var(--line-height);
|
| 359 |
+
border-radius: 4px;
|
| 360 |
+
position: relative;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.assistant-message {
|
| 364 |
+
padding: 0;
|
| 365 |
+
position: relative;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
/* Copy link button - appears on hover */
|
| 369 |
+
.copy-link-btn {
|
| 370 |
+
position: absolute;
|
| 371 |
+
top: 8px;
|
| 372 |
+
right: 8px;
|
| 373 |
+
width: 28px;
|
| 374 |
+
height: 28px;
|
| 375 |
+
padding: 6px;
|
| 376 |
+
background: var(--container-bg);
|
| 377 |
+
border: 1px solid var(--dim);
|
| 378 |
+
border-radius: 4px;
|
| 379 |
+
color: var(--muted);
|
| 380 |
+
cursor: pointer;
|
| 381 |
+
opacity: 0;
|
| 382 |
+
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
| 383 |
+
display: flex;
|
| 384 |
+
align-items: center;
|
| 385 |
+
justify-content: center;
|
| 386 |
+
z-index: 10;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
.user-message:hover .copy-link-btn,
|
| 390 |
+
.assistant-message:hover .copy-link-btn,
|
| 391 |
+
.skill-user-entry:hover .copy-link-btn {
|
| 392 |
+
opacity: 1;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.copy-link-btn:hover {
|
| 396 |
+
background: var(--accent);
|
| 397 |
+
color: var(--body-bg);
|
| 398 |
+
border-color: var(--accent);
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.copy-link-btn.copied {
|
| 402 |
+
background: var(--success, #22c55e);
|
| 403 |
+
color: white;
|
| 404 |
+
border-color: var(--success, #22c55e);
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* Highlight effect for deep-linked messages */
|
| 408 |
+
.user-message.highlight,
|
| 409 |
+
.assistant-message.highlight {
|
| 410 |
+
animation: highlight-pulse 2s ease-out;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
@keyframes highlight-pulse {
|
| 414 |
+
0% {
|
| 415 |
+
box-shadow: 0 0 0 3px var(--accent);
|
| 416 |
+
}
|
| 417 |
+
100% {
|
| 418 |
+
box-shadow: 0 0 0 0 transparent;
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.assistant-message > .message-timestamp {
|
| 423 |
+
padding-left: var(--line-height);
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.assistant-text {
|
| 427 |
+
padding: var(--line-height);
|
| 428 |
+
padding-bottom: 0;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.message-timestamp + .assistant-text,
|
| 432 |
+
.message-timestamp + .thinking-block {
|
| 433 |
+
padding-top: 0;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.thinking-block + .assistant-text {
|
| 437 |
+
padding-top: 0;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.thinking-text {
|
| 441 |
+
padding: var(--line-height);
|
| 442 |
+
color: var(--thinkingText);
|
| 443 |
+
font-style: italic;
|
| 444 |
+
white-space: pre-wrap;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
.message-timestamp + .thinking-block .thinking-text,
|
| 448 |
+
.message-timestamp + .thinking-block .thinking-collapsed {
|
| 449 |
+
padding-top: 0;
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
.thinking-collapsed {
|
| 453 |
+
display: none;
|
| 454 |
+
padding: var(--line-height);
|
| 455 |
+
color: var(--thinkingText);
|
| 456 |
+
font-style: italic;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
/* Tool execution */
|
| 460 |
+
.tool-execution {
|
| 461 |
+
padding: var(--line-height);
|
| 462 |
+
border-radius: 4px;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
.tool-execution + .tool-execution {
|
| 466 |
+
margin-top: var(--line-height);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.assistant-text + .tool-execution {
|
| 470 |
+
margin-top: var(--line-height);
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
.tool-execution.pending { background: var(--toolPendingBg); }
|
| 474 |
+
.tool-execution.success { background: var(--toolSuccessBg); }
|
| 475 |
+
.tool-execution.error { background: var(--toolErrorBg); }
|
| 476 |
+
|
| 477 |
+
.tool-header, .tool-name {
|
| 478 |
+
font-weight: bold;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.tool-path {
|
| 482 |
+
color: var(--accent);
|
| 483 |
+
word-break: break-all;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.line-numbers {
|
| 487 |
+
color: var(--warning);
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.line-count {
|
| 491 |
+
color: var(--dim);
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.tool-command {
|
| 495 |
+
font-weight: bold;
|
| 496 |
+
white-space: pre-wrap;
|
| 497 |
+
word-wrap: break-word;
|
| 498 |
+
overflow-wrap: break-word;
|
| 499 |
+
word-break: break-word;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
.tool-output {
|
| 503 |
+
margin-top: var(--line-height);
|
| 504 |
+
color: var(--toolOutput);
|
| 505 |
+
word-wrap: break-word;
|
| 506 |
+
overflow-wrap: break-word;
|
| 507 |
+
word-break: break-word;
|
| 508 |
+
font-family: inherit;
|
| 509 |
+
overflow-x: auto;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.tool-output > div,
|
| 513 |
+
.output-preview > div,
|
| 514 |
+
.output-full > div {
|
| 515 |
+
margin: 0;
|
| 516 |
+
padding: 0;
|
| 517 |
+
line-height: var(--line-height);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.tool-output > div:not(.output-preview):not(.output-full),
|
| 521 |
+
.output-preview > div:not(.expand-hint),
|
| 522 |
+
.output-full > div:not(.expand-hint) {
|
| 523 |
+
white-space: pre-wrap;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.tool-output pre {
|
| 527 |
+
margin: 0;
|
| 528 |
+
padding: 0;
|
| 529 |
+
font-family: inherit;
|
| 530 |
+
color: inherit;
|
| 531 |
+
white-space: pre-wrap;
|
| 532 |
+
word-wrap: break-word;
|
| 533 |
+
overflow-wrap: break-word;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
.tool-output code {
|
| 537 |
+
padding: 0;
|
| 538 |
+
background: none;
|
| 539 |
+
color: var(--text);
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.tool-output.expandable {
|
| 543 |
+
cursor: pointer;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
.tool-output.expandable:hover {
|
| 547 |
+
opacity: 0.9;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.tool-output.expandable .output-full {
|
| 551 |
+
display: none;
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
.tool-output.expandable.expanded .output-preview {
|
| 555 |
+
display: none;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.tool-output.expandable.expanded .output-full {
|
| 559 |
+
display: block;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.ansi-line {
|
| 563 |
+
white-space: pre;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.tool-images {
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.tool-image {
|
| 570 |
+
max-width: 100%;
|
| 571 |
+
max-height: 500px;
|
| 572 |
+
border-radius: 4px;
|
| 573 |
+
margin: var(--line-height) 0;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
.expand-hint {
|
| 577 |
+
color: var(--toolOutput);
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
/* Diff */
|
| 581 |
+
.tool-diff {
|
| 582 |
+
font-size: 11px;
|
| 583 |
+
overflow-x: auto;
|
| 584 |
+
white-space: pre;
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.diff-added { color: var(--toolDiffAdded); }
|
| 588 |
+
.diff-removed { color: var(--toolDiffRemoved); }
|
| 589 |
+
.diff-context { color: var(--toolDiffContext); }
|
| 590 |
+
|
| 591 |
+
/* Model change */
|
| 592 |
+
.model-change {
|
| 593 |
+
padding: 0 var(--line-height);
|
| 594 |
+
color: var(--dim);
|
| 595 |
+
font-size: 11px;
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.model-name {
|
| 599 |
+
color: var(--borderAccent);
|
| 600 |
+
font-weight: bold;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
/* Compaction / Branch Summary - matches customMessage colors from TUI */
|
| 604 |
+
.compaction {
|
| 605 |
+
background: var(--customMessageBg);
|
| 606 |
+
border-radius: 4px;
|
| 607 |
+
padding: var(--line-height);
|
| 608 |
+
cursor: pointer;
|
| 609 |
+
}
|
| 610 |
+
|
| 611 |
+
.compaction-label {
|
| 612 |
+
color: var(--customMessageLabel);
|
| 613 |
+
font-weight: bold;
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
.compaction-collapsed {
|
| 617 |
+
color: var(--customMessageText);
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.compaction-content {
|
| 621 |
+
display: none;
|
| 622 |
+
color: var(--customMessageText);
|
| 623 |
+
white-space: pre-wrap;
|
| 624 |
+
margin-top: var(--line-height);
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
.compaction.expanded .compaction-collapsed {
|
| 628 |
+
display: none;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.compaction.expanded .compaction-content {
|
| 632 |
+
display: block;
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
+
/* System prompt */
|
| 636 |
+
.system-prompt {
|
| 637 |
+
background: var(--customMessageBg);
|
| 638 |
+
padding: var(--line-height);
|
| 639 |
+
border-radius: 4px;
|
| 640 |
+
margin-bottom: var(--line-height);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.system-prompt.expandable {
|
| 644 |
+
cursor: pointer;
|
| 645 |
+
}
|
| 646 |
+
|
| 647 |
+
.system-prompt-header {
|
| 648 |
+
font-weight: bold;
|
| 649 |
+
color: var(--customMessageLabel);
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.system-prompt-preview {
|
| 653 |
+
color: var(--customMessageText);
|
| 654 |
+
white-space: pre-wrap;
|
| 655 |
+
word-wrap: break-word;
|
| 656 |
+
font-size: 11px;
|
| 657 |
+
margin-top: var(--line-height);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
.system-prompt-expand-hint {
|
| 661 |
+
color: var(--muted);
|
| 662 |
+
font-style: italic;
|
| 663 |
+
margin-top: 4px;
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
.system-prompt-full {
|
| 667 |
+
display: none;
|
| 668 |
+
color: var(--customMessageText);
|
| 669 |
+
white-space: pre-wrap;
|
| 670 |
+
word-wrap: break-word;
|
| 671 |
+
font-size: 11px;
|
| 672 |
+
margin-top: var(--line-height);
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.system-prompt.expanded .system-prompt-preview,
|
| 676 |
+
.system-prompt.expanded .system-prompt-expand-hint {
|
| 677 |
+
display: none;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.system-prompt.expanded .system-prompt-full {
|
| 681 |
+
display: block;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.system-prompt.provider-prompt {
|
| 685 |
+
border-left: 3px solid var(--warning);
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.system-prompt-note {
|
| 689 |
+
font-size: 10px;
|
| 690 |
+
font-style: italic;
|
| 691 |
+
color: var(--muted);
|
| 692 |
+
margin-top: 4px;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
/* Tools list */
|
| 696 |
+
.tools-list {
|
| 697 |
+
background: var(--customMessageBg);
|
| 698 |
+
padding: var(--line-height);
|
| 699 |
+
border-radius: 4px;
|
| 700 |
+
margin-bottom: var(--line-height);
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.tools-header {
|
| 704 |
+
font-weight: bold;
|
| 705 |
+
color: var(--customMessageLabel);
|
| 706 |
+
margin-bottom: var(--line-height);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.tool-item {
|
| 710 |
+
font-size: 11px;
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.tool-item-name {
|
| 714 |
+
font-weight: bold;
|
| 715 |
+
color: var(--text);
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
.tool-item-desc {
|
| 719 |
+
color: var(--dim);
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
.tool-params-hint {
|
| 723 |
+
color: var(--muted);
|
| 724 |
+
font-style: italic;
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
.tool-item:has(.tool-params-hint) {
|
| 728 |
+
cursor: pointer;
|
| 729 |
+
}
|
| 730 |
+
|
| 731 |
+
.tool-params-hint::after {
|
| 732 |
+
content: '[click to show parameters]';
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.tool-item.params-expanded .tool-params-hint::after {
|
| 736 |
+
content: '[hide parameters]';
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.tool-params-content {
|
| 740 |
+
display: none;
|
| 741 |
+
margin-top: 4px;
|
| 742 |
+
margin-left: 12px;
|
| 743 |
+
padding-left: 8px;
|
| 744 |
+
border-left: 1px solid var(--dim);
|
| 745 |
+
}
|
| 746 |
+
|
| 747 |
+
.tool-item.params-expanded .tool-params-content {
|
| 748 |
+
display: block;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
.tool-param {
|
| 752 |
+
margin-bottom: 4px;
|
| 753 |
+
font-size: 11px;
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
.tool-param-name {
|
| 757 |
+
font-weight: bold;
|
| 758 |
+
color: var(--text);
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
.tool-param-type {
|
| 762 |
+
color: var(--dim);
|
| 763 |
+
font-style: italic;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.tool-param-required {
|
| 767 |
+
color: var(--warning, #e8a838);
|
| 768 |
+
font-size: 10px;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.tool-param-optional {
|
| 772 |
+
color: var(--dim);
|
| 773 |
+
font-size: 10px;
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
.tool-param-desc {
|
| 777 |
+
color: var(--dim);
|
| 778 |
+
margin-left: 8px;
|
| 779 |
+
}
|
| 780 |
+
|
| 781 |
+
/* Hook/custom messages */
|
| 782 |
+
.hook-message {
|
| 783 |
+
background: var(--customMessageBg);
|
| 784 |
+
color: var(--customMessageText);
|
| 785 |
+
padding: var(--line-height);
|
| 786 |
+
border-radius: 4px;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.hook-type {
|
| 790 |
+
color: var(--customMessageLabel);
|
| 791 |
+
font-weight: bold;
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
/* Skill invocation - matches compaction style (clickable, collapsed by default) */
|
| 795 |
+
.skill-invocation {
|
| 796 |
+
background: var(--customMessageBg);
|
| 797 |
+
border-radius: 4px;
|
| 798 |
+
padding: var(--line-height);
|
| 799 |
+
cursor: pointer;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.skill-invocation-label {
|
| 803 |
+
color: var(--customMessageLabel);
|
| 804 |
+
font-weight: bold;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.skill-invocation-collapsed {
|
| 808 |
+
color: var(--customMessageText);
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.skill-invocation-content {
|
| 812 |
+
display: none;
|
| 813 |
+
color: var(--customMessageText);
|
| 814 |
+
margin-top: var(--line-height);
|
| 815 |
+
}
|
| 816 |
+
|
| 817 |
+
.skill-invocation.expanded .skill-invocation-collapsed {
|
| 818 |
+
display: none;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
.skill-invocation.expanded .skill-invocation-content {
|
| 822 |
+
display: block;
|
| 823 |
+
}
|
| 824 |
+
|
| 825 |
+
.skill-invocation + .user-message {
|
| 826 |
+
margin-top: var(--line-height);
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
.skill-user-entry {
|
| 830 |
+
position: relative;
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
/* Branch summary */
|
| 834 |
+
.branch-summary {
|
| 835 |
+
background: var(--customMessageBg);
|
| 836 |
+
padding: var(--line-height);
|
| 837 |
+
border-radius: 4px;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.branch-summary-header {
|
| 841 |
+
font-weight: bold;
|
| 842 |
+
color: var(--borderAccent);
|
| 843 |
+
}
|
| 844 |
+
|
| 845 |
+
/* Error */
|
| 846 |
+
.error-text {
|
| 847 |
+
color: var(--error);
|
| 848 |
+
padding: 0 var(--line-height);
|
| 849 |
+
}
|
| 850 |
+
.tool-error {
|
| 851 |
+
color: var(--error);
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
/* Images */
|
| 855 |
+
.message-images {
|
| 856 |
+
margin-bottom: 12px;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
.message-image {
|
| 860 |
+
max-width: 100%;
|
| 861 |
+
max-height: 400px;
|
| 862 |
+
border-radius: 4px;
|
| 863 |
+
margin: var(--line-height) 0;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
/* Markdown content */
|
| 867 |
+
.markdown-content h1,
|
| 868 |
+
.markdown-content h2,
|
| 869 |
+
.markdown-content h3,
|
| 870 |
+
.markdown-content h4,
|
| 871 |
+
.markdown-content h5,
|
| 872 |
+
.markdown-content h6 {
|
| 873 |
+
color: var(--mdHeading);
|
| 874 |
+
margin: var(--line-height) 0 0 0;
|
| 875 |
+
font-weight: bold;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.markdown-content h1 { font-size: 1em; }
|
| 879 |
+
.markdown-content h2 { font-size: 1em; }
|
| 880 |
+
.markdown-content h3 { font-size: 1em; }
|
| 881 |
+
.markdown-content h4 { font-size: 1em; }
|
| 882 |
+
.markdown-content h5 { font-size: 1em; }
|
| 883 |
+
.markdown-content h6 { font-size: 1em; }
|
| 884 |
+
.markdown-content p { margin: 0; }
|
| 885 |
+
.markdown-content p + p { margin-top: var(--line-height); }
|
| 886 |
+
|
| 887 |
+
.markdown-content a {
|
| 888 |
+
color: var(--mdLink);
|
| 889 |
+
text-decoration: underline;
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
.markdown-content code {
|
| 893 |
+
background: rgba(128, 128, 128, 0.2);
|
| 894 |
+
color: var(--mdCode);
|
| 895 |
+
padding: 0 4px;
|
| 896 |
+
border-radius: 3px;
|
| 897 |
+
font-family: inherit;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
.markdown-content pre {
|
| 901 |
+
background: transparent;
|
| 902 |
+
margin: var(--line-height) 0;
|
| 903 |
+
overflow-x: auto;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
.markdown-content pre code {
|
| 907 |
+
display: block;
|
| 908 |
+
background: none;
|
| 909 |
+
color: var(--text);
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
.markdown-content blockquote {
|
| 913 |
+
border-left: 3px solid var(--mdQuoteBorder);
|
| 914 |
+
padding-left: var(--line-height);
|
| 915 |
+
margin: var(--line-height) 0;
|
| 916 |
+
color: var(--mdQuote);
|
| 917 |
+
font-style: italic;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.markdown-content ul,
|
| 921 |
+
.markdown-content ol {
|
| 922 |
+
margin: var(--line-height) 0;
|
| 923 |
+
padding-left: calc(var(--line-height) * 2);
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
.markdown-content li { margin: 0; }
|
| 927 |
+
.markdown-content li::marker { color: var(--mdListBullet); }
|
| 928 |
+
|
| 929 |
+
.markdown-content hr {
|
| 930 |
+
border: none;
|
| 931 |
+
border-top: 1px solid var(--mdHr);
|
| 932 |
+
margin: var(--line-height) 0;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
.markdown-content table {
|
| 936 |
+
border-collapse: collapse;
|
| 937 |
+
margin: 0.5em 0;
|
| 938 |
+
width: 100%;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
.markdown-content th,
|
| 942 |
+
.markdown-content td {
|
| 943 |
+
border: 1px solid var(--mdCodeBlockBorder);
|
| 944 |
+
padding: 6px 10px;
|
| 945 |
+
text-align: left;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.markdown-content th {
|
| 949 |
+
background: rgba(128, 128, 128, 0.1);
|
| 950 |
+
font-weight: bold;
|
| 951 |
+
}
|
| 952 |
+
|
| 953 |
+
.markdown-content img {
|
| 954 |
+
max-width: 100%;
|
| 955 |
+
border-radius: 4px;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
/* Syntax highlighting */
|
| 959 |
+
.hljs { background: transparent; color: var(--text); }
|
| 960 |
+
.hljs-comment, .hljs-quote { color: var(--syntaxComment); }
|
| 961 |
+
.hljs-keyword, .hljs-selector-tag { color: var(--syntaxKeyword); }
|
| 962 |
+
.hljs-number, .hljs-literal { color: var(--syntaxNumber); }
|
| 963 |
+
.hljs-string, .hljs-doctag { color: var(--syntaxString); }
|
| 964 |
+
/* Function names: hljs v11 uses .hljs-title.function_ compound class */
|
| 965 |
+
.hljs-function, .hljs-title, .hljs-title.function_, .hljs-section, .hljs-name { color: var(--syntaxFunction); }
|
| 966 |
+
/* Types: hljs v11 uses .hljs-title.class_ for class names */
|
| 967 |
+
.hljs-type, .hljs-class, .hljs-title.class_, .hljs-built_in { color: var(--syntaxType); }
|
| 968 |
+
.hljs-attr, .hljs-variable, .hljs-variable.language_, .hljs-params, .hljs-property { color: var(--syntaxVariable); }
|
| 969 |
+
.hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string { color: var(--syntaxKeyword); }
|
| 970 |
+
.hljs-operator { color: var(--syntaxOperator); }
|
| 971 |
+
.hljs-punctuation { color: var(--syntaxPunctuation); }
|
| 972 |
+
.hljs-subst { color: var(--text); }
|
| 973 |
+
|
| 974 |
+
/* Footer */
|
| 975 |
+
.footer {
|
| 976 |
+
margin-top: 48px;
|
| 977 |
+
padding: 20px;
|
| 978 |
+
text-align: center;
|
| 979 |
+
color: var(--dim);
|
| 980 |
+
font-size: 10px;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
/* Mobile */
|
| 984 |
+
#hamburger {
|
| 985 |
+
display: none;
|
| 986 |
+
position: fixed;
|
| 987 |
+
top: 10px;
|
| 988 |
+
left: 10px;
|
| 989 |
+
z-index: 100;
|
| 990 |
+
padding: 3px 8px;
|
| 991 |
+
font-size: 12px;
|
| 992 |
+
font-family: inherit;
|
| 993 |
+
background: transparent;
|
| 994 |
+
color: var(--muted);
|
| 995 |
+
border: 1px solid var(--dim);
|
| 996 |
+
border-radius: 3px;
|
| 997 |
+
cursor: pointer;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
#hamburger:hover {
|
| 1001 |
+
color: var(--text);
|
| 1002 |
+
border-color: var(--text);
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
|
| 1006 |
+
|
| 1007 |
+
#sidebar-overlay {
|
| 1008 |
+
display: none;
|
| 1009 |
+
position: fixed;
|
| 1010 |
+
top: 0;
|
| 1011 |
+
left: 0;
|
| 1012 |
+
right: 0;
|
| 1013 |
+
bottom: 0;
|
| 1014 |
+
background: rgba(0, 0, 0, 0.5);
|
| 1015 |
+
z-index: 98;
|
| 1016 |
+
}
|
| 1017 |
+
|
| 1018 |
+
@media (max-width: 900px) {
|
| 1019 |
+
#sidebar {
|
| 1020 |
+
position: fixed;
|
| 1021 |
+
left: 0;
|
| 1022 |
+
width: min(var(--sidebar-width), 100vw);
|
| 1023 |
+
min-width: min(var(--sidebar-width), 100vw);
|
| 1024 |
+
max-width: min(var(--sidebar-width), 100vw);
|
| 1025 |
+
top: 0;
|
| 1026 |
+
bottom: 0;
|
| 1027 |
+
height: 100vh;
|
| 1028 |
+
z-index: 99;
|
| 1029 |
+
transform: translateX(-100%);
|
| 1030 |
+
transition: transform 0.3s;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
#sidebar.open {
|
| 1034 |
+
transform: translateX(0);
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
#sidebar-resizer {
|
| 1038 |
+
display: none;
|
| 1039 |
+
}
|
| 1040 |
+
|
| 1041 |
+
#sidebar-overlay.open {
|
| 1042 |
+
display: block;
|
| 1043 |
+
}
|
| 1044 |
+
|
| 1045 |
+
#hamburger {
|
| 1046 |
+
display: block;
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
.sidebar-close {
|
| 1050 |
+
display: block;
|
| 1051 |
+
}
|
| 1052 |
+
|
| 1053 |
+
#content {
|
| 1054 |
+
padding: var(--line-height) 16px;
|
| 1055 |
+
}
|
| 1056 |
+
|
| 1057 |
+
#content > * {
|
| 1058 |
+
max-width: 100%;
|
| 1059 |
+
}
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
@media print {
|
| 1063 |
+
#sidebar, #sidebar-resizer, #sidebar-toggle { display: none !important; }
|
| 1064 |
+
body { background: white; color: black; }
|
| 1065 |
+
#content { max-width: none; }
|
| 1066 |
+
}
|
packages/coding-agent/src/core/export-html/template.html
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Session Export</title>
|
| 7 |
+
<style>
|
| 8 |
+
{{CSS}}
|
| 9 |
+
</style>
|
| 10 |
+
</head>
|
| 11 |
+
<body>
|
| 12 |
+
<button id="hamburger" title="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><rect x="5" y="6" width="2" height="12"/><path d="M6 12h10c1 0 2 0 2-2V8"/></svg></button>
|
| 13 |
+
<div id="sidebar-overlay"></div>
|
| 14 |
+
<div id="app">
|
| 15 |
+
<aside id="sidebar">
|
| 16 |
+
<div class="sidebar-header">
|
| 17 |
+
<div class="sidebar-controls">
|
| 18 |
+
<input type="text" class="sidebar-search" id="tree-search" placeholder="Search...">
|
| 19 |
+
</div>
|
| 20 |
+
<div class="sidebar-filters">
|
| 21 |
+
<button class="filter-btn active" data-filter="default" title="Hide settings entries">Default</button>
|
| 22 |
+
<button class="filter-btn" data-filter="no-tools" title="Default minus tool results">No-tools</button>
|
| 23 |
+
<button class="filter-btn" data-filter="user-only" title="Only user messages">User</button>
|
| 24 |
+
<button class="filter-btn" data-filter="labeled-only" title="Only labeled entries">Labeled</button>
|
| 25 |
+
<button class="filter-btn" data-filter="all" title="Show everything">All</button>
|
| 26 |
+
<button class="sidebar-close" id="sidebar-close" title="Close">✕</button>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="tree-container" id="tree-container"></div>
|
| 30 |
+
<div class="tree-status" id="tree-status"></div>
|
| 31 |
+
</aside>
|
| 32 |
+
<div id="sidebar-resizer" role="separator" aria-orientation="vertical" aria-label="Resize session tree sidebar"></div>
|
| 33 |
+
<main id="content">
|
| 34 |
+
<div id="header-container"></div>
|
| 35 |
+
<div id="messages"></div>
|
| 36 |
+
</main>
|
| 37 |
+
<div id="image-modal" class="image-modal">
|
| 38 |
+
<img id="modal-image" src="" alt="">
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
|
| 43 |
+
|
| 44 |
+
<!-- Vendored libraries -->
|
| 45 |
+
<script>{{MARKED_JS}}</script>
|
| 46 |
+
|
| 47 |
+
<!-- highlight.js -->
|
| 48 |
+
<script>{{HIGHLIGHT_JS}}</script>
|
| 49 |
+
|
| 50 |
+
<!-- Main application code -->
|
| 51 |
+
<script>
|
| 52 |
+
{{JS}}
|
| 53 |
+
</script>
|
| 54 |
+
</body>
|
| 55 |
+
</html>
|
packages/coding-agent/src/core/export-html/template.js
ADDED
|
@@ -0,0 +1,1864 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
(function() {
|
| 2 |
+
'use strict';
|
| 3 |
+
|
| 4 |
+
// ============================================================
|
| 5 |
+
// DATA LOADING
|
| 6 |
+
// ============================================================
|
| 7 |
+
|
| 8 |
+
const base64 = document.getElementById('session-data').textContent;
|
| 9 |
+
const binary = atob(base64);
|
| 10 |
+
const bytes = new Uint8Array(binary.length);
|
| 11 |
+
for (let i = 0; i < binary.length; i++) {
|
| 12 |
+
bytes[i] = binary.charCodeAt(i);
|
| 13 |
+
}
|
| 14 |
+
const data = JSON.parse(new TextDecoder('utf-8').decode(bytes));
|
| 15 |
+
const { header, entries, leafId: defaultLeafId, systemPrompt, tools, renderedTools } = data;
|
| 16 |
+
|
| 17 |
+
// ============================================================
|
| 18 |
+
// URL PARAMETER HANDLING
|
| 19 |
+
// ============================================================
|
| 20 |
+
|
| 21 |
+
// Parse URL parameters for deep linking: leafId and targetId
|
| 22 |
+
// Check for injected params (when loaded in iframe via srcdoc) or use window.location
|
| 23 |
+
const injectedParams = document.querySelector('meta[name="pi-url-params"]');
|
| 24 |
+
const searchString = injectedParams ? injectedParams.content : window.location.search.substring(1);
|
| 25 |
+
const urlParams = new URLSearchParams(searchString);
|
| 26 |
+
const urlLeafId = urlParams.get('leafId');
|
| 27 |
+
const urlTargetId = urlParams.get('targetId');
|
| 28 |
+
// Use URL leafId if provided, otherwise fall back to session default
|
| 29 |
+
const leafId = urlLeafId || defaultLeafId;
|
| 30 |
+
|
| 31 |
+
// ============================================================
|
| 32 |
+
// DATA STRUCTURES
|
| 33 |
+
// ============================================================
|
| 34 |
+
|
| 35 |
+
// Entry lookup by ID
|
| 36 |
+
const byId = new Map();
|
| 37 |
+
for (const entry of entries) {
|
| 38 |
+
byId.set(entry.id, entry);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Tool call lookup (toolCallId -> {name, arguments})
|
| 42 |
+
const toolCallMap = new Map();
|
| 43 |
+
for (const entry of entries) {
|
| 44 |
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
| 45 |
+
const content = entry.message.content;
|
| 46 |
+
if (Array.isArray(content)) {
|
| 47 |
+
for (const block of content) {
|
| 48 |
+
if (block.type === 'toolCall') {
|
| 49 |
+
toolCallMap.set(block.id, { name: block.name, arguments: block.arguments });
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// Label lookup (entryId -> label string)
|
| 57 |
+
// Labels are stored in 'label' entries that reference their target via targetId
|
| 58 |
+
const labelMap = new Map();
|
| 59 |
+
for (const entry of entries) {
|
| 60 |
+
if (entry.type === 'label' && entry.targetId && entry.label) {
|
| 61 |
+
labelMap.set(entry.targetId, entry.label);
|
| 62 |
+
}
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ============================================================
|
| 66 |
+
// TREE DATA PREPARATION (no DOM, pure data)
|
| 67 |
+
// ============================================================
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Build tree structure from flat entries.
|
| 71 |
+
* Returns array of root nodes, each with { entry, children, label }.
|
| 72 |
+
*/
|
| 73 |
+
function buildTree() {
|
| 74 |
+
const nodeMap = new Map();
|
| 75 |
+
const roots = [];
|
| 76 |
+
|
| 77 |
+
// Create nodes
|
| 78 |
+
for (const entry of entries) {
|
| 79 |
+
nodeMap.set(entry.id, {
|
| 80 |
+
entry,
|
| 81 |
+
children: [],
|
| 82 |
+
label: labelMap.get(entry.id)
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// Build parent-child relationships
|
| 87 |
+
for (const entry of entries) {
|
| 88 |
+
const node = nodeMap.get(entry.id);
|
| 89 |
+
if (entry.parentId === null || entry.parentId === undefined || entry.parentId === entry.id) {
|
| 90 |
+
roots.push(node);
|
| 91 |
+
} else {
|
| 92 |
+
const parent = nodeMap.get(entry.parentId);
|
| 93 |
+
if (parent) {
|
| 94 |
+
parent.children.push(node);
|
| 95 |
+
} else {
|
| 96 |
+
roots.push(node);
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
// Sort children by timestamp
|
| 102 |
+
function sortChildren(node) {
|
| 103 |
+
node.children.sort((a, b) =>
|
| 104 |
+
new Date(a.entry.timestamp).getTime() - new Date(b.entry.timestamp).getTime()
|
| 105 |
+
);
|
| 106 |
+
node.children.forEach(sortChildren);
|
| 107 |
+
}
|
| 108 |
+
roots.forEach(sortChildren);
|
| 109 |
+
|
| 110 |
+
return roots;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
/**
|
| 114 |
+
* Build set of entry IDs on path from root to target.
|
| 115 |
+
*/
|
| 116 |
+
function buildActivePathIds(targetId) {
|
| 117 |
+
const ids = new Set();
|
| 118 |
+
let current = byId.get(targetId);
|
| 119 |
+
while (current) {
|
| 120 |
+
ids.add(current.id);
|
| 121 |
+
// Stop if no parent or self-referencing (root)
|
| 122 |
+
if (!current.parentId || current.parentId === current.id) {
|
| 123 |
+
break;
|
| 124 |
+
}
|
| 125 |
+
current = byId.get(current.parentId);
|
| 126 |
+
}
|
| 127 |
+
return ids;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/**
|
| 131 |
+
* Get array of entries from root to target (the conversation path).
|
| 132 |
+
*/
|
| 133 |
+
function getPath(targetId) {
|
| 134 |
+
const path = [];
|
| 135 |
+
let current = byId.get(targetId);
|
| 136 |
+
while (current) {
|
| 137 |
+
path.unshift(current);
|
| 138 |
+
// Stop if no parent or self-referencing (root)
|
| 139 |
+
if (!current.parentId || current.parentId === current.id) {
|
| 140 |
+
break;
|
| 141 |
+
}
|
| 142 |
+
current = byId.get(current.parentId);
|
| 143 |
+
}
|
| 144 |
+
return path;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
// Tree node lookup for finding leaves
|
| 148 |
+
let treeNodeMap = null;
|
| 149 |
+
|
| 150 |
+
/**
|
| 151 |
+
* Find the newest leaf node reachable from a given node.
|
| 152 |
+
* This allows clicking any node in a branch to show the full branch.
|
| 153 |
+
* Children are sorted by timestamp, so the newest is always last.
|
| 154 |
+
*/
|
| 155 |
+
function findNewestLeaf(nodeId) {
|
| 156 |
+
// Build tree node map lazily
|
| 157 |
+
if (!treeNodeMap) {
|
| 158 |
+
treeNodeMap = new Map();
|
| 159 |
+
const tree = buildTree();
|
| 160 |
+
function mapNodes(node) {
|
| 161 |
+
treeNodeMap.set(node.entry.id, node);
|
| 162 |
+
node.children.forEach(mapNodes);
|
| 163 |
+
}
|
| 164 |
+
tree.forEach(mapNodes);
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
const node = treeNodeMap.get(nodeId);
|
| 168 |
+
if (!node) return nodeId;
|
| 169 |
+
|
| 170 |
+
// Follow the newest (last) child at each level
|
| 171 |
+
let current = node;
|
| 172 |
+
while (current.children.length > 0) {
|
| 173 |
+
current = current.children[current.children.length - 1];
|
| 174 |
+
}
|
| 175 |
+
return current.entry.id;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
/**
|
| 179 |
+
* Flatten tree into list with indentation and connector info.
|
| 180 |
+
* Returns array of { node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots }.
|
| 181 |
+
* Matches tree-selector.ts logic exactly.
|
| 182 |
+
*/
|
| 183 |
+
function flattenTree(roots, activePathIds) {
|
| 184 |
+
const result = [];
|
| 185 |
+
const multipleRoots = roots.length > 1;
|
| 186 |
+
|
| 187 |
+
// Mark which subtrees contain the active leaf
|
| 188 |
+
const containsActive = new Map();
|
| 189 |
+
function markActive(node) {
|
| 190 |
+
let has = activePathIds.has(node.entry.id);
|
| 191 |
+
for (const child of node.children) {
|
| 192 |
+
if (markActive(child)) has = true;
|
| 193 |
+
}
|
| 194 |
+
containsActive.set(node, has);
|
| 195 |
+
return has;
|
| 196 |
+
}
|
| 197 |
+
roots.forEach(markActive);
|
| 198 |
+
|
| 199 |
+
// Stack: [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
|
| 200 |
+
const stack = [];
|
| 201 |
+
|
| 202 |
+
// Add roots (prioritize branch containing active leaf)
|
| 203 |
+
const orderedRoots = [...roots].sort((a, b) =>
|
| 204 |
+
Number(containsActive.get(b)) - Number(containsActive.get(a))
|
| 205 |
+
);
|
| 206 |
+
for (let i = orderedRoots.length - 1; i >= 0; i--) {
|
| 207 |
+
const isLast = i === orderedRoots.length - 1;
|
| 208 |
+
stack.push([orderedRoots[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, isLast, [], multipleRoots]);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
while (stack.length > 0) {
|
| 212 |
+
const [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
|
| 213 |
+
|
| 214 |
+
result.push({ node, indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots });
|
| 215 |
+
|
| 216 |
+
const children = node.children;
|
| 217 |
+
const multipleChildren = children.length > 1;
|
| 218 |
+
|
| 219 |
+
// Order children (active branch first)
|
| 220 |
+
const orderedChildren = [...children].sort((a, b) =>
|
| 221 |
+
Number(containsActive.get(b)) - Number(containsActive.get(a))
|
| 222 |
+
);
|
| 223 |
+
|
| 224 |
+
// Calculate child indent (matches tree-selector.ts)
|
| 225 |
+
let childIndent;
|
| 226 |
+
if (multipleChildren) {
|
| 227 |
+
// Parent branches: children get +1
|
| 228 |
+
childIndent = indent + 1;
|
| 229 |
+
} else if (justBranched && indent > 0) {
|
| 230 |
+
// First generation after a branch: +1 for visual grouping
|
| 231 |
+
childIndent = indent + 1;
|
| 232 |
+
} else {
|
| 233 |
+
// Single-child chain: stay flat
|
| 234 |
+
childIndent = indent;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// Build gutters for children
|
| 238 |
+
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
| 239 |
+
const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
| 240 |
+
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
| 241 |
+
const childGutters = connectorDisplayed
|
| 242 |
+
? [...gutters, { position: connectorPosition, show: !isLast }]
|
| 243 |
+
: gutters;
|
| 244 |
+
|
| 245 |
+
// Add children in reverse order for stack
|
| 246 |
+
for (let i = orderedChildren.length - 1; i >= 0; i--) {
|
| 247 |
+
const childIsLast = i === orderedChildren.length - 1;
|
| 248 |
+
stack.push([orderedChildren[i], childIndent, multipleChildren, multipleChildren, childIsLast, childGutters, false]);
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
return result;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/**
|
| 256 |
+
* Build ASCII prefix string for tree node.
|
| 257 |
+
*/
|
| 258 |
+
function buildTreePrefix(flatNode) {
|
| 259 |
+
const { indent, showConnector, isLast, gutters, isVirtualRootChild, multipleRoots } = flatNode;
|
| 260 |
+
const displayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
| 261 |
+
const connector = showConnector && !isVirtualRootChild ? (isLast ? '└─ ' : '├─ ') : '';
|
| 262 |
+
const connectorPosition = connector ? displayIndent - 1 : -1;
|
| 263 |
+
|
| 264 |
+
const totalChars = displayIndent * 3;
|
| 265 |
+
const prefixChars = [];
|
| 266 |
+
for (let i = 0; i < totalChars; i++) {
|
| 267 |
+
const level = Math.floor(i / 3);
|
| 268 |
+
const posInLevel = i % 3;
|
| 269 |
+
|
| 270 |
+
const gutter = gutters.find(g => g.position === level);
|
| 271 |
+
if (gutter) {
|
| 272 |
+
prefixChars.push(posInLevel === 0 ? (gutter.show ? '│' : ' ') : ' ');
|
| 273 |
+
} else if (connector && level === connectorPosition) {
|
| 274 |
+
if (posInLevel === 0) {
|
| 275 |
+
prefixChars.push(isLast ? '└' : '├');
|
| 276 |
+
} else if (posInLevel === 1) {
|
| 277 |
+
prefixChars.push('─');
|
| 278 |
+
} else {
|
| 279 |
+
prefixChars.push(' ');
|
| 280 |
+
}
|
| 281 |
+
} else {
|
| 282 |
+
prefixChars.push(' ');
|
| 283 |
+
}
|
| 284 |
+
}
|
| 285 |
+
return prefixChars.join('');
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// ============================================================
|
| 289 |
+
// FILTERING (pure data)
|
| 290 |
+
// ============================================================
|
| 291 |
+
|
| 292 |
+
let filterMode = 'default';
|
| 293 |
+
let searchQuery = '';
|
| 294 |
+
|
| 295 |
+
function hasTextContent(content) {
|
| 296 |
+
if (typeof content === 'string') return content.trim().length > 0;
|
| 297 |
+
if (Array.isArray(content)) {
|
| 298 |
+
for (const c of content) {
|
| 299 |
+
if (c.type === 'text' && c.text && c.text.trim().length > 0) return true;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
return false;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
function extractContent(content) {
|
| 306 |
+
if (typeof content === 'string') return content;
|
| 307 |
+
if (Array.isArray(content)) {
|
| 308 |
+
return content
|
| 309 |
+
.filter(c => c.type === 'text' && c.text)
|
| 310 |
+
.map(c => c.text)
|
| 311 |
+
.join('');
|
| 312 |
+
}
|
| 313 |
+
return '';
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
/**
|
| 317 |
+
* Parse a skill block from message text.
|
| 318 |
+
* Returns null if the text doesn't contain a skill block.
|
| 319 |
+
* Matches the format: <skill name="..." location="...">\n...\n</skill>\n\nuser message
|
| 320 |
+
*/
|
| 321 |
+
function parseSkillBlock(text) {
|
| 322 |
+
const match = text.match(/^<skill name="([^"]+)" location="([^"]+)">\n([\s\S]*?)\n<\/skill>(?:\n\n([\s\S]+))?$/);
|
| 323 |
+
if (!match) return null;
|
| 324 |
+
return {
|
| 325 |
+
name: match[1],
|
| 326 |
+
location: match[2],
|
| 327 |
+
content: match[3],
|
| 328 |
+
userMessage: match[4]?.trim() || undefined,
|
| 329 |
+
};
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function getSearchableText(entry, label) {
|
| 333 |
+
const parts = [];
|
| 334 |
+
if (label) parts.push(label);
|
| 335 |
+
|
| 336 |
+
switch (entry.type) {
|
| 337 |
+
case 'message': {
|
| 338 |
+
const msg = entry.message;
|
| 339 |
+
parts.push(msg.role);
|
| 340 |
+
if (msg.content) parts.push(extractContent(msg.content));
|
| 341 |
+
if (msg.role === 'bashExecution' && msg.command) parts.push(msg.command);
|
| 342 |
+
break;
|
| 343 |
+
}
|
| 344 |
+
case 'custom_message':
|
| 345 |
+
parts.push(entry.customType);
|
| 346 |
+
parts.push(typeof entry.content === 'string' ? entry.content : extractContent(entry.content));
|
| 347 |
+
break;
|
| 348 |
+
case 'compaction':
|
| 349 |
+
parts.push('compaction');
|
| 350 |
+
break;
|
| 351 |
+
case 'branch_summary':
|
| 352 |
+
parts.push('branch summary', entry.summary);
|
| 353 |
+
break;
|
| 354 |
+
case 'model_change':
|
| 355 |
+
parts.push('model', entry.modelId);
|
| 356 |
+
break;
|
| 357 |
+
case 'thinking_level_change':
|
| 358 |
+
parts.push('thinking', entry.thinkingLevel);
|
| 359 |
+
break;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
return parts.join(' ').toLowerCase();
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/**
|
| 366 |
+
* Filter flat nodes based on current filterMode and searchQuery.
|
| 367 |
+
*/
|
| 368 |
+
function filterNodes(flatNodes, currentLeafId) {
|
| 369 |
+
const searchTokens = searchQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
| 370 |
+
|
| 371 |
+
const filtered = flatNodes.filter(flatNode => {
|
| 372 |
+
const entry = flatNode.node.entry;
|
| 373 |
+
const label = flatNode.node.label;
|
| 374 |
+
const isCurrentLeaf = entry.id === currentLeafId;
|
| 375 |
+
|
| 376 |
+
// Always show current leaf
|
| 377 |
+
if (isCurrentLeaf) return true;
|
| 378 |
+
|
| 379 |
+
// Hide assistant messages with only tool calls (no text) unless error/aborted
|
| 380 |
+
if (entry.type === 'message' && entry.message.role === 'assistant') {
|
| 381 |
+
const msg = entry.message;
|
| 382 |
+
const hasText = hasTextContent(msg.content);
|
| 383 |
+
const isErrorOrAborted = msg.stopReason && msg.stopReason !== 'stop' && msg.stopReason !== 'toolUse';
|
| 384 |
+
if (!hasText && !isErrorOrAborted) return false;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// Apply filter mode
|
| 388 |
+
const isSettingsEntry = ['label', 'custom', 'model_change', 'thinking_level_change'].includes(entry.type);
|
| 389 |
+
let passesFilter = true;
|
| 390 |
+
|
| 391 |
+
switch (filterMode) {
|
| 392 |
+
case 'user-only':
|
| 393 |
+
passesFilter = entry.type === 'message' && entry.message.role === 'user';
|
| 394 |
+
break;
|
| 395 |
+
case 'no-tools':
|
| 396 |
+
passesFilter = !isSettingsEntry && !(entry.type === 'message' && entry.message.role === 'toolResult');
|
| 397 |
+
break;
|
| 398 |
+
case 'labeled-only':
|
| 399 |
+
passesFilter = label !== undefined;
|
| 400 |
+
break;
|
| 401 |
+
case 'all':
|
| 402 |
+
passesFilter = true;
|
| 403 |
+
break;
|
| 404 |
+
default: // 'default'
|
| 405 |
+
passesFilter = !isSettingsEntry;
|
| 406 |
+
break;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
if (!passesFilter) return false;
|
| 410 |
+
|
| 411 |
+
// Apply search filter
|
| 412 |
+
if (searchTokens.length > 0) {
|
| 413 |
+
const nodeText = getSearchableText(entry, label);
|
| 414 |
+
if (!searchTokens.every(t => nodeText.includes(t))) return false;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
return true;
|
| 418 |
+
});
|
| 419 |
+
|
| 420 |
+
// Recalculate visual structure based on visible tree
|
| 421 |
+
recalculateVisualStructure(filtered, flatNodes);
|
| 422 |
+
|
| 423 |
+
return filtered;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
/**
|
| 427 |
+
* Recompute indentation/connectors for the filtered view
|
| 428 |
+
*
|
| 429 |
+
* Filtering can hide intermediate entries; descendants attach to the nearest visible ancestor.
|
| 430 |
+
* Keep indentation semantics aligned with flattenTree() so single-child chains don't drift right.
|
| 431 |
+
*/
|
| 432 |
+
function recalculateVisualStructure(filteredNodes, allFlatNodes) {
|
| 433 |
+
if (filteredNodes.length === 0) return;
|
| 434 |
+
|
| 435 |
+
const visibleIds = new Set(filteredNodes.map(n => n.node.entry.id));
|
| 436 |
+
|
| 437 |
+
// Build entry map for parent lookup (using full tree)
|
| 438 |
+
const entryMap = new Map();
|
| 439 |
+
for (const flatNode of allFlatNodes) {
|
| 440 |
+
entryMap.set(flatNode.node.entry.id, flatNode);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
// Find nearest visible ancestor for a node
|
| 444 |
+
function findVisibleAncestor(nodeId) {
|
| 445 |
+
let currentId = entryMap.get(nodeId)?.node.entry.parentId;
|
| 446 |
+
while (currentId != null) {
|
| 447 |
+
if (visibleIds.has(currentId)) {
|
| 448 |
+
return currentId;
|
| 449 |
+
}
|
| 450 |
+
currentId = entryMap.get(currentId)?.node.entry.parentId;
|
| 451 |
+
}
|
| 452 |
+
return null;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
// Build visible tree structure
|
| 456 |
+
const visibleParent = new Map();
|
| 457 |
+
const visibleChildren = new Map();
|
| 458 |
+
visibleChildren.set(null, []); // root-level nodes
|
| 459 |
+
|
| 460 |
+
for (const flatNode of filteredNodes) {
|
| 461 |
+
const nodeId = flatNode.node.entry.id;
|
| 462 |
+
const ancestorId = findVisibleAncestor(nodeId);
|
| 463 |
+
visibleParent.set(nodeId, ancestorId);
|
| 464 |
+
|
| 465 |
+
if (!visibleChildren.has(ancestorId)) {
|
| 466 |
+
visibleChildren.set(ancestorId, []);
|
| 467 |
+
}
|
| 468 |
+
visibleChildren.get(ancestorId).push(nodeId);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
// Update multipleRoots based on visible roots
|
| 472 |
+
const visibleRootIds = visibleChildren.get(null);
|
| 473 |
+
const multipleRoots = visibleRootIds.length > 1;
|
| 474 |
+
|
| 475 |
+
// Build a map for quick lookup: nodeId → FlatNode
|
| 476 |
+
const filteredNodeMap = new Map();
|
| 477 |
+
for (const flatNode of filteredNodes) {
|
| 478 |
+
filteredNodeMap.set(flatNode.node.entry.id, flatNode);
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
// DFS traversal of visible tree, applying same indentation rules as flattenTree()
|
| 482 |
+
// Stack items: [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild]
|
| 483 |
+
const stack = [];
|
| 484 |
+
|
| 485 |
+
// Add visible roots in reverse order (to process in forward order via stack)
|
| 486 |
+
for (let i = visibleRootIds.length - 1; i >= 0; i--) {
|
| 487 |
+
const isLast = i === visibleRootIds.length - 1;
|
| 488 |
+
stack.push([
|
| 489 |
+
visibleRootIds[i],
|
| 490 |
+
multipleRoots ? 1 : 0,
|
| 491 |
+
multipleRoots,
|
| 492 |
+
multipleRoots,
|
| 493 |
+
isLast,
|
| 494 |
+
[],
|
| 495 |
+
multipleRoots
|
| 496 |
+
]);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
while (stack.length > 0) {
|
| 500 |
+
const [nodeId, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
|
| 501 |
+
|
| 502 |
+
const flatNode = filteredNodeMap.get(nodeId);
|
| 503 |
+
if (!flatNode) continue;
|
| 504 |
+
|
| 505 |
+
// Update this node's visual properties
|
| 506 |
+
flatNode.indent = indent;
|
| 507 |
+
flatNode.showConnector = showConnector;
|
| 508 |
+
flatNode.isLast = isLast;
|
| 509 |
+
flatNode.gutters = gutters;
|
| 510 |
+
flatNode.isVirtualRootChild = isVirtualRootChild;
|
| 511 |
+
flatNode.multipleRoots = multipleRoots;
|
| 512 |
+
|
| 513 |
+
// Get visible children of this node
|
| 514 |
+
const children = visibleChildren.get(nodeId) || [];
|
| 515 |
+
const multipleChildren = children.length > 1;
|
| 516 |
+
|
| 517 |
+
// Calculate child indent using same rules as flattenTree():
|
| 518 |
+
// - Parent branches (multiple children): children get +1
|
| 519 |
+
// - Just branched and indent > 0: children get +1 for visual grouping
|
| 520 |
+
// - Single-child chain: stay flat
|
| 521 |
+
let childIndent;
|
| 522 |
+
if (multipleChildren) {
|
| 523 |
+
childIndent = indent + 1;
|
| 524 |
+
} else if (justBranched && indent > 0) {
|
| 525 |
+
childIndent = indent + 1;
|
| 526 |
+
} else {
|
| 527 |
+
childIndent = indent;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// Build gutters for children (same logic as flattenTree)
|
| 531 |
+
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
| 532 |
+
const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
| 533 |
+
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
| 534 |
+
const childGutters = connectorDisplayed
|
| 535 |
+
? [...gutters, { position: connectorPosition, show: !isLast }]
|
| 536 |
+
: gutters;
|
| 537 |
+
|
| 538 |
+
// Add children in reverse order (to process in forward order via stack)
|
| 539 |
+
for (let i = children.length - 1; i >= 0; i--) {
|
| 540 |
+
const childIsLast = i === children.length - 1;
|
| 541 |
+
stack.push([
|
| 542 |
+
children[i],
|
| 543 |
+
childIndent,
|
| 544 |
+
multipleChildren,
|
| 545 |
+
multipleChildren,
|
| 546 |
+
childIsLast,
|
| 547 |
+
childGutters,
|
| 548 |
+
false
|
| 549 |
+
]);
|
| 550 |
+
}
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
// ============================================================
|
| 555 |
+
// TREE DISPLAY TEXT (pure data -> string)
|
| 556 |
+
// ============================================================
|
| 557 |
+
|
| 558 |
+
function shortenPath(p) {
|
| 559 |
+
if (typeof p !== 'string') return '';
|
| 560 |
+
if (p.startsWith('/Users/')) {
|
| 561 |
+
const parts = p.split('/');
|
| 562 |
+
if (parts.length > 2) return '~' + p.slice(('/Users/' + parts[2]).length);
|
| 563 |
+
}
|
| 564 |
+
if (p.startsWith('/home/')) {
|
| 565 |
+
const parts = p.split('/');
|
| 566 |
+
if (parts.length > 2) return '~' + p.slice(('/home/' + parts[2]).length);
|
| 567 |
+
}
|
| 568 |
+
return p;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
function formatToolCall(name, args) {
|
| 572 |
+
switch (name) {
|
| 573 |
+
case 'read': {
|
| 574 |
+
const path = shortenPath(String(args.path || args.file_path || ''));
|
| 575 |
+
const offset = args.offset;
|
| 576 |
+
const limit = args.limit;
|
| 577 |
+
let display = path;
|
| 578 |
+
if (offset !== undefined || limit !== undefined) {
|
| 579 |
+
const start = offset ?? 1;
|
| 580 |
+
const end = limit !== undefined ? start + limit - 1 : '';
|
| 581 |
+
display += `:${start}${end ? `-${end}` : ''}`;
|
| 582 |
+
}
|
| 583 |
+
return `[read: ${display}]`;
|
| 584 |
+
}
|
| 585 |
+
case 'write':
|
| 586 |
+
return `[write: ${shortenPath(String(args.path || args.file_path || ''))}]`;
|
| 587 |
+
case 'edit':
|
| 588 |
+
return `[edit: ${shortenPath(String(args.path || args.file_path || ''))}]`;
|
| 589 |
+
case 'bash': {
|
| 590 |
+
const rawCmd = String(args.command || '');
|
| 591 |
+
const cmd = rawCmd.replace(/[\n\t]/g, ' ').trim().slice(0, 50);
|
| 592 |
+
return `[bash: ${cmd}${rawCmd.length > 50 ? '...' : ''}]`;
|
| 593 |
+
}
|
| 594 |
+
case 'grep':
|
| 595 |
+
return `[grep: /${args.pattern || ''}/ in ${shortenPath(String(args.path || '.'))}]`;
|
| 596 |
+
case 'find':
|
| 597 |
+
return `[find: ${args.pattern || ''} in ${shortenPath(String(args.path || '.'))}]`;
|
| 598 |
+
case 'ls':
|
| 599 |
+
return `[ls: ${shortenPath(String(args.path || '.'))}]`;
|
| 600 |
+
default: {
|
| 601 |
+
const argsStr = JSON.stringify(args).slice(0, 40);
|
| 602 |
+
return `[${name}: ${argsStr}${JSON.stringify(args).length > 40 ? '...' : ''}]`;
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
function escapeHtml(text) {
|
| 608 |
+
return String(text)
|
| 609 |
+
.replace(/&/g, '&')
|
| 610 |
+
.replace(/</g, '<')
|
| 611 |
+
.replace(/>/g, '>')
|
| 612 |
+
.replace(/"/g, '"')
|
| 613 |
+
.replace(/'/g, ''');
|
| 614 |
+
}
|
| 615 |
+
|
| 616 |
+
function sanitizeMarkdownUrl(value) {
|
| 617 |
+
const href = String(value || '').trim().replace(/[\x00-\x1f\x7f]/g, '');
|
| 618 |
+
if (!href) return href;
|
| 619 |
+
|
| 620 |
+
const scheme = href.match(/^([A-Za-z][A-Za-z0-9+.-]*):/);
|
| 621 |
+
if (scheme && !/^(https?|mailto|tel|ftp)$/i.test(scheme[1])) {
|
| 622 |
+
return null;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
return href;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
/**
|
| 629 |
+
* Truncate string to maxLen chars, append "..." if truncated.
|
| 630 |
+
*/
|
| 631 |
+
function truncate(s, maxLen = 100) {
|
| 632 |
+
if (s.length <= maxLen) return s;
|
| 633 |
+
return s.slice(0, maxLen) + '...';
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
/**
|
| 637 |
+
* Get display text for tree node (returns HTML string).
|
| 638 |
+
*/
|
| 639 |
+
function getTreeNodeDisplayHtml(entry, label) {
|
| 640 |
+
const normalize = s => s.replace(/[\n\t]/g, ' ').trim();
|
| 641 |
+
const labelHtml = label ? `<span class="tree-label">[${escapeHtml(label)}]</span> ` : '';
|
| 642 |
+
|
| 643 |
+
switch (entry.type) {
|
| 644 |
+
case 'message': {
|
| 645 |
+
const msg = entry.message;
|
| 646 |
+
if (msg.role === 'user') {
|
| 647 |
+
const rawContent = extractContent(msg.content);
|
| 648 |
+
const skillBlock = parseSkillBlock(rawContent);
|
| 649 |
+
if (skillBlock) {
|
| 650 |
+
let treeHtml = labelHtml + `<span class="tree-role-skill">skill:</span> ${escapeHtml(skillBlock.name)}`;
|
| 651 |
+
if (skillBlock.userMessage) {
|
| 652 |
+
treeHtml += ` · <span class="tree-role-user">user:</span> ${escapeHtml(truncate(normalize(skillBlock.userMessage)))}`;
|
| 653 |
+
}
|
| 654 |
+
return treeHtml;
|
| 655 |
+
}
|
| 656 |
+
const content = truncate(normalize(rawContent));
|
| 657 |
+
return labelHtml + `<span class="tree-role-user">user:</span> ${escapeHtml(content)}`;
|
| 658 |
+
}
|
| 659 |
+
if (msg.role === 'assistant') {
|
| 660 |
+
const textContent = truncate(normalize(extractContent(msg.content)));
|
| 661 |
+
if (textContent) {
|
| 662 |
+
return labelHtml + `<span class="tree-role-assistant">assistant:</span> ${escapeHtml(textContent)}`;
|
| 663 |
+
}
|
| 664 |
+
if (msg.stopReason === 'aborted') {
|
| 665 |
+
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-muted">(aborted)</span>`;
|
| 666 |
+
}
|
| 667 |
+
if (msg.errorMessage) {
|
| 668 |
+
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-error">${escapeHtml(truncate(msg.errorMessage))}</span>`;
|
| 669 |
+
}
|
| 670 |
+
return labelHtml + `<span class="tree-role-assistant">assistant:</span> <span class="tree-muted">(no text)</span>`;
|
| 671 |
+
}
|
| 672 |
+
if (msg.role === 'toolResult') {
|
| 673 |
+
const toolCall = msg.toolCallId ? toolCallMap.get(msg.toolCallId) : null;
|
| 674 |
+
if (toolCall) {
|
| 675 |
+
return labelHtml + `<span class="tree-role-tool">${escapeHtml(formatToolCall(toolCall.name, toolCall.arguments))}</span>`;
|
| 676 |
+
}
|
| 677 |
+
return labelHtml + `<span class="tree-role-tool">[${escapeHtml(msg.toolName || 'tool')}]</span>`;
|
| 678 |
+
}
|
| 679 |
+
if (msg.role === 'bashExecution') {
|
| 680 |
+
const cmd = truncate(normalize(msg.command || ''));
|
| 681 |
+
return labelHtml + `<span class="tree-role-tool">[bash]:</span> ${escapeHtml(cmd)}`;
|
| 682 |
+
}
|
| 683 |
+
return labelHtml + `<span class="tree-muted">[${escapeHtml(msg.role)}]</span>`;
|
| 684 |
+
}
|
| 685 |
+
case 'compaction':
|
| 686 |
+
return labelHtml + `<span class="tree-compaction">[compaction: ${Math.round(entry.tokensBefore/1000)}k tokens]</span>`;
|
| 687 |
+
case 'branch_summary': {
|
| 688 |
+
const summary = truncate(normalize(entry.summary || ''));
|
| 689 |
+
return labelHtml + `<span class="tree-branch-summary">[branch summary]:</span> ${escapeHtml(summary)}`;
|
| 690 |
+
}
|
| 691 |
+
case 'custom_message': {
|
| 692 |
+
const content = typeof entry.content === 'string' ? entry.content : extractContent(entry.content);
|
| 693 |
+
return labelHtml + `<span class="tree-custom">[${escapeHtml(entry.customType)}]:</span> ${escapeHtml(truncate(normalize(content)))}`;
|
| 694 |
+
}
|
| 695 |
+
case 'model_change':
|
| 696 |
+
return labelHtml + `<span class="tree-muted">[model: ${escapeHtml(entry.modelId)}]</span>`;
|
| 697 |
+
case 'thinking_level_change':
|
| 698 |
+
return labelHtml + `<span class="tree-muted">[thinking: ${escapeHtml(entry.thinkingLevel)}]</span>`;
|
| 699 |
+
default:
|
| 700 |
+
return labelHtml + `<span class="tree-muted">[${escapeHtml(entry.type)}]</span>`;
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
+
// ============================================================
|
| 705 |
+
// TREE RENDERING (DOM manipulation)
|
| 706 |
+
// ============================================================
|
| 707 |
+
|
| 708 |
+
let currentLeafId = leafId;
|
| 709 |
+
let currentTargetId = urlTargetId || leafId;
|
| 710 |
+
let treeRendered = false;
|
| 711 |
+
|
| 712 |
+
function renderTree() {
|
| 713 |
+
const tree = buildTree();
|
| 714 |
+
const activePathIds = buildActivePathIds(currentLeafId);
|
| 715 |
+
const flatNodes = flattenTree(tree, activePathIds);
|
| 716 |
+
const filtered = filterNodes(flatNodes, currentLeafId);
|
| 717 |
+
const container = document.getElementById('tree-container');
|
| 718 |
+
|
| 719 |
+
// Full render only on first call or when filter/search changes
|
| 720 |
+
if (!treeRendered) {
|
| 721 |
+
container.innerHTML = '';
|
| 722 |
+
|
| 723 |
+
for (const flatNode of filtered) {
|
| 724 |
+
const entry = flatNode.node.entry;
|
| 725 |
+
const isOnPath = activePathIds.has(entry.id);
|
| 726 |
+
const isTarget = entry.id === currentTargetId;
|
| 727 |
+
|
| 728 |
+
const div = document.createElement('div');
|
| 729 |
+
div.className = 'tree-node';
|
| 730 |
+
if (isOnPath) div.classList.add('in-path');
|
| 731 |
+
if (isTarget) div.classList.add('active');
|
| 732 |
+
div.dataset.id = entry.id;
|
| 733 |
+
|
| 734 |
+
const prefix = buildTreePrefix(flatNode);
|
| 735 |
+
const prefixSpan = document.createElement('span');
|
| 736 |
+
prefixSpan.className = 'tree-prefix';
|
| 737 |
+
prefixSpan.textContent = prefix;
|
| 738 |
+
|
| 739 |
+
const marker = document.createElement('span');
|
| 740 |
+
marker.className = 'tree-marker';
|
| 741 |
+
marker.textContent = isOnPath ? '•' : ' ';
|
| 742 |
+
|
| 743 |
+
const content = document.createElement('span');
|
| 744 |
+
content.className = 'tree-content';
|
| 745 |
+
content.innerHTML = getTreeNodeDisplayHtml(entry, flatNode.node.label);
|
| 746 |
+
|
| 747 |
+
div.appendChild(prefixSpan);
|
| 748 |
+
div.appendChild(marker);
|
| 749 |
+
div.appendChild(content);
|
| 750 |
+
// Navigate to the newest leaf through this node, but scroll to the clicked node
|
| 751 |
+
div.addEventListener('click', () => {
|
| 752 |
+
if (window.getSelection().toString()) return;
|
| 753 |
+
const leafId = findNewestLeaf(entry.id);
|
| 754 |
+
navigateTo(leafId, 'target', entry.id);
|
| 755 |
+
});
|
| 756 |
+
|
| 757 |
+
container.appendChild(div);
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
treeRendered = true;
|
| 761 |
+
} else {
|
| 762 |
+
// Just update markers and classes
|
| 763 |
+
const nodes = container.querySelectorAll('.tree-node');
|
| 764 |
+
for (const node of nodes) {
|
| 765 |
+
const id = node.dataset.id;
|
| 766 |
+
const isOnPath = activePathIds.has(id);
|
| 767 |
+
const isTarget = id === currentTargetId;
|
| 768 |
+
|
| 769 |
+
node.classList.toggle('in-path', isOnPath);
|
| 770 |
+
node.classList.toggle('active', isTarget);
|
| 771 |
+
|
| 772 |
+
const marker = node.querySelector('.tree-marker');
|
| 773 |
+
if (marker) {
|
| 774 |
+
marker.textContent = isOnPath ? '•' : ' ';
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
document.getElementById('tree-status').textContent = `${filtered.length} / ${flatNodes.length} entries`;
|
| 780 |
+
|
| 781 |
+
// Scroll active node into view after layout
|
| 782 |
+
setTimeout(() => {
|
| 783 |
+
const activeNode = container.querySelector('.tree-node.active');
|
| 784 |
+
if (activeNode) {
|
| 785 |
+
activeNode.scrollIntoView({ block: 'nearest' });
|
| 786 |
+
}
|
| 787 |
+
}, 0);
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
function forceTreeRerender() {
|
| 791 |
+
treeRendered = false;
|
| 792 |
+
renderTree();
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
// ============================================================
|
| 796 |
+
// MESSAGE RENDERING
|
| 797 |
+
// ============================================================
|
| 798 |
+
|
| 799 |
+
function formatTokens(count) {
|
| 800 |
+
if (count < 1000) return count.toString();
|
| 801 |
+
if (count < 10000) return (count / 1000).toFixed(1) + 'k';
|
| 802 |
+
if (count < 1000000) return Math.round(count / 1000) + 'k';
|
| 803 |
+
return (count / 1000000).toFixed(1) + 'M';
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
function formatTimestamp(ts) {
|
| 807 |
+
if (!ts) return '';
|
| 808 |
+
const date = new Date(ts);
|
| 809 |
+
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
+
function replaceTabs(text) {
|
| 813 |
+
return text.replace(/\t/g, ' ');
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
/** Safely coerce value to string for display. Returns null if invalid type. */
|
| 817 |
+
function str(value) {
|
| 818 |
+
if (typeof value === 'string') return value;
|
| 819 |
+
if (value == null) return '';
|
| 820 |
+
return null;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
function getLanguageFromPath(filePath) {
|
| 824 |
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
| 825 |
+
const extToLang = {
|
| 826 |
+
ts: 'typescript', tsx: 'typescript', js: 'javascript', jsx: 'javascript',
|
| 827 |
+
py: 'python', rb: 'ruby', rs: 'rust', go: 'go', java: 'java',
|
| 828 |
+
c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp',
|
| 829 |
+
php: 'php', sh: 'bash', bash: 'bash', zsh: 'bash',
|
| 830 |
+
sql: 'sql', html: 'html', css: 'css', scss: 'scss',
|
| 831 |
+
json: 'json', yaml: 'yaml', yml: 'yaml', xml: 'xml',
|
| 832 |
+
md: 'markdown', dockerfile: 'dockerfile'
|
| 833 |
+
};
|
| 834 |
+
return extToLang[ext];
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
function findToolResult(toolCallId) {
|
| 838 |
+
for (const entry of entries) {
|
| 839 |
+
if (entry.type === 'message' && entry.message.role === 'toolResult') {
|
| 840 |
+
if (entry.message.toolCallId === toolCallId) {
|
| 841 |
+
return entry.message;
|
| 842 |
+
}
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
return null;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
function formatExpandableOutput(text, maxLines, lang) {
|
| 849 |
+
text = replaceTabs(text);
|
| 850 |
+
const lines = text.split('\n');
|
| 851 |
+
const displayLines = lines.slice(0, maxLines);
|
| 852 |
+
const remaining = lines.length - maxLines;
|
| 853 |
+
|
| 854 |
+
if (lang) {
|
| 855 |
+
let highlighted;
|
| 856 |
+
try {
|
| 857 |
+
highlighted = hljs.highlight(text, { language: lang }).value;
|
| 858 |
+
} catch {
|
| 859 |
+
highlighted = escapeHtml(text);
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
if (remaining > 0) {
|
| 863 |
+
const previewCode = displayLines.join('\n');
|
| 864 |
+
let previewHighlighted;
|
| 865 |
+
try {
|
| 866 |
+
previewHighlighted = hljs.highlight(previewCode, { language: lang }).value;
|
| 867 |
+
} catch {
|
| 868 |
+
previewHighlighted = escapeHtml(previewCode);
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
return `<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
| 872 |
+
<div class="output-preview"><pre><code class="hljs">${previewHighlighted}</code></pre>
|
| 873 |
+
<div class="expand-hint">... (${remaining} more lines)</div></div>
|
| 874 |
+
<div class="output-full"><pre><code class="hljs">${highlighted}</code></pre></div></div>`;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
return `<div class="tool-output"><pre><code class="hljs">${highlighted}</code></pre></div>`;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
// Plain text output
|
| 881 |
+
if (remaining > 0) {
|
| 882 |
+
let out = '<div class="tool-output expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle(\'expanded\')">';
|
| 883 |
+
out += '<div class="output-preview">';
|
| 884 |
+
for (const line of displayLines) {
|
| 885 |
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
| 886 |
+
}
|
| 887 |
+
out += `<div class="expand-hint">... (${remaining} more lines)</div></div>`;
|
| 888 |
+
out += '<div class="output-full">';
|
| 889 |
+
for (const line of lines) {
|
| 890 |
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
| 891 |
+
}
|
| 892 |
+
out += '</div></div>';
|
| 893 |
+
return out;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
let out = '<div class="tool-output">';
|
| 897 |
+
for (const line of displayLines) {
|
| 898 |
+
out += `<div>${escapeHtml(replaceTabs(line))}</div>`;
|
| 899 |
+
}
|
| 900 |
+
out += '</div>';
|
| 901 |
+
return out;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
function renderToolCall(call) {
|
| 905 |
+
const result = findToolResult(call.id);
|
| 906 |
+
const isError = result?.isError || false;
|
| 907 |
+
const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
|
| 908 |
+
|
| 909 |
+
const getResultText = () => {
|
| 910 |
+
if (!result) return '';
|
| 911 |
+
const textBlocks = result.content.filter(c => c.type === 'text');
|
| 912 |
+
return textBlocks.map(c => c.text).join('\n');
|
| 913 |
+
};
|
| 914 |
+
|
| 915 |
+
const getResultImages = () => {
|
| 916 |
+
if (!result) return [];
|
| 917 |
+
return result.content.filter(c => c.type === 'image');
|
| 918 |
+
};
|
| 919 |
+
|
| 920 |
+
const renderResultImages = () => {
|
| 921 |
+
const images = getResultImages();
|
| 922 |
+
if (images.length === 0) return '';
|
| 923 |
+
return '<div class="tool-images">' +
|
| 924 |
+
images.map(img => `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="tool-image" />`).join('') +
|
| 925 |
+
'</div>';
|
| 926 |
+
};
|
| 927 |
+
|
| 928 |
+
const toolDomId = `tool-call-${escapeHtml(call.id)}`;
|
| 929 |
+
let html = `<div class="tool-execution ${statusClass}" id="${toolDomId}">`;
|
| 930 |
+
const args = call.arguments || {};
|
| 931 |
+
const name = call.name;
|
| 932 |
+
|
| 933 |
+
const invalidArg = '<span class="tool-error">[invalid arg]</span>';
|
| 934 |
+
|
| 935 |
+
switch (name) {
|
| 936 |
+
case 'bash': {
|
| 937 |
+
const command = str(args.command);
|
| 938 |
+
const cmdDisplay = command === null ? invalidArg : escapeHtml(command || '...');
|
| 939 |
+
html += `<div class="tool-command">$ ${cmdDisplay}</div>`;
|
| 940 |
+
if (result) {
|
| 941 |
+
const output = getResultText().trim();
|
| 942 |
+
if (output) html += formatExpandableOutput(output, 5);
|
| 943 |
+
}
|
| 944 |
+
break;
|
| 945 |
+
}
|
| 946 |
+
case 'read': {
|
| 947 |
+
const filePath = str(args.file_path ?? args.path);
|
| 948 |
+
const offset = args.offset;
|
| 949 |
+
const limit = args.limit;
|
| 950 |
+
|
| 951 |
+
let pathHtml = filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''));
|
| 952 |
+
if (filePath !== null && (offset !== undefined || limit !== undefined)) {
|
| 953 |
+
const startLine = offset ?? 1;
|
| 954 |
+
const endLine = limit !== undefined ? startLine + limit - 1 : '';
|
| 955 |
+
pathHtml += `<span class="line-numbers">:${startLine}${endLine ? '-' + endLine : ''}</span>`;
|
| 956 |
+
}
|
| 957 |
+
|
| 958 |
+
html += `<div class="tool-header"><span class="tool-name">read</span> <span class="tool-path">${pathHtml}</span></div>`;
|
| 959 |
+
if (result) {
|
| 960 |
+
html += renderResultImages();
|
| 961 |
+
const output = getResultText();
|
| 962 |
+
const lang = filePath ? getLanguageFromPath(filePath) : null;
|
| 963 |
+
if (output) html += formatExpandableOutput(output, 10, lang);
|
| 964 |
+
}
|
| 965 |
+
break;
|
| 966 |
+
}
|
| 967 |
+
case 'write': {
|
| 968 |
+
const filePath = str(args.file_path ?? args.path);
|
| 969 |
+
const content = str(args.content);
|
| 970 |
+
|
| 971 |
+
html += `<div class="tool-header"><span class="tool-name">write</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span>`;
|
| 972 |
+
if (content !== null && content) {
|
| 973 |
+
const lines = content.split('\n');
|
| 974 |
+
if (lines.length > 10) html += ` <span class="line-count">(${lines.length} lines)</span>`;
|
| 975 |
+
}
|
| 976 |
+
html += '</div>';
|
| 977 |
+
|
| 978 |
+
if (content === null) {
|
| 979 |
+
html += `<div class="tool-error">[invalid content arg - expected string]</div>`;
|
| 980 |
+
} else if (content) {
|
| 981 |
+
const lang = filePath ? getLanguageFromPath(filePath) : null;
|
| 982 |
+
html += formatExpandableOutput(content, 10, lang);
|
| 983 |
+
}
|
| 984 |
+
if (result) {
|
| 985 |
+
const output = getResultText().trim();
|
| 986 |
+
if (output) html += `<div class="tool-output"><div>${escapeHtml(output)}</div></div>`;
|
| 987 |
+
}
|
| 988 |
+
break;
|
| 989 |
+
}
|
| 990 |
+
case 'edit': {
|
| 991 |
+
const filePath = str(args.file_path ?? args.path);
|
| 992 |
+
html += `<div class="tool-header"><span class="tool-name">edit</span> <span class="tool-path">${filePath === null ? invalidArg : escapeHtml(shortenPath(filePath || ''))}</span></div>`;
|
| 993 |
+
|
| 994 |
+
if (result?.details?.diff) {
|
| 995 |
+
const diffLines = result.details.diff.split('\n');
|
| 996 |
+
html += '<div class="tool-diff">';
|
| 997 |
+
for (const line of diffLines) {
|
| 998 |
+
const cls = line.match(/^\+/) ? 'diff-added' : line.match(/^-/) ? 'diff-removed' : 'diff-context';
|
| 999 |
+
html += `<div class="${cls}">${escapeHtml(replaceTabs(line))}</div>`;
|
| 1000 |
+
}
|
| 1001 |
+
html += '</div>';
|
| 1002 |
+
} else if (result) {
|
| 1003 |
+
const output = getResultText().trim();
|
| 1004 |
+
if (output) html += `<div class="tool-output"><pre>${escapeHtml(output)}</pre></div>`;
|
| 1005 |
+
}
|
| 1006 |
+
break;
|
| 1007 |
+
}
|
| 1008 |
+
case 'ls': {
|
| 1009 |
+
const dirPath = str(args.path);
|
| 1010 |
+
const limit = args.limit;
|
| 1011 |
+
|
| 1012 |
+
let pathHtml = dirPath === null ? invalidArg : escapeHtml(shortenPath(dirPath || '.'));
|
| 1013 |
+
if (limit !== undefined) {
|
| 1014 |
+
pathHtml += ` <span class="line-count">(limit ${escapeHtml(String(limit))})</span>`;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
html += `<div class="tool-header"><span class="tool-name">ls</span> <span class="tool-path">${pathHtml}</span></div>`;
|
| 1018 |
+
if (result) {
|
| 1019 |
+
const output = getResultText().trim();
|
| 1020 |
+
if (output) html += formatExpandableOutput(output, 20);
|
| 1021 |
+
}
|
| 1022 |
+
break;
|
| 1023 |
+
}
|
| 1024 |
+
default: {
|
| 1025 |
+
// Check for pre-rendered custom tool HTML
|
| 1026 |
+
const rendered = renderedTools?.[call.id];
|
| 1027 |
+
if (rendered?.callHtml || rendered?.resultHtmlCollapsed || rendered?.resultHtmlExpanded) {
|
| 1028 |
+
// Custom tool with pre-rendered HTML from TUI renderer
|
| 1029 |
+
if (rendered.callHtml) {
|
| 1030 |
+
html += `<div class="tool-header ansi-rendered">${rendered.callHtml}</div>`;
|
| 1031 |
+
} else {
|
| 1032 |
+
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
if (rendered.resultHtmlCollapsed && rendered.resultHtmlExpanded && rendered.resultHtmlCollapsed !== rendered.resultHtmlExpanded) {
|
| 1036 |
+
// Both collapsed and expanded differ - render expandable section
|
| 1037 |
+
html += `<div class="tool-output expandable ansi-rendered" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
| 1038 |
+
<div class="output-preview">${rendered.resultHtmlCollapsed}</div>
|
| 1039 |
+
<div class="output-full">${rendered.resultHtmlExpanded}</div>
|
| 1040 |
+
</div>`;
|
| 1041 |
+
} else if (rendered.resultHtmlExpanded) {
|
| 1042 |
+
// Only expanded exists (or collapsed is identical) - show directly
|
| 1043 |
+
html += `<div class="tool-output ansi-rendered">${rendered.resultHtmlExpanded}</div>`;
|
| 1044 |
+
} else if (result) {
|
| 1045 |
+
// No pre-rendered result HTML - fallback to JSON
|
| 1046 |
+
const output = getResultText();
|
| 1047 |
+
if (output) html += formatExpandableOutput(output, 10);
|
| 1048 |
+
}
|
| 1049 |
+
} else {
|
| 1050 |
+
// Fallback to JSON display (existing behavior)
|
| 1051 |
+
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
|
| 1052 |
+
html += `<div class="tool-output"><pre>${escapeHtml(JSON.stringify(args, null, 2))}</pre></div>`;
|
| 1053 |
+
if (result) {
|
| 1054 |
+
const output = getResultText();
|
| 1055 |
+
if (output) html += formatExpandableOutput(output, 10);
|
| 1056 |
+
}
|
| 1057 |
+
}
|
| 1058 |
+
}
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
html += '</div>';
|
| 1062 |
+
return html;
|
| 1063 |
+
}
|
| 1064 |
+
|
| 1065 |
+
/**
|
| 1066 |
+
* Download the session data as a JSONL file.
|
| 1067 |
+
* Reconstructs the original format: header line + entry lines.
|
| 1068 |
+
*/
|
| 1069 |
+
window.downloadSessionJson = function() {
|
| 1070 |
+
// Build JSONL content: header first, then all entries
|
| 1071 |
+
const lines = [];
|
| 1072 |
+
if (header) {
|
| 1073 |
+
lines.push(JSON.stringify({ type: 'header', ...header }));
|
| 1074 |
+
}
|
| 1075 |
+
for (const entry of entries) {
|
| 1076 |
+
lines.push(JSON.stringify(entry));
|
| 1077 |
+
}
|
| 1078 |
+
const jsonlContent = lines.join('\n');
|
| 1079 |
+
|
| 1080 |
+
// Create download
|
| 1081 |
+
const blob = new Blob([jsonlContent], { type: 'application/x-ndjson' });
|
| 1082 |
+
const url = URL.createObjectURL(blob);
|
| 1083 |
+
const a = document.createElement('a');
|
| 1084 |
+
a.href = url;
|
| 1085 |
+
a.download = `${header?.id || 'session'}.jsonl`;
|
| 1086 |
+
document.body.appendChild(a);
|
| 1087 |
+
a.click();
|
| 1088 |
+
document.body.removeChild(a);
|
| 1089 |
+
URL.revokeObjectURL(url);
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
/**
|
| 1093 |
+
* Build a shareable URL for a specific message.
|
| 1094 |
+
* URL format: base?gistId&leafId=<leafId>&targetId=<entryId>
|
| 1095 |
+
*/
|
| 1096 |
+
function buildShareUrl(entryId) {
|
| 1097 |
+
// Check for injected base URL (used when loaded in iframe via srcdoc)
|
| 1098 |
+
const baseUrlMeta = document.querySelector('meta[name="pi-share-base-url"]');
|
| 1099 |
+
const baseUrl = baseUrlMeta ? baseUrlMeta.content : window.location.href.split('?')[0];
|
| 1100 |
+
|
| 1101 |
+
const url = new URL(window.location.href);
|
| 1102 |
+
// Find the gist ID (first query param without value, e.g., ?abc123)
|
| 1103 |
+
const gistId = Array.from(url.searchParams.keys()).find(k => !url.searchParams.get(k));
|
| 1104 |
+
|
| 1105 |
+
// Build the share URL
|
| 1106 |
+
const params = new URLSearchParams();
|
| 1107 |
+
params.set('leafId', currentLeafId);
|
| 1108 |
+
params.set('targetId', entryId);
|
| 1109 |
+
|
| 1110 |
+
// If we have an injected base URL (iframe context), use it directly
|
| 1111 |
+
if (baseUrlMeta) {
|
| 1112 |
+
return `${baseUrl}&${params.toString()}`;
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
// Otherwise build from current location (direct file access)
|
| 1116 |
+
url.search = gistId ? `?${gistId}&${params.toString()}` : `?${params.toString()}`;
|
| 1117 |
+
return url.toString();
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
+
/**
|
| 1121 |
+
* Copy text to clipboard with visual feedback.
|
| 1122 |
+
* Uses navigator.clipboard with fallback to execCommand for HTTP contexts.
|
| 1123 |
+
*/
|
| 1124 |
+
async function copyToClipboard(text, button) {
|
| 1125 |
+
let success = false;
|
| 1126 |
+
try {
|
| 1127 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 1128 |
+
await navigator.clipboard.writeText(text);
|
| 1129 |
+
success = true;
|
| 1130 |
+
}
|
| 1131 |
+
} catch (err) {
|
| 1132 |
+
// Clipboard API failed, try fallback
|
| 1133 |
+
}
|
| 1134 |
+
|
| 1135 |
+
// Fallback for HTTP or when Clipboard API is unavailable
|
| 1136 |
+
if (!success) {
|
| 1137 |
+
try {
|
| 1138 |
+
const textarea = document.createElement('textarea');
|
| 1139 |
+
textarea.value = text;
|
| 1140 |
+
textarea.style.position = 'fixed';
|
| 1141 |
+
textarea.style.opacity = '0';
|
| 1142 |
+
document.body.appendChild(textarea);
|
| 1143 |
+
textarea.select();
|
| 1144 |
+
success = document.execCommand('copy');
|
| 1145 |
+
document.body.removeChild(textarea);
|
| 1146 |
+
} catch (err) {
|
| 1147 |
+
console.error('Failed to copy:', err);
|
| 1148 |
+
}
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
if (success && button) {
|
| 1152 |
+
const originalHtml = button.innerHTML;
|
| 1153 |
+
button.innerHTML = '✓';
|
| 1154 |
+
button.classList.add('copied');
|
| 1155 |
+
setTimeout(() => {
|
| 1156 |
+
button.innerHTML = originalHtml;
|
| 1157 |
+
button.classList.remove('copied');
|
| 1158 |
+
}, 1500);
|
| 1159 |
+
}
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
/**
|
| 1163 |
+
* Render the copy-link button HTML for a message.
|
| 1164 |
+
*/
|
| 1165 |
+
function renderCopyLinkButton(entryId) {
|
| 1166 |
+
return `<button class="copy-link-btn" data-entry-id="${escapeHtml(entryId)}" title="Copy link to this message">
|
| 1167 |
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 1168 |
+
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
| 1169 |
+
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
| 1170 |
+
</svg>
|
| 1171 |
+
</button>`;
|
| 1172 |
+
}
|
| 1173 |
+
|
| 1174 |
+
function renderEntry(entry) {
|
| 1175 |
+
const ts = formatTimestamp(entry.timestamp);
|
| 1176 |
+
const tsHtml = ts ? `<div class="message-timestamp">${ts}</div>` : '';
|
| 1177 |
+
const entryDomId = `entry-${escapeHtml(entry.id)}`;
|
| 1178 |
+
const copyBtnHtml = renderCopyLinkButton(entry.id);
|
| 1179 |
+
|
| 1180 |
+
if (entry.type === 'message') {
|
| 1181 |
+
const msg = entry.message;
|
| 1182 |
+
|
| 1183 |
+
if (msg.role === 'user') {
|
| 1184 |
+
const content = msg.content;
|
| 1185 |
+
const text = typeof content === 'string' ? content :
|
| 1186 |
+
content.filter(c => c.type === 'text').map(c => c.text).join('\n');
|
| 1187 |
+
const skillBlock = parseSkillBlock(text);
|
| 1188 |
+
|
| 1189 |
+
if (skillBlock) {
|
| 1190 |
+
// Collect images from content array
|
| 1191 |
+
const images = Array.isArray(content) ? content.filter(c => c.type === 'image') : [];
|
| 1192 |
+
const hasUserContent = skillBlock.userMessage || images.length > 0;
|
| 1193 |
+
let html = `<div class="skill-user-entry" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
| 1194 |
+
|
| 1195 |
+
// Skill invocation (collapsed by default, click to expand)
|
| 1196 |
+
html += `<div class="skill-invocation" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
| 1197 |
+
<div class="skill-invocation-label">[skill] ${escapeHtml(skillBlock.name)}</div>
|
| 1198 |
+
<div class="skill-invocation-collapsed">${escapeHtml(skillBlock.name)} (click to expand)</div>
|
| 1199 |
+
<div class="skill-invocation-content markdown-content">${safeMarkedParse(skillBlock.content)}</div>
|
| 1200 |
+
</div>`;
|
| 1201 |
+
|
| 1202 |
+
// User message (separate block if present)
|
| 1203 |
+
if (hasUserContent) {
|
| 1204 |
+
html += '<div class="user-message">';
|
| 1205 |
+
if (images.length > 0) {
|
| 1206 |
+
html += '<div class="message-images">';
|
| 1207 |
+
for (const img of images) {
|
| 1208 |
+
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
|
| 1209 |
+
}
|
| 1210 |
+
html += '</div>';
|
| 1211 |
+
}
|
| 1212 |
+
if (skillBlock.userMessage) {
|
| 1213 |
+
html += `<div class="markdown-content">${safeMarkedParse(skillBlock.userMessage)}</div>`;
|
| 1214 |
+
}
|
| 1215 |
+
html += '</div>';
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
html += '</div>';
|
| 1219 |
+
return html;
|
| 1220 |
+
}
|
| 1221 |
+
|
| 1222 |
+
// No skill block - normal user message
|
| 1223 |
+
let html = `<div class="user-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
| 1224 |
+
|
| 1225 |
+
if (Array.isArray(content)) {
|
| 1226 |
+
const images = content.filter(c => c.type === 'image');
|
| 1227 |
+
if (images.length > 0) {
|
| 1228 |
+
html += '<div class="message-images">';
|
| 1229 |
+
for (const img of images) {
|
| 1230 |
+
html += `<img src="data:${escapeHtml(img.mimeType || 'image/png')};base64,${escapeHtml(img.data || '')}" class="message-image" />`;
|
| 1231 |
+
}
|
| 1232 |
+
html += '</div>';
|
| 1233 |
+
}
|
| 1234 |
+
}
|
| 1235 |
+
|
| 1236 |
+
if (text.trim()) {
|
| 1237 |
+
html += `<div class="markdown-content">${safeMarkedParse(text)}</div>`;
|
| 1238 |
+
}
|
| 1239 |
+
html += '</div>';
|
| 1240 |
+
return html;
|
| 1241 |
+
}
|
| 1242 |
+
|
| 1243 |
+
if (msg.role === 'assistant') {
|
| 1244 |
+
let html = `<div class="assistant-message" id="${entryDomId}">${copyBtnHtml}${tsHtml}`;
|
| 1245 |
+
|
| 1246 |
+
for (const block of msg.content) {
|
| 1247 |
+
if (block.type === 'text' && block.text.trim()) {
|
| 1248 |
+
html += `<div class="assistant-text markdown-content">${safeMarkedParse(block.text)}</div>`;
|
| 1249 |
+
} else if (block.type === 'thinking' && block.thinking.trim()) {
|
| 1250 |
+
html += `<div class="thinking-block">
|
| 1251 |
+
<div class="thinking-text">${escapeHtml(block.thinking)}</div>
|
| 1252 |
+
<div class="thinking-collapsed">Thinking ...</div>
|
| 1253 |
+
</div>`;
|
| 1254 |
+
}
|
| 1255 |
+
}
|
| 1256 |
+
|
| 1257 |
+
for (const block of msg.content) {
|
| 1258 |
+
if (block.type === 'toolCall') {
|
| 1259 |
+
html += renderToolCall(block);
|
| 1260 |
+
}
|
| 1261 |
+
}
|
| 1262 |
+
|
| 1263 |
+
if (msg.stopReason === 'aborted') {
|
| 1264 |
+
html += '<div class="error-text">Aborted</div>';
|
| 1265 |
+
} else if (msg.stopReason === 'error') {
|
| 1266 |
+
html += `<div class="error-text">Error: ${escapeHtml(msg.errorMessage || 'Unknown error')}</div>`;
|
| 1267 |
+
}
|
| 1268 |
+
|
| 1269 |
+
html += '</div>';
|
| 1270 |
+
return html;
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
if (msg.role === 'bashExecution') {
|
| 1274 |
+
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
|
| 1275 |
+
let html = `<div class="tool-execution ${isError ? 'error' : 'success'}" id="${entryDomId}">${tsHtml}`;
|
| 1276 |
+
html += `<div class="tool-command">$ ${escapeHtml(msg.command)}</div>`;
|
| 1277 |
+
if (msg.output) html += formatExpandableOutput(msg.output, 10);
|
| 1278 |
+
if (msg.cancelled) {
|
| 1279 |
+
html += '<div style="color: var(--warning)">(cancelled)</div>';
|
| 1280 |
+
} else if (msg.exitCode !== 0 && msg.exitCode !== null) {
|
| 1281 |
+
html += `<div style="color: var(--error)">(exit ${msg.exitCode})</div>`;
|
| 1282 |
+
}
|
| 1283 |
+
html += '</div>';
|
| 1284 |
+
return html;
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
if (msg.role === 'toolResult') return '';
|
| 1288 |
+
}
|
| 1289 |
+
|
| 1290 |
+
if (entry.type === 'model_change') {
|
| 1291 |
+
return `<div class="model-change" id="${entryDomId}">${tsHtml}Switched to model: <span class="model-name">${escapeHtml(entry.provider)}/${escapeHtml(entry.modelId)}</span></div>`;
|
| 1292 |
+
}
|
| 1293 |
+
|
| 1294 |
+
if (entry.type === 'compaction') {
|
| 1295 |
+
return `<div class="compaction" id="${entryDomId}" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
| 1296 |
+
<div class="compaction-label">[compaction]</div>
|
| 1297 |
+
<div class="compaction-collapsed">Compacted from ${entry.tokensBefore.toLocaleString()} tokens</div>
|
| 1298 |
+
<div class="compaction-content"><strong>Compacted from ${entry.tokensBefore.toLocaleString()} tokens</strong>\n\n${escapeHtml(entry.summary)}</div>
|
| 1299 |
+
</div>`;
|
| 1300 |
+
}
|
| 1301 |
+
|
| 1302 |
+
if (entry.type === 'branch_summary') {
|
| 1303 |
+
return `<div class="branch-summary" id="${entryDomId}">${tsHtml}
|
| 1304 |
+
<div class="branch-summary-header">Branch Summary</div>
|
| 1305 |
+
<div class="markdown-content">${safeMarkedParse(entry.summary)}</div>
|
| 1306 |
+
</div>`;
|
| 1307 |
+
}
|
| 1308 |
+
|
| 1309 |
+
if (entry.type === 'custom_message' && entry.display) {
|
| 1310 |
+
return `<div class="hook-message" id="${entryDomId}">${tsHtml}
|
| 1311 |
+
<div class="hook-type">[${escapeHtml(entry.customType)}]</div>
|
| 1312 |
+
<div class="markdown-content">${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}</div>
|
| 1313 |
+
</div>`;
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
return '';
|
| 1317 |
+
}
|
| 1318 |
+
|
| 1319 |
+
// ============================================================
|
| 1320 |
+
// HEADER / STATS
|
| 1321 |
+
// ============================================================
|
| 1322 |
+
|
| 1323 |
+
function computeStats(entryList) {
|
| 1324 |
+
let userMessages = 0, assistantMessages = 0, toolResults = 0;
|
| 1325 |
+
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
|
| 1326 |
+
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
| 1327 |
+
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
| 1328 |
+
const models = new Set();
|
| 1329 |
+
|
| 1330 |
+
for (const entry of entryList) {
|
| 1331 |
+
if (entry.type === 'message') {
|
| 1332 |
+
const msg = entry.message;
|
| 1333 |
+
if (msg.role === 'user') userMessages++;
|
| 1334 |
+
if (msg.role === 'assistant') {
|
| 1335 |
+
assistantMessages++;
|
| 1336 |
+
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
|
| 1337 |
+
if (msg.usage) {
|
| 1338 |
+
tokens.input += msg.usage.input || 0;
|
| 1339 |
+
tokens.output += msg.usage.output || 0;
|
| 1340 |
+
tokens.cacheRead += msg.usage.cacheRead || 0;
|
| 1341 |
+
tokens.cacheWrite += msg.usage.cacheWrite || 0;
|
| 1342 |
+
if (msg.usage.cost) {
|
| 1343 |
+
cost.input += msg.usage.cost.input || 0;
|
| 1344 |
+
cost.output += msg.usage.cost.output || 0;
|
| 1345 |
+
cost.cacheRead += msg.usage.cost.cacheRead || 0;
|
| 1346 |
+
cost.cacheWrite += msg.usage.cost.cacheWrite || 0;
|
| 1347 |
+
}
|
| 1348 |
+
}
|
| 1349 |
+
toolCalls += msg.content.filter(c => c.type === 'toolCall').length;
|
| 1350 |
+
}
|
| 1351 |
+
if (msg.role === 'toolResult') toolResults++;
|
| 1352 |
+
} else if (entry.type === 'compaction') {
|
| 1353 |
+
compactions++;
|
| 1354 |
+
} else if (entry.type === 'branch_summary') {
|
| 1355 |
+
branchSummaries++;
|
| 1356 |
+
} else if (entry.type === 'custom_message') {
|
| 1357 |
+
customMessages++;
|
| 1358 |
+
}
|
| 1359 |
+
}
|
| 1360 |
+
|
| 1361 |
+
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
const globalStats = computeStats(entries);
|
| 1365 |
+
|
| 1366 |
+
function renderHeader() {
|
| 1367 |
+
const totalCost = globalStats.cost.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite;
|
| 1368 |
+
|
| 1369 |
+
const tokenParts = [];
|
| 1370 |
+
if (globalStats.tokens.input) tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`);
|
| 1371 |
+
if (globalStats.tokens.output) tokenParts.push(`↓${formatTokens(globalStats.tokens.output)}`);
|
| 1372 |
+
if (globalStats.tokens.cacheRead) tokenParts.push(`R${formatTokens(globalStats.tokens.cacheRead)}`);
|
| 1373 |
+
if (globalStats.tokens.cacheWrite) tokenParts.push(`W${formatTokens(globalStats.tokens.cacheWrite)}`);
|
| 1374 |
+
|
| 1375 |
+
const msgParts = [];
|
| 1376 |
+
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
|
| 1377 |
+
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
|
| 1378 |
+
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
|
| 1379 |
+
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
|
| 1380 |
+
if (globalStats.compactions) msgParts.push(`${globalStats.compactions} compactions`);
|
| 1381 |
+
if (globalStats.branchSummaries) msgParts.push(`${globalStats.branchSummaries} branch summaries`);
|
| 1382 |
+
|
| 1383 |
+
let html = `
|
| 1384 |
+
<div class="header">
|
| 1385 |
+
<h1>Session: ${escapeHtml(header?.id || 'unknown')}</h1>
|
| 1386 |
+
<div class="help-bar">
|
| 1387 |
+
<span class="help-hint">T toggle thinking · O toggle tools</span>
|
| 1388 |
+
<div class="help-actions">
|
| 1389 |
+
<button type="button" class="header-toggle-btn" data-action="toggle-thinking" title="Toggle thinking (T)">Toggle thinking</button>
|
| 1390 |
+
<button type="button" class="header-toggle-btn" data-action="toggle-tools" title="Toggle tools (O)">Toggle tools</button>
|
| 1391 |
+
<button type="button" class="download-json-btn" onclick="downloadSessionJson()" title="Download session as JSONL">↓ JSONL</button>
|
| 1392 |
+
</div>
|
| 1393 |
+
</div>
|
| 1394 |
+
<div class="header-info">
|
| 1395 |
+
<div class="info-item"><span class="info-label">Date:</span><span class="info-value">${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}</span></div>
|
| 1396 |
+
<div class="info-item"><span class="info-label">Models:</span><span class="info-value">${escapeHtml(globalStats.models.join(', ') || 'unknown')}</span></div>
|
| 1397 |
+
<div class="info-item"><span class="info-label">Messages:</span><span class="info-value">${msgParts.join(', ') || '0'}</span></div>
|
| 1398 |
+
<div class="info-item"><span class="info-label">Tool Calls:</span><span class="info-value">${globalStats.toolCalls}</span></div>
|
| 1399 |
+
<div class="info-item"><span class="info-label">Tokens:</span><span class="info-value">${tokenParts.join(' ') || '0'}</span></div>
|
| 1400 |
+
<div class="info-item"><span class="info-label">Cost:</span><span class="info-value">$${totalCost.toFixed(3)}</span></div>
|
| 1401 |
+
</div>
|
| 1402 |
+
</div>`;
|
| 1403 |
+
|
| 1404 |
+
// Render system prompt (user's base prompt, applies to all providers)
|
| 1405 |
+
if (systemPrompt) {
|
| 1406 |
+
const lines = systemPrompt.split('\n');
|
| 1407 |
+
const previewLines = 10;
|
| 1408 |
+
if (lines.length > previewLines) {
|
| 1409 |
+
const preview = lines.slice(0, previewLines).join('\n');
|
| 1410 |
+
const remaining = lines.length - previewLines;
|
| 1411 |
+
html += `<div class="system-prompt expandable" onclick="if(window.getSelection().toString())return;this.classList.toggle('expanded')">
|
| 1412 |
+
<div class="system-prompt-header">System Prompt</div>
|
| 1413 |
+
<div class="system-prompt-preview">${escapeHtml(preview)}</div>
|
| 1414 |
+
<div class="system-prompt-expand-hint">... (${remaining} more lines, click to expand)</div>
|
| 1415 |
+
<div class="system-prompt-full">${escapeHtml(systemPrompt)}</div>
|
| 1416 |
+
</div>`;
|
| 1417 |
+
} else {
|
| 1418 |
+
html += `<div class="system-prompt">
|
| 1419 |
+
<div class="system-prompt-header">System Prompt</div>
|
| 1420 |
+
<div class="system-prompt-full" style="display: block">${escapeHtml(systemPrompt)}</div>
|
| 1421 |
+
</div>`;
|
| 1422 |
+
}
|
| 1423 |
+
}
|
| 1424 |
+
|
| 1425 |
+
if (tools && tools.length > 0) {
|
| 1426 |
+
html += `<div class="tools-list">
|
| 1427 |
+
<div class="tools-header">Available Tools</div>
|
| 1428 |
+
<div class="tools-content">
|
| 1429 |
+
${tools.map(t => {
|
| 1430 |
+
const hasParams = t.parameters && typeof t.parameters === 'object' && t.parameters.properties && Object.keys(t.parameters.properties).length > 0;
|
| 1431 |
+
if (!hasParams) {
|
| 1432 |
+
return `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span></div>`;
|
| 1433 |
+
}
|
| 1434 |
+
const params = t.parameters;
|
| 1435 |
+
const properties = params.properties;
|
| 1436 |
+
const required = params.required || [];
|
| 1437 |
+
let paramsHtml = '';
|
| 1438 |
+
for (const [name, prop] of Object.entries(properties)) {
|
| 1439 |
+
const isRequired = required.includes(name);
|
| 1440 |
+
const typeStr = prop.type || 'any';
|
| 1441 |
+
const reqLabel = isRequired ? '<span class="tool-param-required">required</span>' : '<span class="tool-param-optional">optional</span>';
|
| 1442 |
+
paramsHtml += `<div class="tool-param"><span class="tool-param-name">${escapeHtml(name)}</span> <span class="tool-param-type">${escapeHtml(typeStr)}</span> ${reqLabel}`;
|
| 1443 |
+
if (prop.description) {
|
| 1444 |
+
paramsHtml += `<div class="tool-param-desc">${escapeHtml(prop.description)}</div>`;
|
| 1445 |
+
}
|
| 1446 |
+
paramsHtml += `</div>`;
|
| 1447 |
+
}
|
| 1448 |
+
return `<div class="tool-item" onclick="if(window.getSelection().toString())return;this.classList.toggle('params-expanded')"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span> <span class="tool-params-hint"></span><div class="tool-params-content">${paramsHtml}</div></div>`;
|
| 1449 |
+
}).join('')}
|
| 1450 |
+
</div>
|
| 1451 |
+
</div>`;
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
+
return html;
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
// ============================================================
|
| 1458 |
+
// NAVIGATION
|
| 1459 |
+
// ============================================================
|
| 1460 |
+
|
| 1461 |
+
// Cache for rendered entry DOM nodes
|
| 1462 |
+
const entryCache = new Map();
|
| 1463 |
+
|
| 1464 |
+
function getScrollTargetElementId(entryId) {
|
| 1465 |
+
const entry = byId.get(entryId);
|
| 1466 |
+
if (entry?.type === 'message' && entry.message.role === 'toolResult' && entry.message.toolCallId) {
|
| 1467 |
+
// getElementById() matches the parsed DOM id attribute, whose HTML entities
|
| 1468 |
+
// were already resolved from the escaped id rendered by renderToolCall().
|
| 1469 |
+
return `tool-call-${entry.message.toolCallId}`;
|
| 1470 |
+
}
|
| 1471 |
+
return `entry-${entryId}`;
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
function renderEntryToNode(entry) {
|
| 1475 |
+
// Check cache first
|
| 1476 |
+
if (entryCache.has(entry.id)) {
|
| 1477 |
+
return entryCache.get(entry.id).cloneNode(true);
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
// Render to HTML string, then parse to node
|
| 1481 |
+
const html = renderEntry(entry);
|
| 1482 |
+
if (!html) return null;
|
| 1483 |
+
|
| 1484 |
+
const template = document.createElement('template');
|
| 1485 |
+
template.innerHTML = html;
|
| 1486 |
+
const node = template.content.firstElementChild;
|
| 1487 |
+
|
| 1488 |
+
// Cache the node
|
| 1489 |
+
if (node) {
|
| 1490 |
+
entryCache.set(entry.id, node.cloneNode(true));
|
| 1491 |
+
}
|
| 1492 |
+
return node;
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
function navigateTo(targetId, scrollMode = 'target', scrollToEntryId = null) {
|
| 1496 |
+
currentLeafId = targetId;
|
| 1497 |
+
currentTargetId = scrollToEntryId || targetId;
|
| 1498 |
+
const path = getPath(targetId);
|
| 1499 |
+
|
| 1500 |
+
renderTree();
|
| 1501 |
+
|
| 1502 |
+
document.getElementById('header-container').innerHTML = renderHeader();
|
| 1503 |
+
attachHeaderHandlers();
|
| 1504 |
+
|
| 1505 |
+
// Build messages using cached DOM nodes
|
| 1506 |
+
const messagesEl = document.getElementById('messages');
|
| 1507 |
+
const fragment = document.createDocumentFragment();
|
| 1508 |
+
|
| 1509 |
+
for (const entry of path) {
|
| 1510 |
+
const node = renderEntryToNode(entry);
|
| 1511 |
+
if (node) {
|
| 1512 |
+
fragment.appendChild(node);
|
| 1513 |
+
}
|
| 1514 |
+
}
|
| 1515 |
+
|
| 1516 |
+
messagesEl.innerHTML = '';
|
| 1517 |
+
messagesEl.appendChild(fragment);
|
| 1518 |
+
|
| 1519 |
+
// Attach click handlers for copy-link buttons
|
| 1520 |
+
messagesEl.querySelectorAll('.copy-link-btn').forEach(btn => {
|
| 1521 |
+
btn.addEventListener('click', (e) => {
|
| 1522 |
+
e.stopPropagation();
|
| 1523 |
+
const entryId = btn.dataset.entryId;
|
| 1524 |
+
const shareUrl = buildShareUrl(entryId);
|
| 1525 |
+
copyToClipboard(shareUrl, btn);
|
| 1526 |
+
});
|
| 1527 |
+
});
|
| 1528 |
+
|
| 1529 |
+
// Use setTimeout(0) to ensure DOM is fully laid out before scrolling
|
| 1530 |
+
setTimeout(() => {
|
| 1531 |
+
const content = document.getElementById('content');
|
| 1532 |
+
if (scrollMode === 'bottom') {
|
| 1533 |
+
content.scrollTop = content.scrollHeight;
|
| 1534 |
+
} else if (scrollMode === 'target') {
|
| 1535 |
+
// If scrollToEntryId is provided, scroll to that specific entry.
|
| 1536 |
+
// Tool result entries are rendered inside their assistant tool-call block,
|
| 1537 |
+
// so route them to the visible tool-call element instead.
|
| 1538 |
+
const scrollTargetId = scrollToEntryId || targetId;
|
| 1539 |
+
const targetEl = document.getElementById(getScrollTargetElementId(scrollTargetId)) ||
|
| 1540 |
+
document.getElementById(`entry-${scrollTargetId}`);
|
| 1541 |
+
if (targetEl) {
|
| 1542 |
+
targetEl.scrollIntoView({ block: 'center' });
|
| 1543 |
+
// Briefly highlight the target message
|
| 1544 |
+
if (scrollToEntryId) {
|
| 1545 |
+
targetEl.classList.add('highlight');
|
| 1546 |
+
setTimeout(() => targetEl.classList.remove('highlight'), 2000);
|
| 1547 |
+
}
|
| 1548 |
+
}
|
| 1549 |
+
}
|
| 1550 |
+
}, 0);
|
| 1551 |
+
}
|
| 1552 |
+
|
| 1553 |
+
// ============================================================
|
| 1554 |
+
// INITIALIZATION
|
| 1555 |
+
// ============================================================
|
| 1556 |
+
|
| 1557 |
+
// Configure marked with syntax highlighting and TUI-compatible HTML handling
|
| 1558 |
+
const strictStrikethroughRegex = /^(~~)(?=[^\s~])((?:\\.|[^\\])*?(?:\\.|[^\s~\\]))\1(?=[^~]|$)/;
|
| 1559 |
+
|
| 1560 |
+
marked.use({
|
| 1561 |
+
breaks: true,
|
| 1562 |
+
gfm: true,
|
| 1563 |
+
tokenizer: {
|
| 1564 |
+
// Treat HTML-like input as plain text so tags are shown verbatim,
|
| 1565 |
+
// matching the TUI markdown renderer.
|
| 1566 |
+
html() {
|
| 1567 |
+
return undefined;
|
| 1568 |
+
},
|
| 1569 |
+
tag() {
|
| 1570 |
+
return undefined;
|
| 1571 |
+
},
|
| 1572 |
+
del(src) {
|
| 1573 |
+
const match = strictStrikethroughRegex.exec(src);
|
| 1574 |
+
if (!match) return undefined;
|
| 1575 |
+
return {
|
| 1576 |
+
type: 'del',
|
| 1577 |
+
raw: match[0],
|
| 1578 |
+
text: match[2],
|
| 1579 |
+
tokens: this.lexer.inlineTokens(match[2])
|
| 1580 |
+
};
|
| 1581 |
+
}
|
| 1582 |
+
},
|
| 1583 |
+
renderer: {
|
| 1584 |
+
// Sanitize link URLs with a scheme allow-list. Browsers strip C0
|
| 1585 |
+
// controls from schemes, so strip them before checking and emitting.
|
| 1586 |
+
link(token) {
|
| 1587 |
+
const href = sanitizeMarkdownUrl(token.href);
|
| 1588 |
+
if (href === null) {
|
| 1589 |
+
return this.parser.parseInline(token.tokens);
|
| 1590 |
+
}
|
| 1591 |
+
let out = '<a href="' + escapeHtml(href) + '"';
|
| 1592 |
+
if (token.title) {
|
| 1593 |
+
out += ' title="' + escapeHtml(token.title) + '"';
|
| 1594 |
+
}
|
| 1595 |
+
out += '>' + this.parser.parseInline(token.tokens) + '</a>';
|
| 1596 |
+
return out;
|
| 1597 |
+
},
|
| 1598 |
+
// Sanitize image src URLs with the same scheme allow-list.
|
| 1599 |
+
image(token) {
|
| 1600 |
+
const href = sanitizeMarkdownUrl(token.href);
|
| 1601 |
+
if (href === null) {
|
| 1602 |
+
return escapeHtml(token.text || '');
|
| 1603 |
+
}
|
| 1604 |
+
let out = '<img src="' + escapeHtml(href) + '" alt="' + escapeHtml(token.text || '') + '"';
|
| 1605 |
+
if (token.title) {
|
| 1606 |
+
out += ' title="' + escapeHtml(token.title) + '"';
|
| 1607 |
+
}
|
| 1608 |
+
out += '>';
|
| 1609 |
+
return out;
|
| 1610 |
+
},
|
| 1611 |
+
// Code blocks: syntax highlight, no HTML escaping
|
| 1612 |
+
code(token) {
|
| 1613 |
+
const code = token.text;
|
| 1614 |
+
const lang = token.lang;
|
| 1615 |
+
let highlighted;
|
| 1616 |
+
if (lang && hljs.getLanguage(lang)) {
|
| 1617 |
+
try {
|
| 1618 |
+
highlighted = hljs.highlight(code, { language: lang }).value;
|
| 1619 |
+
} catch {
|
| 1620 |
+
highlighted = escapeHtml(code);
|
| 1621 |
+
}
|
| 1622 |
+
} else {
|
| 1623 |
+
// Auto-detect language if not specified
|
| 1624 |
+
try {
|
| 1625 |
+
highlighted = hljs.highlightAuto(code).value;
|
| 1626 |
+
} catch {
|
| 1627 |
+
highlighted = escapeHtml(code);
|
| 1628 |
+
}
|
| 1629 |
+
}
|
| 1630 |
+
return `<pre><code class="hljs">${highlighted}</code></pre>`;
|
| 1631 |
+
},
|
| 1632 |
+
// Inline code: escape HTML
|
| 1633 |
+
codespan(token) {
|
| 1634 |
+
return `<code>${escapeHtml(token.text)}</code>`;
|
| 1635 |
+
}
|
| 1636 |
+
}
|
| 1637 |
+
});
|
| 1638 |
+
|
| 1639 |
+
// Simple marked parse (escaping handled in renderers)
|
| 1640 |
+
function safeMarkedParse(text) {
|
| 1641 |
+
return marked.parse(text);
|
| 1642 |
+
}
|
| 1643 |
+
|
| 1644 |
+
// Search input
|
| 1645 |
+
const searchInput = document.getElementById('tree-search');
|
| 1646 |
+
searchInput.addEventListener('input', (e) => {
|
| 1647 |
+
searchQuery = e.target.value;
|
| 1648 |
+
forceTreeRerender();
|
| 1649 |
+
});
|
| 1650 |
+
|
| 1651 |
+
// Filter buttons
|
| 1652 |
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
| 1653 |
+
btn.addEventListener('click', () => {
|
| 1654 |
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
| 1655 |
+
btn.classList.add('active');
|
| 1656 |
+
filterMode = btn.dataset.filter;
|
| 1657 |
+
forceTreeRerender();
|
| 1658 |
+
});
|
| 1659 |
+
});
|
| 1660 |
+
|
| 1661 |
+
// Sidebar toggle
|
| 1662 |
+
const sidebar = document.getElementById('sidebar');
|
| 1663 |
+
const overlay = document.getElementById('sidebar-overlay');
|
| 1664 |
+
const hamburger = document.getElementById('hamburger');
|
| 1665 |
+
const sidebarResizer = document.getElementById('sidebar-resizer');
|
| 1666 |
+
const SIDEBAR_WIDTH_STORAGE_KEY = 'pi-share:v1:sidebar-width';
|
| 1667 |
+
const MIN_CONTENT_WIDTH = 320;
|
| 1668 |
+
|
| 1669 |
+
function isMobileLayout() {
|
| 1670 |
+
return window.matchMedia('(max-width: 900px)').matches;
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
function getSidebarBounds() {
|
| 1674 |
+
const rootStyles = getComputedStyle(document.documentElement);
|
| 1675 |
+
const minWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-min-width')) || 240;
|
| 1676 |
+
const maxWidth = parseFloat(rootStyles.getPropertyValue('--sidebar-max-width')) || 720;
|
| 1677 |
+
const viewportMaxWidth = window.innerWidth - MIN_CONTENT_WIDTH;
|
| 1678 |
+
return {
|
| 1679 |
+
minWidth,
|
| 1680 |
+
maxWidth: Math.max(minWidth, Math.min(maxWidth, viewportMaxWidth))
|
| 1681 |
+
};
|
| 1682 |
+
}
|
| 1683 |
+
|
| 1684 |
+
function clampSidebarWidth(width) {
|
| 1685 |
+
const { minWidth, maxWidth } = getSidebarBounds();
|
| 1686 |
+
return Math.max(minWidth, Math.min(maxWidth, width));
|
| 1687 |
+
}
|
| 1688 |
+
|
| 1689 |
+
function applySidebarWidth(width) {
|
| 1690 |
+
document.documentElement.style.setProperty('--sidebar-width', `${Math.round(clampSidebarWidth(width))}px`);
|
| 1691 |
+
}
|
| 1692 |
+
|
| 1693 |
+
function loadSidebarWidth() {
|
| 1694 |
+
try {
|
| 1695 |
+
const raw = localStorage.getItem(SIDEBAR_WIDTH_STORAGE_KEY);
|
| 1696 |
+
if (raw === null) return null;
|
| 1697 |
+
const width = Number(raw);
|
| 1698 |
+
return Number.isFinite(width) ? width : null;
|
| 1699 |
+
} catch {
|
| 1700 |
+
return null;
|
| 1701 |
+
}
|
| 1702 |
+
}
|
| 1703 |
+
|
| 1704 |
+
function saveSidebarWidth(width) {
|
| 1705 |
+
try {
|
| 1706 |
+
localStorage.setItem(SIDEBAR_WIDTH_STORAGE_KEY, String(Math.round(clampSidebarWidth(width))));
|
| 1707 |
+
} catch {
|
| 1708 |
+
// Ignore storage failures (e.g. private browsing restrictions)
|
| 1709 |
+
}
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
function setupSidebarResize() {
|
| 1713 |
+
const savedWidth = loadSidebarWidth();
|
| 1714 |
+
if (savedWidth !== null) {
|
| 1715 |
+
applySidebarWidth(savedWidth);
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
if (!sidebarResizer) return;
|
| 1719 |
+
|
| 1720 |
+
let cleanupDrag = null;
|
| 1721 |
+
|
| 1722 |
+
const stopDrag = (pointerId) => {
|
| 1723 |
+
if (cleanupDrag) {
|
| 1724 |
+
cleanupDrag(pointerId);
|
| 1725 |
+
cleanupDrag = null;
|
| 1726 |
+
}
|
| 1727 |
+
};
|
| 1728 |
+
|
| 1729 |
+
sidebarResizer.addEventListener('pointerdown', (e) => {
|
| 1730 |
+
if (isMobileLayout()) return;
|
| 1731 |
+
|
| 1732 |
+
e.preventDefault();
|
| 1733 |
+
const startX = e.clientX;
|
| 1734 |
+
const startWidth = sidebar.getBoundingClientRect().width;
|
| 1735 |
+
document.body.classList.add('sidebar-resizing');
|
| 1736 |
+
sidebarResizer.setPointerCapture?.(e.pointerId);
|
| 1737 |
+
|
| 1738 |
+
const onPointerMove = (event) => {
|
| 1739 |
+
applySidebarWidth(startWidth + (event.clientX - startX));
|
| 1740 |
+
};
|
| 1741 |
+
|
| 1742 |
+
cleanupDrag = (pointerIdToRelease) => {
|
| 1743 |
+
document.body.classList.remove('sidebar-resizing');
|
| 1744 |
+
sidebarResizer.releasePointerCapture?.(pointerIdToRelease);
|
| 1745 |
+
window.removeEventListener('pointermove', onPointerMove);
|
| 1746 |
+
window.removeEventListener('pointerup', onPointerUp);
|
| 1747 |
+
window.removeEventListener('pointercancel', onPointerCancel);
|
| 1748 |
+
saveSidebarWidth(sidebar.getBoundingClientRect().width);
|
| 1749 |
+
};
|
| 1750 |
+
|
| 1751 |
+
const onPointerUp = (event) => stopDrag(event.pointerId);
|
| 1752 |
+
const onPointerCancel = (event) => stopDrag(event.pointerId);
|
| 1753 |
+
|
| 1754 |
+
window.addEventListener('pointermove', onPointerMove);
|
| 1755 |
+
window.addEventListener('pointerup', onPointerUp);
|
| 1756 |
+
window.addEventListener('pointercancel', onPointerCancel);
|
| 1757 |
+
});
|
| 1758 |
+
|
| 1759 |
+
sidebarResizer.addEventListener('dblclick', () => {
|
| 1760 |
+
if (isMobileLayout()) return;
|
| 1761 |
+
applySidebarWidth(400);
|
| 1762 |
+
saveSidebarWidth(400);
|
| 1763 |
+
});
|
| 1764 |
+
|
| 1765 |
+
window.addEventListener('resize', () => {
|
| 1766 |
+
if (isMobileLayout()) return;
|
| 1767 |
+
applySidebarWidth(sidebar.getBoundingClientRect().width);
|
| 1768 |
+
});
|
| 1769 |
+
}
|
| 1770 |
+
|
| 1771 |
+
setupSidebarResize();
|
| 1772 |
+
|
| 1773 |
+
hamburger.addEventListener('click', () => {
|
| 1774 |
+
sidebar.classList.add('open');
|
| 1775 |
+
overlay.classList.add('open');
|
| 1776 |
+
hamburger.style.display = 'none';
|
| 1777 |
+
});
|
| 1778 |
+
|
| 1779 |
+
const closeSidebar = () => {
|
| 1780 |
+
sidebar.classList.remove('open');
|
| 1781 |
+
overlay.classList.remove('open');
|
| 1782 |
+
hamburger.style.display = '';
|
| 1783 |
+
};
|
| 1784 |
+
|
| 1785 |
+
overlay.addEventListener('click', closeSidebar);
|
| 1786 |
+
document.getElementById('sidebar-close').addEventListener('click', closeSidebar);
|
| 1787 |
+
|
| 1788 |
+
// Toggle states
|
| 1789 |
+
let thinkingExpanded = true;
|
| 1790 |
+
let toolOutputsExpanded = false;
|
| 1791 |
+
|
| 1792 |
+
const toggleThinking = () => {
|
| 1793 |
+
thinkingExpanded = !thinkingExpanded;
|
| 1794 |
+
document.querySelectorAll('.thinking-text').forEach(el => {
|
| 1795 |
+
el.style.display = thinkingExpanded ? '' : 'none';
|
| 1796 |
+
});
|
| 1797 |
+
document.querySelectorAll('.thinking-collapsed').forEach(el => {
|
| 1798 |
+
el.style.display = thinkingExpanded ? 'none' : 'block';
|
| 1799 |
+
});
|
| 1800 |
+
};
|
| 1801 |
+
|
| 1802 |
+
const toggleToolOutputs = () => {
|
| 1803 |
+
toolOutputsExpanded = !toolOutputsExpanded;
|
| 1804 |
+
document.querySelectorAll('.tool-output.expandable').forEach(el => {
|
| 1805 |
+
el.classList.toggle('expanded', toolOutputsExpanded);
|
| 1806 |
+
});
|
| 1807 |
+
document.querySelectorAll('.compaction').forEach(el => {
|
| 1808 |
+
el.classList.toggle('expanded', toolOutputsExpanded);
|
| 1809 |
+
});
|
| 1810 |
+
document.querySelectorAll('.skill-invocation').forEach(el => {
|
| 1811 |
+
el.classList.toggle('expanded', toolOutputsExpanded);
|
| 1812 |
+
});
|
| 1813 |
+
};
|
| 1814 |
+
|
| 1815 |
+
const attachHeaderHandlers = () => {
|
| 1816 |
+
document.querySelector('[data-action="toggle-thinking"]')?.addEventListener('click', toggleThinking);
|
| 1817 |
+
document.querySelector('[data-action="toggle-tools"]')?.addEventListener('click', toggleToolOutputs);
|
| 1818 |
+
};
|
| 1819 |
+
|
| 1820 |
+
const isEditableTarget = (element) => {
|
| 1821 |
+
if (!element) return false;
|
| 1822 |
+
const tagName = element.tagName;
|
| 1823 |
+
if (tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || tagName === 'BUTTON') {
|
| 1824 |
+
return true;
|
| 1825 |
+
}
|
| 1826 |
+
return element.isContentEditable || Boolean(element.closest?.('[contenteditable="true"]'));
|
| 1827 |
+
};
|
| 1828 |
+
|
| 1829 |
+
// Keyboard shortcuts
|
| 1830 |
+
document.addEventListener('keydown', (e) => {
|
| 1831 |
+
if (e.key === 'Escape') {
|
| 1832 |
+
searchInput.value = '';
|
| 1833 |
+
searchQuery = '';
|
| 1834 |
+
navigateTo(leafId, 'bottom');
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
if (isEditableTarget(document.activeElement)) {
|
| 1838 |
+
return;
|
| 1839 |
+
}
|
| 1840 |
+
|
| 1841 |
+
const key = e.key.toLowerCase();
|
| 1842 |
+
if (key === 't') {
|
| 1843 |
+
e.preventDefault();
|
| 1844 |
+
toggleThinking();
|
| 1845 |
+
} else if (key === 'o') {
|
| 1846 |
+
e.preventDefault();
|
| 1847 |
+
toggleToolOutputs();
|
| 1848 |
+
}
|
| 1849 |
+
});
|
| 1850 |
+
|
| 1851 |
+
// Initial render
|
| 1852 |
+
// If URL has targetId, scroll to that specific message; otherwise stay at top
|
| 1853 |
+
if (leafId) {
|
| 1854 |
+
if (urlTargetId && byId.has(urlTargetId)) {
|
| 1855 |
+
// Deep link: navigate to leaf and scroll to target message
|
| 1856 |
+
navigateTo(leafId, 'target', urlTargetId);
|
| 1857 |
+
} else {
|
| 1858 |
+
navigateTo(leafId, 'none');
|
| 1859 |
+
}
|
| 1860 |
+
} else if (entries.length > 0) {
|
| 1861 |
+
// Fallback: use last entry if no leafId
|
| 1862 |
+
navigateTo(entries[entries.length - 1].id, 'none');
|
| 1863 |
+
}
|
| 1864 |
+
})();
|
packages/coding-agent/src/core/export-html/tool-renderer.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Tool HTML renderer for custom tools in HTML export.
|
| 3 |
+
*
|
| 4 |
+
* Renders custom tool calls and results to HTML by invoking their TUI renderers
|
| 5 |
+
* and converting the ANSI output to HTML.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
| 9 |
+
import type { Component } from "@earendil-works/pi-tui";
|
| 10 |
+
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
| 11 |
+
import type { ToolDefinition, ToolRenderContext } from "../extensions/types.ts";
|
| 12 |
+
import { ansiLinesToHtml } from "./ansi-to-html.ts";
|
| 13 |
+
|
| 14 |
+
export interface ToolHtmlRendererDeps {
|
| 15 |
+
/** Function to look up tool definition by name */
|
| 16 |
+
getToolDefinition: (name: string) => ToolDefinition | undefined;
|
| 17 |
+
/** Theme for styling */
|
| 18 |
+
theme: Theme;
|
| 19 |
+
/** Working directory for render context */
|
| 20 |
+
cwd: string;
|
| 21 |
+
/** Terminal width for rendering (default: 100) */
|
| 22 |
+
width?: number;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface ToolHtmlRenderer {
|
| 26 |
+
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
|
| 27 |
+
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined;
|
| 28 |
+
/** Render a tool result to collapsed/expanded HTML. Returns undefined if tool has no custom renderer. */
|
| 29 |
+
renderResult(
|
| 30 |
+
toolCallId: string,
|
| 31 |
+
toolName: string,
|
| 32 |
+
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
|
| 33 |
+
details: unknown,
|
| 34 |
+
isError: boolean,
|
| 35 |
+
): { collapsed?: string; expanded?: string } | undefined;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
/**
|
| 39 |
+
* Create a tool HTML renderer.
|
| 40 |
+
*
|
| 41 |
+
* The renderer looks up tool definitions and invokes their renderCall/renderResult
|
| 42 |
+
* methods, converting the resulting TUI Component output (ANSI) to HTML.
|
| 43 |
+
*/
|
| 44 |
+
const ANSI_ESCAPE_REGEX = /\x1b\[[\d;]*m/g;
|
| 45 |
+
|
| 46 |
+
function isBlankRenderedLine(line: string): boolean {
|
| 47 |
+
return line.replace(ANSI_ESCAPE_REGEX, "").trim().length === 0;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
function trimRenderedResultLines(lines: string[]): string[] {
|
| 51 |
+
let start = 0;
|
| 52 |
+
let end = lines.length;
|
| 53 |
+
while (start < end && isBlankRenderedLine(lines[start])) start++;
|
| 54 |
+
while (end > start && isBlankRenderedLine(lines[end - 1])) end--;
|
| 55 |
+
return lines.slice(start, end);
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRenderer {
|
| 59 |
+
const { getToolDefinition, theme, cwd, width = 100 } = deps;
|
| 60 |
+
|
| 61 |
+
const renderedCallComponents = new Map<string, Component>();
|
| 62 |
+
const renderedResultComponents = new Map<string, Component>();
|
| 63 |
+
const renderedStates = new Map<string, any>();
|
| 64 |
+
const renderedArgs = new Map<string, unknown>();
|
| 65 |
+
|
| 66 |
+
const getState = (toolCallId: string): any => {
|
| 67 |
+
let state = renderedStates.get(toolCallId);
|
| 68 |
+
if (!state) {
|
| 69 |
+
state = {};
|
| 70 |
+
renderedStates.set(toolCallId, state);
|
| 71 |
+
}
|
| 72 |
+
return state;
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
const createRenderContext = (
|
| 76 |
+
toolCallId: string,
|
| 77 |
+
lastComponent: Component | undefined,
|
| 78 |
+
expanded: boolean,
|
| 79 |
+
isPartial: boolean,
|
| 80 |
+
isError: boolean,
|
| 81 |
+
): ToolRenderContext => {
|
| 82 |
+
return {
|
| 83 |
+
args: renderedArgs.get(toolCallId),
|
| 84 |
+
toolCallId,
|
| 85 |
+
invalidate: () => {},
|
| 86 |
+
lastComponent,
|
| 87 |
+
state: getState(toolCallId),
|
| 88 |
+
cwd,
|
| 89 |
+
executionStarted: true,
|
| 90 |
+
argsComplete: true,
|
| 91 |
+
isPartial,
|
| 92 |
+
expanded,
|
| 93 |
+
showImages: false,
|
| 94 |
+
isError,
|
| 95 |
+
};
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
return {
|
| 99 |
+
renderCall(toolCallId: string, toolName: string, args: unknown): string | undefined {
|
| 100 |
+
try {
|
| 101 |
+
renderedArgs.set(toolCallId, args);
|
| 102 |
+
const toolDef = getToolDefinition(toolName);
|
| 103 |
+
if (!toolDef?.renderCall) {
|
| 104 |
+
return undefined;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const component = toolDef.renderCall(
|
| 108 |
+
args,
|
| 109 |
+
theme,
|
| 110 |
+
createRenderContext(toolCallId, renderedCallComponents.get(toolCallId), false, true, false),
|
| 111 |
+
);
|
| 112 |
+
renderedCallComponents.set(toolCallId, component);
|
| 113 |
+
const lines = component.render(width);
|
| 114 |
+
return ansiLinesToHtml(lines);
|
| 115 |
+
} catch {
|
| 116 |
+
// On error, return undefined so HTML export can fall back to structured result rendering
|
| 117 |
+
return undefined;
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
|
| 121 |
+
renderResult(
|
| 122 |
+
toolCallId: string,
|
| 123 |
+
toolName: string,
|
| 124 |
+
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
|
| 125 |
+
details: unknown,
|
| 126 |
+
isError: boolean,
|
| 127 |
+
): { collapsed?: string; expanded?: string } | undefined {
|
| 128 |
+
try {
|
| 129 |
+
const toolDef = getToolDefinition(toolName);
|
| 130 |
+
if (!toolDef?.renderResult) {
|
| 131 |
+
return undefined;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
// Build AgentToolResult from content array
|
| 135 |
+
// Cast content since session storage uses generic object types
|
| 136 |
+
const agentToolResult = {
|
| 137 |
+
content: result as (TextContent | ImageContent)[],
|
| 138 |
+
details,
|
| 139 |
+
isError,
|
| 140 |
+
};
|
| 141 |
+
|
| 142 |
+
// Render collapsed
|
| 143 |
+
const collapsedComponent = toolDef.renderResult(
|
| 144 |
+
agentToolResult,
|
| 145 |
+
{ expanded: false, isPartial: false },
|
| 146 |
+
theme,
|
| 147 |
+
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), false, false, isError),
|
| 148 |
+
);
|
| 149 |
+
renderedResultComponents.set(toolCallId, collapsedComponent);
|
| 150 |
+
const collapsed = ansiLinesToHtml(trimRenderedResultLines(collapsedComponent.render(width)));
|
| 151 |
+
|
| 152 |
+
// Render expanded
|
| 153 |
+
const expandedComponent = toolDef.renderResult(
|
| 154 |
+
agentToolResult,
|
| 155 |
+
{ expanded: true, isPartial: false },
|
| 156 |
+
theme,
|
| 157 |
+
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), true, false, isError),
|
| 158 |
+
);
|
| 159 |
+
renderedResultComponents.set(toolCallId, expandedComponent);
|
| 160 |
+
const expanded = ansiLinesToHtml(trimRenderedResultLines(expandedComponent.render(width)));
|
| 161 |
+
|
| 162 |
+
return {
|
| 163 |
+
...(collapsed && collapsed !== expanded ? { collapsed } : {}),
|
| 164 |
+
expanded,
|
| 165 |
+
};
|
| 166 |
+
} catch {
|
| 167 |
+
// On error, return undefined so HTML export can fall back to structured result rendering
|
| 168 |
+
return undefined;
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
};
|
| 172 |
+
}
|
packages/coding-agent/src/core/export-html/vendor/highlight.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
packages/coding-agent/src/core/export-html/vendor/marked.min.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* marked v18.0.5 - a markdown parser
|
| 3 |
+
* Copyright (c) 2018-2026, MarkedJS. (MIT License)
|
| 4 |
+
* Copyright (c) 2011-2018, Christopher Jeffrey. (MIT License)
|
| 5 |
+
* https://github.com/markedjs/marked
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* DO NOT EDIT THIS FILE
|
| 10 |
+
* The code in this file is generated from files in ./src/
|
| 11 |
+
*/
|
| 12 |
+
(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("marked",f)}else {g["marked"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
|
| 13 |
+
"use strict";var N=Object.defineProperty;var Oe=Object.getOwnPropertyDescriptor;var we=Object.getOwnPropertyNames;var ye=Object.prototype.hasOwnProperty;var Pe=(l,e)=>{for(var t in e)N(l,t,{get:e[t],enumerable:!0})},Se=(l,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of we(e))!ye.call(l,s)&&s!==t&&N(l,s,{get:()=>e[s],enumerable:!(n=Oe(e,s))||n.enumerable});return l};var $e=l=>Se(N({},"__esModule",{value:!0}),l);var Rt={};Pe(Rt,{Hooks:()=>P,Lexer:()=>x,Marked:()=>C,Parser:()=>b,Renderer:()=>y,TextRenderer:()=>S,Tokenizer:()=>w,defaults:()=>T,getDefaults:()=>_,lexer:()=>bt,marked:()=>g,options:()=>ht,parse:()=>mt,parseInline:()=>ft,parser:()=>xt,setOptions:()=>kt,use:()=>dt,walkTokens:()=>gt});module.exports=$e(Rt);function _(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var T=_();function Q(l){T=l}var z={exec:()=>null};function E(l){let e=[];return t=>{let n=Math.max(0,Math.min(3,t-1)),s=e[n];return s||(s=l(n),e[n]=s),s}}function d(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(s,r)=>{let i=typeof r=="string"?r:r.source;return i=i.replace(m.caret,"$1"),t=t.replace(s,i),n},getRegex:()=>new RegExp(t,e)};return n}var Le=((l="")=>{try{return!!new RegExp("(?<=1)(?<!1)"+l)}catch{return!1}})(),m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] +\S/,listReplaceTask:/^\[[ xX]\] +/,listTaskCheckbox:/\[[ xX]\]/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:E(l=>new RegExp(`^ {0,${l}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`)),hrRegex:E(l=>new RegExp(`^ {0,${l}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`)),fencesBeginRegex:E(l=>new RegExp(`^ {0,${l}}(?:\`\`\`|~~~)`)),headingBeginRegex:E(l=>new RegExp(`^ {0,${l}}#`)),htmlBeginRegex:E(l=>new RegExp(`^ {0,${l}}<(?:[a-z].*>|!--)`,"i")),blockquoteBeginRegex:E(l=>new RegExp(`^ {0,${l}}>`))},_e=/^(?:[ \t]*(?:\n|$))+/,ze=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Me=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,D=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Ee=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,F=/ {0,3}(?:[*+-]|\d{1,9}[.)])/,ae=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,le=d(ae).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Ie=d(ae).replace(/bull/g,F).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),U=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,Ae=/^[^\n]+/,K=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,Ce=d(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",K).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Be=d(/^(bull)([ \t][^\n]*?)?(?:\n|$)/).replace(/bull/g,F).getRegex(),H="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",W=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,De=d("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",W).replace("tag",H).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),ue=d(U).replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex(),qe=d(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",ue).getRegex(),X={blockquote:qe,code:ze,def:Ce,fences:Me,heading:Ee,hr:D,html:De,lheading:le,list:Be,newline:_e,paragraph:ue,table:z,text:Ae},ie=d("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex(),ve={...X,lheading:Ie,table:ie,paragraph:d(U).replace("hr",D).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",ie).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)])[ \\t]+[^ \\t\\n]").replace("html","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",H).getRegex()},He={...X,html:d(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\s[^'"/>\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",W).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:z,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:d(U).replace("hr",D).replace("heading",` *#{1,6} *[^
|
| 14 |
+
]`).replace("lheading",le).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Ze=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,Ge=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,pe=/^( {2,}|\\)\n(?!\s*$)/,Ne=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,I=/[\p{P}\p{S}]/u,Z=/[\s\p{P}\p{S}]/u,J=/[^\s\p{P}\p{S}]/u,Qe=d(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,Z).getRegex(),ce=/(?!~)[\p{P}\p{S}]/u,je=/(?!~)[\s\p{P}\p{S}]/u,Fe=/(?:[^\s\p{P}\p{S}]|~)/u,Ue=d(/link|precode-code|html/,"g").replace("link",/\[(?:[^\[\]`]|(?<a>`+)[^`]+\k<a>(?!`))*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)/).replace("precode-",Le?"(?<!`)()":"(^^|[^`])").replace("code",/(?<b>`+)[^`]+\k<b>(?!`)/).replace("html",/<(?! )[^<>]*?>/).getRegex(),he=/^(?:\*+(?:((?!\*)punct)|([^\s*]))?)|^_+(?:((?!_)punct)|([^\s_]))?/,Ke=d(he,"u").replace(/punct/g,I).getRegex(),We=d(he,"u").replace(/punct/g,ce).getRegex(),ke="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",Xe=d(ke,"gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),Je=d(ke,"gu").replace(/notPunctSpace/g,Fe).replace(/punctSpace/g,je).replace(/punct/g,ce).getRegex(),Ve=d("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),Ye=d(/^~~?(?:((?!~)punct)|[^\s~])/,"u").replace(/punct/g,I).getRegex(),et="^[^~]+(?=[^~])|(?!~)punct(~~?)(?=[\\s]|$)|notPunctSpace(~~?)(?!~)(?=punctSpace|$)|(?!~)punctSpace(~~?)(?=notPunctSpace)|[\\s](~~?)(?!~)(?=punct)|(?!~)punct(~~?)(?!~)(?=punct)|notPunctSpace(~~?)(?=notPunctSpace)",tt=d(et,"gu").replace(/notPunctSpace/g,J).replace(/punctSpace/g,Z).replace(/punct/g,I).getRegex(),nt=d(/\\(punct)/,"gu").replace(/punct/g,I).getRegex(),rt=d(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),st=d(W).replace("(?:-->|$)","-->").getRegex(),it=d("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",st).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),v=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`+(?!`)[^`]*?`+(?!`)|``+(?=\])|[^\[\]\\`])*?/,ot=d(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]+(?:\n[ \t]*)?|\n[ \t]*)(title))?\s*\)/).replace("label",v).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),de=d(/^!?\[(label)\]\[(ref)\]/).replace("label",v).replace("ref",K).getRegex(),ge=d(/^!?\[(ref)\](?:\[\])?/).replace("ref",K).getRegex(),at=d("reflink|nolink(?!\\()","g").replace("reflink",de).replace("nolink",ge).getRegex(),oe=/[hH][tT][tT][pP][sS]?|[fF][tT][pP]/,V={_backpedal:z,anyPunctuation:nt,autolink:rt,blockSkip:Ue,br:pe,code:Ge,del:z,delLDelim:z,delRDelim:z,emStrongLDelim:Ke,emStrongRDelimAst:Xe,emStrongRDelimUnd:Ve,escape:Ze,link:ot,nolink:ge,punctuation:Qe,reflink:de,reflinkSearch:at,tag:it,text:Ne,url:z},lt={...V,link:d(/^!?\[(label)\]\((.*?)\)/).replace("label",v).getRegex(),reflink:d(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",v).getRegex()},j={...V,emStrongRDelimAst:Je,emStrongLDelim:We,delLDelim:Ye,delRDelim:tt,url:d(/^((?:protocol):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/).replace("protocol",oe).replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:d(/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|protocol:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/).replace("protocol",oe).getRegex()},ut={...j,br:d(pe).replace("{2,}","*").getRegex(),text:d(j.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},q={normal:X,gfm:ve,pedantic:He},A={normal:V,gfm:j,breaks:ut,pedantic:lt};var pt={"&":"&","<":"<",">":">",'"':""","'":"'"},fe=l=>pt[l];function O(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,fe)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,fe);return l}function Y(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function ee(l,e){let t=l.replace(m.findPipe,(r,i,o)=>{let u=!1,a=i;for(;--a>=0&&o[a]==="\\";)u=!u;return u?"|":" |"}),n=t.split(m.splitPipe),s=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;s<n.length;s++)n[s]=n[s].trim().replace(m.slashPipe,"|");return n}function L(l,e,t){let n=l.length;if(n===0)return"";let s=0;for(;s<n;){let r=l.charAt(n-s-1);if(r===e&&!t)s++;else if(r!==e&&t)s++;else break}return l.slice(0,n-s)}function te(l){let e=l.split(`
|
| 15 |
+
`),t=e.length-1;for(;t>=0&&m.blankLine.test(e[t]);)t--;return e.length-t<=2?l:e.slice(0,t+1).join(`
|
| 16 |
+
`)}function me(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-2:-1}function xe(l,e=0){let t=e,n="";for(let s of l)if(s===" "){let r=4-t%4;n+=" ".repeat(r),t+=r}else n+=s,t++;return n}function be(l,e,t,n,s){let r=e.href,i=e.title||null,o=l[1].replace(s.other.outputLinkReplace,"$1");n.state.inLink=!0;let u={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:r,title:i,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,u}function ct(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let s=n[1];return e.split(`
|
| 17 |
+
`).map(r=>{let i=r.match(t.other.beginningSpace);if(i===null)return r;let[o]=i;return o.length>=s.length?r.slice(s.length):r}).join(`
|
| 18 |
+
`)}var w=class{options;rules;lexer;constructor(e){this.options=e||T}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=this.options.pedantic?t[0]:te(t[0]),s=n.replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:n,codeBlockStyle:"indented",text:s}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],s=ct(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:s}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let s=L(n,"#");(this.options.pedantic||!s||this.rules.other.endingSpaceChar.test(s))&&(n=s.trim())}return{type:"heading",raw:L(t[0],`
|
| 19 |
+
`),depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:L(t[0],`
|
| 20 |
+
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=L(t[0],`
|
| 21 |
+
`).split(`
|
| 22 |
+
`),s="",r="",i=[];for(;n.length>0;){let o=!1,u=[],a;for(a=0;a<n.length;a++)if(this.rules.other.blockquoteStart.test(n[a]))u.push(n[a]),o=!0;else if(!o)u.push(n[a]);else break;n=n.slice(a);let c=u.join(`
|
| 23 |
+
`),p=c.replace(this.rules.other.blockquoteSetextReplace,`
|
| 24 |
+
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");s=s?`${s}
|
| 25 |
+
${c}`:c,r=r?`${r}
|
| 26 |
+
${p}`:p;let k=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(p,i,!0),this.lexer.state.top=k,n.length===0)break;let h=i.at(-1);if(h?.type==="code")break;if(h?.type==="blockquote"){let R=h,f=R.raw+`
|
| 27 |
+
`+n.join(`
|
| 28 |
+
`),$=this.blockquote(f);i[i.length-1]=$,s=s.substring(0,s.length-R.raw.length)+$.raw,r=r.substring(0,r.length-R.text.length)+$.text;break}else if(h?.type==="list"){let R=h,f=R.raw+`
|
| 29 |
+
`+n.join(`
|
| 30 |
+
`),$=this.list(f);i[i.length-1]=$,s=s.substring(0,s.length-h.raw.length)+$.raw,r=r.substring(0,r.length-R.raw.length)+$.raw,n=f.substring(i.at(-1).raw.length).split(`
|
| 31 |
+
`);continue}}return{type:"blockquote",raw:s,tokens:i,text:r}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");let i=this.rules.other.listItemRegex(n),o=!1;for(;e;){let a=!1,c="",p="";if(!(t=i.exec(e))||this.rules.block.hr.test(e))break;c=t[0],e=e.substring(c.length);let k=xe(t[2].split(`
|
| 32 |
+
`,1)[0],t[1].length),h=e.split(`
|
| 33 |
+
`,1)[0],R=!k.trim(),f=0;if(this.options.pedantic?(f=2,p=k.trimStart()):R?f=t[1].length+1:(f=k.search(this.rules.other.nonSpaceChar),f=f>4?1:f,p=k.slice(f),f+=t[1].length),R&&this.rules.other.blankLine.test(h)&&(c+=h+`
|
| 34 |
+
`,e=e.substring(h.length+1),a=!0),!a){let $=this.rules.other.nextBulletRegex(f),ne=this.rules.other.hrRegex(f),re=this.rules.other.fencesBeginRegex(f),se=this.rules.other.headingBeginRegex(f),Re=this.rules.other.htmlBeginRegex(f),Te=this.rules.other.blockquoteBeginRegex(f);for(;e;){let G=e.split(`
|
| 35 |
+
`,1)[0],B;if(h=G,this.options.pedantic?(h=h.replace(this.rules.other.listReplaceNesting," "),B=h):B=h.replace(this.rules.other.tabCharGlobal," "),re.test(h)||se.test(h)||Re.test(h)||Te.test(h)||$.test(h)||ne.test(h))break;if(B.search(this.rules.other.nonSpaceChar)>=f||!h.trim())p+=`
|
| 36 |
+
`+B.slice(f);else{if(R||k.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||re.test(k)||se.test(k)||ne.test(k))break;p+=`
|
| 37 |
+
`+h}R=!h.trim(),c+=G+`
|
| 38 |
+
`,e=e.substring(G.length+1),k=B.slice(f)}}r.loose||(o?r.loose=!0:this.rules.other.doubleBlankLine.test(c)&&(o=!0)),r.items.push({type:"list_item",raw:c,task:!!this.options.gfm&&this.rules.other.listIsTask.test(p),loose:!1,text:p,tokens:[]}),r.raw+=c}let u=r.items.at(-1);if(u)u.raw=u.raw.trimEnd(),u.text=u.text.trimEnd();else return;r.raw=r.raw.trimEnd();for(let a of r.items){this.lexer.state.top=!1,a.tokens=this.lexer.blockTokens(a.text,[]);let c=a.tokens[0];if(a.task&&(c?.type==="text"||c?.type==="paragraph")){a.text=a.text.replace(this.rules.other.listReplaceTask,""),c.raw=c.raw.replace(this.rules.other.listReplaceTask,""),c.text=c.text.replace(this.rules.other.listReplaceTask,"");for(let k=this.lexer.inlineQueue.length-1;k>=0;k--)if(this.rules.other.listIsTask.test(this.lexer.inlineQueue[k].src)){this.lexer.inlineQueue[k].src=this.lexer.inlineQueue[k].src.replace(this.rules.other.listReplaceTask,"");break}let p=this.rules.other.listTaskCheckbox.exec(a.raw);if(p){let k={type:"checkbox",raw:p[0]+" ",checked:p[0]!=="[ ]"};a.checked=k.checked,r.loose?a.tokens[0]&&["paragraph","text"].includes(a.tokens[0].type)&&"tokens"in a.tokens[0]&&a.tokens[0].tokens?(a.tokens[0].raw=k.raw+a.tokens[0].raw,a.tokens[0].text=k.raw+a.tokens[0].text,a.tokens[0].tokens.unshift(k)):a.tokens.unshift({type:"paragraph",raw:k.raw,text:k.raw,tokens:[k]}):a.tokens.unshift(k)}}else a.task&&(a.task=!1);if(!r.loose){let p=a.tokens.filter(h=>h.type==="space"),k=p.length>0&&p.some(h=>this.rules.other.anyLine.test(h.raw));r.loose=k}}if(r.loose)for(let a of r.items){a.loose=!0;for(let c of a.tokens)c.type==="text"&&(c.type="paragraph")}return r}}html(e){let t=this.rules.block.html.exec(e);if(t){let n=te(t[0]);return{type:"html",block:!0,raw:n,pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:n}}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),s=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",r=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:L(t[0],`
|
| 39 |
+
`),href:s,title:r}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=ee(t[1]),s=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),r=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
|
| 40 |
+
`):[],i={type:"table",raw:L(t[0],`
|
| 41 |
+
`),header:[],align:[],rows:[]};if(n.length===s.length){for(let o of s)this.rules.other.tableAlignRight.test(o)?i.align.push("right"):this.rules.other.tableAlignCenter.test(o)?i.align.push("center"):this.rules.other.tableAlignLeft.test(o)?i.align.push("left"):i.align.push(null);for(let o=0;o<n.length;o++)i.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:i.align[o]});for(let o of r)i.rows.push(ee(o,i.header.length).map((u,a)=>({text:u,tokens:this.lexer.inline(u),header:!1,align:i.align[a]})));return i}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t){let n=t[1].trim();return{type:"heading",raw:L(t[0],`
|
| 42 |
+
`),depth:t[2].charAt(0)==="="?1:2,text:n,tokens:this.lexer.inline(n)}}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
|
| 43 |
+
`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let i=L(n.slice(0,-1),"\\");if((n.length-i.length)%2===0)return}else{let i=me(t[2],"()");if(i===-2)return;if(i>-1){let u=(t[0].indexOf("!")===0?5:4)+t[1].length+i;t[2]=t[2].substring(0,i),t[0]=t[0].substring(0,u).trim(),t[3]=""}}let s=t[2],r="";if(this.options.pedantic){let i=this.rules.other.pedanticHrefTitle.exec(s);i&&(s=i[1],r=i[3])}else r=t[3]?t[3].slice(1,-1):"";return s=s.trim(),this.rules.other.startAngleBracket.test(s)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?s=s.slice(1):s=s.slice(1,-1)),be(t,{href:s&&s.replace(this.rules.inline.anyPunctuation,"$1"),title:r&&r.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let s=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),r=t[s.toLowerCase()];if(!r){let i=n[0].charAt(0);return{type:"text",raw:i,text:i}}return be(n,r,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s||!s[1]&&!s[2]&&!s[3]&&!s[4]||s[4]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(s[1]||s[3]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,c=0,p=s[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(p.lastIndex=0,t=t.slice(-1*e.length+i);(s=p.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o)continue;if(u=[...o].length,s[3]||s[4]){a+=u;continue}else if((s[5]||s[6])&&i%3&&!((i+u)%3)){c+=u;continue}if(a-=u,a>0)continue;u=Math.min(u,u+a+c);let k=[...s[0]][0].length,h=e.slice(0,i+s.index+k+u);if(Math.min(i,u)%2){let f=h.slice(1,-1);return{type:"em",raw:h,text:f,tokens:this.lexer.inlineTokens(f)}}let R=h.slice(2,-2);return{type:"strong",raw:h,text:R,tokens:this.lexer.inlineTokens(R)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),s=this.rules.other.nonSpaceChar.test(n),r=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return s&&r&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e,t,n=""){let s=this.rules.inline.delLDelim.exec(e);if(!s)return;if(!(s[1]||"")||!n||this.rules.inline.punctuation.exec(n)){let i=[...s[0]].length-1,o,u,a=i,c=this.rules.inline.delRDelim;for(c.lastIndex=0,t=t.slice(-1*e.length+i);(s=c.exec(t))!==null;){if(o=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!o||(u=[...o].length,u!==i))continue;if(s[3]||s[4]){a+=u;continue}if(a-=u,a>0)continue;u=Math.min(u,u+a);let p=[...s[0]][0].length,k=e.slice(0,i+s.index+p+u),h=k.slice(i,-i);return{type:"del",raw:k,text:h,tokens:this.lexer.inlineTokens(h)}}}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,s;return t[2]==="@"?(n=t[1],s="mailto:"+n):(n=t[1],s=n),{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,s;if(t[2]==="@")n=t[0],s="mailto:"+n;else{let r;do r=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(r!==t[0]);n=t[0],t[1]==="www."?s="http://"+t[0]:s=t[0]}return{type:"link",raw:t[0],text:n,href:s,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var x=class l{tokens;options;state;inlineQueue;tokenizer;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||T,this.options.tokenizer=this.options.tokenizer||new w,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:q.normal,inline:A.normal};this.options.pedantic?(t.block=q.pedantic,t.inline=A.pedantic):this.options.gfm&&(t.block=q.gfm,this.options.breaks?t.inline=A.breaks:t.inline=A.gfm),this.tokenizer.rules=t}static get rules(){return{block:q,inline:A}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
|
| 44 |
+
`),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){this.tokenizer.lexer=this,this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));let s=1/0;for(;e;){if(e.length<s)s=e.length;else{this.infiniteLoopError(e.charCodeAt(0));break}let r;if(this.options.extensions?.block?.some(o=>(r=o.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let o=t.at(-1);r.raw.length===1&&o!==void 0?o.raw+=`
|
| 45 |
+
`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(`
|
| 46 |
+
`)?"":`
|
| 47 |
+
`)+r.raw,o.text+=`
|
| 48 |
+
`+r.text,this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="paragraph"||o?.type==="text"?(o.raw+=(o.raw.endsWith(`
|
| 49 |
+
`)?"":`
|
| 50 |
+
`)+r.raw,o.text+=`
|
| 51 |
+
`+r.raw,this.inlineQueue.at(-1).src=o.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let o=1/0,u=e.slice(1),a;this.options.extensions.startBlock.forEach(c=>{a=c.call({lexer:this},u),typeof a=="number"&&a>=0&&(o=Math.min(o,a))}),o<1/0&&o>=0&&(i=e.substring(0,o+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let o=t.at(-1);n&&o?.type==="paragraph"?(o.raw+=(o.raw.endsWith(`
|
| 52 |
+
`)?"":`
|
| 53 |
+
`)+r.raw,o.text+=`
|
| 54 |
+
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let o=t.at(-1);o?.type==="text"?(o.raw+=(o.raw.endsWith(`
|
| 55 |
+
`)?"":`
|
| 56 |
+
`)+r.raw,o.text+=`
|
| 57 |
+
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=o.text):t.push(r);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){this.tokenizer.lexer=this;let n=e,s=null;if(this.tokens.links){let a=Object.keys(this.tokens.links);if(a.length>0)for(;(s=this.tokenizer.rules.inline.reflinkSearch.exec(n))!==null;)a.includes(s[0].slice(s[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,s.index)+"["+"a".repeat(s[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(s=this.tokenizer.rules.inline.anyPunctuation.exec(n))!==null;)n=n.slice(0,s.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);let r;for(;(s=this.tokenizer.rules.inline.blockSkip.exec(n))!==null;)r=s[2]?s[2].length:0,n=n.slice(0,s.index+r)+"["+"a".repeat(s[0].length-r-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let i=!1,o="",u=1/0;for(;e;){if(e.length<u)u=e.length;else{this.infiniteLoopError(e.charCodeAt(0));break}i||(o=""),i=!1;let a;if(this.options.extensions?.inline?.some(p=>(a=p.call({lexer:this},e,t))?(e=e.substring(a.raw.length),t.push(a),!0):!1))continue;if(a=this.tokenizer.escape(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.tag(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.link(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(a.raw.length);let p=t.at(-1);a.type==="text"&&p?.type==="text"?(p.raw+=a.raw,p.text+=a.text):t.push(a);continue}if(a=this.tokenizer.emStrong(e,n,o)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.codespan(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.br(e)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.del(e,n,o)){e=e.substring(a.raw.length),t.push(a);continue}if(a=this.tokenizer.autolink(e)){e=e.substring(a.raw.length),t.push(a);continue}if(!this.state.inLink&&(a=this.tokenizer.url(e))){e=e.substring(a.raw.length),t.push(a);continue}let c=e;if(this.options.extensions?.startInline){let p=1/0,k=e.slice(1),h;this.options.extensions.startInline.forEach(R=>{h=R.call({lexer:this},k),typeof h=="number"&&h>=0&&(p=Math.min(p,h))}),p<1/0&&p>=0&&(c=e.substring(0,p+1))}if(a=this.tokenizer.inlineText(c)){e=e.substring(a.raw.length),a.raw.slice(-1)!=="_"&&(o=a.raw.slice(-1)),i=!0;let p=t.at(-1);p?.type==="text"?(p.raw+=a.raw,p.text+=a.text):t.push(a);continue}if(e){this.infiniteLoopError(e.charCodeAt(0));break}}return t}infiniteLoopError(e){let t="Infinite loop on byte: "+e;if(this.options.silent)console.error(t);else throw new Error(t)}};var y=class{options;parser;constructor(e){this.options=e||T}space(e){return""}code({text:e,lang:t,escaped:n}){let s=(t||"").match(m.notSpaceStart)?.[0],r=e.replace(m.endingNewline,"")+`
|
| 58 |
+
`;return s?'<pre><code class="language-'+O(s)+'">'+(n?r:O(r,!0))+`</code></pre>
|
| 59 |
+
`:"<pre><code>"+(n?r:O(r,!0))+`</code></pre>
|
| 60 |
+
`}blockquote({tokens:e}){return`<blockquote>
|
| 61 |
+
${this.parser.parse(e)}</blockquote>
|
| 62 |
+
`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
|
| 63 |
+
`}hr(e){return`<hr>
|
| 64 |
+
`}list(e){let t=e.ordered,n=e.start,s="";for(let o=0;o<e.items.length;o++){let u=e.items[o];s+=this.listitem(u)}let r=t?"ol":"ul",i=t&&n!==1?' start="'+n+'"':"";return"<"+r+i+`>
|
| 65 |
+
`+s+"</"+r+`>
|
| 66 |
+
`}listitem(e){return`<li>${this.parser.parse(e.tokens)}</li>
|
| 67 |
+
`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox"> '}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
|
| 68 |
+
`}table(e){let t="",n="";for(let r=0;r<e.header.length;r++)n+=this.tablecell(e.header[r]);t+=this.tablerow({text:n});let s="";for(let r=0;r<e.rows.length;r++){let i=e.rows[r];n="";for(let o=0;o<i.length;o++)n+=this.tablecell(i[o]);s+=this.tablerow({text:n})}return s&&(s=`<tbody>${s}</tbody>`),`<table>
|
| 69 |
+
<thead>
|
| 70 |
+
`+t+`</thead>
|
| 71 |
+
`+s+`</table>
|
| 72 |
+
`}tablerow({text:e}){return`<tr>
|
| 73 |
+
${e}</tr>
|
| 74 |
+
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
|
| 75 |
+
`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${O(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let s=this.parser.parseInline(n),r=Y(e);if(r===null)return s;e=r;let i='<a href="'+e+'"';return t&&(i+=' title="'+O(t)+'"'),i+=">"+s+"</a>",i}image({href:e,title:t,text:n,tokens:s}){s&&(n=this.parser.parseInline(s,this.parser.textRenderer));let r=Y(e);if(r===null)return O(n);e=r;let i=`<img src="${e}" alt="${O(n)}"`;return t&&(i+=` title="${O(t)}"`),i+=">",i}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:O(e.text)}};var S=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}checkbox({raw:e}){return e}};var b=class l{options;renderer;textRenderer;constructor(e){this.options=e||T,this.options.renderer=this.options.renderer||new y,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new S}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e){this.renderer.parser=this;let t="";for(let n=0;n<e.length;n++){let s=e[n];if(this.options.extensions?.renderers?.[s.type]){let i=s,o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["space","hr","heading","code","table","blockquote","list","html","def","paragraph","text"].includes(i.type)){t+=o||"";continue}}let r=s;switch(r.type){case"space":{t+=this.renderer.space(r);break}case"hr":{t+=this.renderer.hr(r);break}case"heading":{t+=this.renderer.heading(r);break}case"code":{t+=this.renderer.code(r);break}case"table":{t+=this.renderer.table(r);break}case"blockquote":{t+=this.renderer.blockquote(r);break}case"list":{t+=this.renderer.list(r);break}case"checkbox":{t+=this.renderer.checkbox(r);break}case"html":{t+=this.renderer.html(r);break}case"def":{t+=this.renderer.def(r);break}case"paragraph":{t+=this.renderer.paragraph(r);break}case"text":{t+=this.renderer.text(r);break}default:{let i='Token with "'+r.type+'" type was not found.';if(this.options.silent)return console.error(i),"";throw new Error(i)}}}return t}parseInline(e,t=this.renderer){this.renderer.parser=this;let n="";for(let s=0;s<e.length;s++){let r=e[s];if(this.options.extensions?.renderers?.[r.type]){let o=this.options.extensions.renderers[r.type].call({parser:this},r);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(r.type)){n+=o||"";continue}}let i=r;switch(i.type){case"escape":{n+=t.text(i);break}case"html":{n+=t.html(i);break}case"link":{n+=t.link(i);break}case"image":{n+=t.image(i);break}case"checkbox":{n+=t.checkbox(i);break}case"strong":{n+=t.strong(i);break}case"em":{n+=t.em(i);break}case"codespan":{n+=t.codespan(i);break}case"br":{n+=t.br(i);break}case"del":{n+=t.del(i);break}case"text":{n+=t.text(i);break}default:{let o='Token with "'+i.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var P=class{options;block;constructor(e){this.options=e||T}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens","emStrongMask"]);static passThroughHooksRespectAsync=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}emStrongMask(e){return e}provideLexer(e=this.block){return e?x.lex:x.lexInline}provideParser(e=this.block){return e?b.parse:b.parseInline}};var C=class{defaults=_();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=b;Renderer=y;TextRenderer=S;Lexer=x;Tokenizer=w;Hooks=P;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let s of e)switch(n=n.concat(t.call(this,s)),s.type){case"table":{let r=s;for(let i of r.header)n=n.concat(this.walkTokens(i.tokens,t));for(let i of r.rows)for(let o of i)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let r=s;n=n.concat(this.walkTokens(r.items,t));break}default:{let r=s;this.defaults.extensions?.childTokens?.[r.type]?this.defaults.extensions.childTokens[r.type].forEach(i=>{let o=r[i].flat(1/0);n=n.concat(this.walkTokens(o,t))}):r.tokens&&(n=n.concat(this.walkTokens(r.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let s={...n};if(s.async=this.defaults.async||s.async||!1,n.extensions&&(n.extensions.forEach(r=>{if(!r.name)throw new Error("extension name required");if("renderer"in r){let i=t.renderers[r.name];i?t.renderers[r.name]=function(...o){let u=r.renderer.apply(this,o);return u===!1&&(u=i.apply(this,o)),u}:t.renderers[r.name]=r.renderer}if("tokenizer"in r){if(!r.level||r.level!=="block"&&r.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let i=t[r.level];i?i.unshift(r.tokenizer):t[r.level]=[r.tokenizer],r.start&&(r.level==="block"?t.startBlock?t.startBlock.push(r.start):t.startBlock=[r.start]:r.level==="inline"&&(t.startInline?t.startInline.push(r.start):t.startInline=[r.start]))}"childTokens"in r&&r.childTokens&&(t.childTokens[r.name]=r.childTokens)}),s.extensions=t),n.renderer){let r=this.defaults.renderer||new y(this.defaults);for(let i in n.renderer){if(!(i in r))throw new Error(`renderer '${i}' does not exist`);if(["options","parser"].includes(i))continue;let o=i,u=n.renderer[o],a=r[o];r[o]=(...c)=>{let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p||""}}s.renderer=r}if(n.tokenizer){let r=this.defaults.tokenizer||new w(this.defaults);for(let i in n.tokenizer){if(!(i in r))throw new Error(`tokenizer '${i}' does not exist`);if(["options","rules","lexer"].includes(i))continue;let o=i,u=n.tokenizer[o],a=r[o];r[o]=(...c)=>{let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p}}s.tokenizer=r}if(n.hooks){let r=this.defaults.hooks||new P;for(let i in n.hooks){if(!(i in r))throw new Error(`hook '${i}' does not exist`);if(["options","block"].includes(i))continue;let o=i,u=n.hooks[o],a=r[o];P.passThroughHooks.has(i)?r[o]=c=>{if(this.defaults.async&&P.passThroughHooksRespectAsync.has(i))return(async()=>{let k=await u.call(r,c);return a.call(r,k)})();let p=u.call(r,c);return a.call(r,p)}:r[o]=(...c)=>{if(this.defaults.async)return(async()=>{let k=await u.apply(r,c);return k===!1&&(k=await a.apply(r,c)),k})();let p=u.apply(r,c);return p===!1&&(p=a.apply(r,c)),p}}s.hooks=r}if(n.walkTokens){let r=this.defaults.walkTokens,i=n.walkTokens;s.walkTokens=function(o){let u=[];return u.push(i.call(this,o)),r&&(u=u.concat(r.call(this,o))),u}}this.defaults={...this.defaults,...s}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return x.lex(e,t??this.defaults)}parser(e,t){return b.parse(e,t??this.defaults)}parseMarkdown(e){return(n,s)=>{let r={...s},i={...this.defaults,...r},o=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&r.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===null)return o(new Error("marked(): input parameter is undefined or null"));if(typeof n!="string")return o(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(n)+", string expected"));if(i.hooks&&(i.hooks.options=i,i.hooks.block=e),i.async)return(async()=>{let u=i.hooks?await i.hooks.preprocess(n):n,c=await(i.hooks?await i.hooks.provideLexer(e):e?x.lex:x.lexInline)(u,i),p=i.hooks?await i.hooks.processAllTokens(c):c;i.walkTokens&&await Promise.all(this.walkTokens(p,i.walkTokens));let h=await(i.hooks?await i.hooks.provideParser(e):e?b.parse:b.parseInline)(p,i);return i.hooks?await i.hooks.postprocess(h):h})().catch(o);try{i.hooks&&(n=i.hooks.preprocess(n));let a=(i.hooks?i.hooks.provideLexer(e):e?x.lex:x.lexInline)(n,i);i.hooks&&(a=i.hooks.processAllTokens(a)),i.walkTokens&&this.walkTokens(a,i.walkTokens);let p=(i.hooks?i.hooks.provideParser(e):e?b.parse:b.parseInline)(a,i);return i.hooks&&(p=i.hooks.postprocess(p)),p}catch(u){return o(u)}}}onError(e,t){return n=>{if(n.message+=`
|
| 76 |
+
Please report this to https://github.com/markedjs/marked.`,e){let s="<p>An error occurred:</p><pre>"+O(n.message+"",!0)+"</pre>";return t?Promise.resolve(s):s}if(t)return Promise.reject(n);throw n}}};var M=new C;function g(l,e){return M.parse(l,e)}g.options=g.setOptions=function(l){return M.setOptions(l),g.defaults=M.defaults,Q(g.defaults),g};g.getDefaults=_;g.defaults=T;g.use=function(...l){return M.use(...l),g.defaults=M.defaults,Q(g.defaults),g};g.walkTokens=function(l,e){return M.walkTokens(l,e)};g.parseInline=M.parseInline;g.Parser=b;g.parser=b.parse;g.Renderer=y;g.TextRenderer=S;g.Lexer=x;g.lexer=x.lex;g.Tokenizer=w;g.Hooks=P;g.parse=g;var ht=g.options,kt=g.setOptions,dt=g.use,gt=g.walkTokens,ft=g.parseInline,mt=g,xt=b.parse,bt=x.lex;
|
| 77 |
+
|
| 78 |
+
if(__exports != exports)module.exports = exports;return module.exports}));
|
packages/coding-agent/src/core/extensions/index.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Extension system for lifecycle events and custom tools.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.ts";
|
| 6 |
+
export type { SourceInfo } from "../source-info.ts";
|
| 7 |
+
export {
|
| 8 |
+
createExtensionRuntime,
|
| 9 |
+
discoverAndLoadExtensions,
|
| 10 |
+
loadExtensionFromFactory,
|
| 11 |
+
loadExtensions,
|
| 12 |
+
} from "./loader.ts";
|
| 13 |
+
export type {
|
| 14 |
+
ExtensionErrorListener,
|
| 15 |
+
ForkHandler,
|
| 16 |
+
NavigateTreeHandler,
|
| 17 |
+
NewSessionHandler,
|
| 18 |
+
ShutdownHandler,
|
| 19 |
+
SwitchSessionHandler,
|
| 20 |
+
} from "./runner.ts";
|
| 21 |
+
export { ExtensionRunner } from "./runner.ts";
|
| 22 |
+
export type {
|
| 23 |
+
AfterProviderResponseEvent,
|
| 24 |
+
AgentEndEvent,
|
| 25 |
+
AgentSettledEvent,
|
| 26 |
+
AgentStartEvent,
|
| 27 |
+
// Re-exports
|
| 28 |
+
AgentToolResult,
|
| 29 |
+
AgentToolUpdateCallback,
|
| 30 |
+
AppendEntryHandler,
|
| 31 |
+
// App keybindings (for custom editors)
|
| 32 |
+
AppKeybinding,
|
| 33 |
+
AutocompleteProviderFactory,
|
| 34 |
+
// Events - Tool (ToolCallEvent types)
|
| 35 |
+
BashToolCallEvent,
|
| 36 |
+
BashToolResultEvent,
|
| 37 |
+
BeforeAgentStartEvent,
|
| 38 |
+
BeforeAgentStartEventResult,
|
| 39 |
+
BeforeProviderHeadersEvent,
|
| 40 |
+
BeforeProviderRequestEvent,
|
| 41 |
+
BeforeProviderRequestEventResult,
|
| 42 |
+
BuildSystemPromptOptions,
|
| 43 |
+
// Context
|
| 44 |
+
CompactOptions,
|
| 45 |
+
// Events - Agent
|
| 46 |
+
ContextEvent,
|
| 47 |
+
// Event Results
|
| 48 |
+
ContextEventResult,
|
| 49 |
+
ContextUsage,
|
| 50 |
+
CustomToolCallEvent,
|
| 51 |
+
CustomToolResultEvent,
|
| 52 |
+
EditorFactory,
|
| 53 |
+
EditToolCallEvent,
|
| 54 |
+
EditToolResultEvent,
|
| 55 |
+
// Message and Entry Rendering
|
| 56 |
+
EntryRenderer,
|
| 57 |
+
EntryRenderOptions,
|
| 58 |
+
ExecOptions,
|
| 59 |
+
ExecResult,
|
| 60 |
+
Extension,
|
| 61 |
+
ExtensionActions,
|
| 62 |
+
// API
|
| 63 |
+
ExtensionAPI,
|
| 64 |
+
ExtensionCommandContext,
|
| 65 |
+
ExtensionCommandContextActions,
|
| 66 |
+
ExtensionContext,
|
| 67 |
+
ExtensionContextActions,
|
| 68 |
+
// Errors
|
| 69 |
+
ExtensionError,
|
| 70 |
+
ExtensionEvent,
|
| 71 |
+
ExtensionFactory,
|
| 72 |
+
ExtensionFlag,
|
| 73 |
+
ExtensionHandler,
|
| 74 |
+
ExtensionMode,
|
| 75 |
+
// Runtime
|
| 76 |
+
ExtensionRuntime,
|
| 77 |
+
ExtensionShortcut,
|
| 78 |
+
ExtensionUIContext,
|
| 79 |
+
ExtensionUIDialogOptions,
|
| 80 |
+
ExtensionWidgetOptions,
|
| 81 |
+
FindToolCallEvent,
|
| 82 |
+
FindToolResultEvent,
|
| 83 |
+
GetActiveToolsHandler,
|
| 84 |
+
GetAllToolsHandler,
|
| 85 |
+
GetCommandsHandler,
|
| 86 |
+
GetThinkingLevelHandler,
|
| 87 |
+
GrepToolCallEvent,
|
| 88 |
+
GrepToolResultEvent,
|
| 89 |
+
InlineExtension,
|
| 90 |
+
// Events - Input
|
| 91 |
+
InputEvent,
|
| 92 |
+
InputEventResult,
|
| 93 |
+
InputSource,
|
| 94 |
+
KeybindingsManager,
|
| 95 |
+
LoadExtensionsResult,
|
| 96 |
+
LsToolCallEvent,
|
| 97 |
+
LsToolResultEvent,
|
| 98 |
+
MarkdownTransformContext,
|
| 99 |
+
MarkdownTransformer,
|
| 100 |
+
// Events - Message
|
| 101 |
+
MessageEndEvent,
|
| 102 |
+
MessageRenderer,
|
| 103 |
+
MessageRenderOptions,
|
| 104 |
+
MessageStartEvent,
|
| 105 |
+
MessageUpdateEvent,
|
| 106 |
+
ModelSelectEvent,
|
| 107 |
+
ModelSelectSource,
|
| 108 |
+
PowerShellToolCallEvent,
|
| 109 |
+
PowerShellToolResultEvent,
|
| 110 |
+
ProjectTrustContext,
|
| 111 |
+
ProjectTrustEvent,
|
| 112 |
+
ProjectTrustEventDecision,
|
| 113 |
+
ProjectTrustEventResult,
|
| 114 |
+
ProjectTrustHandler,
|
| 115 |
+
// Provider Registration
|
| 116 |
+
ProviderConfig,
|
| 117 |
+
ProviderModelConfig,
|
| 118 |
+
ReadToolCallEvent,
|
| 119 |
+
ReadToolResultEvent,
|
| 120 |
+
// Commands
|
| 121 |
+
RegisteredCommand,
|
| 122 |
+
RegisteredTool,
|
| 123 |
+
ReplacedSessionContext,
|
| 124 |
+
ResolvedCommand,
|
| 125 |
+
// Events - Resources
|
| 126 |
+
ResourcesDiscoverEvent,
|
| 127 |
+
ResourcesDiscoverResult,
|
| 128 |
+
SendMessageHandler,
|
| 129 |
+
SendUserMessageHandler,
|
| 130 |
+
SessionBeforeCompactEvent,
|
| 131 |
+
SessionBeforeCompactResult,
|
| 132 |
+
SessionBeforeForkEvent,
|
| 133 |
+
SessionBeforeForkResult,
|
| 134 |
+
SessionBeforeSwitchEvent,
|
| 135 |
+
SessionBeforeSwitchResult,
|
| 136 |
+
SessionBeforeTreeEvent,
|
| 137 |
+
SessionBeforeTreeResult,
|
| 138 |
+
SessionCompactEvent,
|
| 139 |
+
SessionCompactFailedEvent,
|
| 140 |
+
SessionEvent,
|
| 141 |
+
SessionInfoChangedEvent,
|
| 142 |
+
SessionShutdownEvent,
|
| 143 |
+
// Events - Session
|
| 144 |
+
SessionStartEvent,
|
| 145 |
+
SessionTreeEvent,
|
| 146 |
+
SetActiveToolsHandler,
|
| 147 |
+
SetLabelHandler,
|
| 148 |
+
SetModelHandler,
|
| 149 |
+
SetThinkingLevelHandler,
|
| 150 |
+
TerminalInputHandler,
|
| 151 |
+
// Events - Tool
|
| 152 |
+
ToolCallEvent,
|
| 153 |
+
ToolCallEventResult,
|
| 154 |
+
// Tools
|
| 155 |
+
ToolDefinition,
|
| 156 |
+
// Events - Tool Execution
|
| 157 |
+
ToolExecutionEndEvent,
|
| 158 |
+
// Tool execution mode
|
| 159 |
+
ToolExecutionMode,
|
| 160 |
+
ToolExecutionStartEvent,
|
| 161 |
+
ToolExecutionUpdateEvent,
|
| 162 |
+
ToolInfo,
|
| 163 |
+
ToolRenderResultOptions,
|
| 164 |
+
ToolResultEvent,
|
| 165 |
+
ToolResultEventResult,
|
| 166 |
+
TreePreparation,
|
| 167 |
+
TurnEndEvent,
|
| 168 |
+
TurnStartEvent,
|
| 169 |
+
UIPromptEndEvent,
|
| 170 |
+
UIPromptKind,
|
| 171 |
+
UIPromptStartEvent,
|
| 172 |
+
// Events - User Bash
|
| 173 |
+
UserBashEvent,
|
| 174 |
+
UserBashEventResult,
|
| 175 |
+
WidgetPlacement,
|
| 176 |
+
WorkingIndicatorOptions,
|
| 177 |
+
WriteToolCallEvent,
|
| 178 |
+
WriteToolResultEvent,
|
| 179 |
+
} from "./types.ts";
|
| 180 |
+
// Type guards
|
| 181 |
+
export {
|
| 182 |
+
defineTool,
|
| 183 |
+
isBashToolResult,
|
| 184 |
+
isEditToolResult,
|
| 185 |
+
isFindToolResult,
|
| 186 |
+
isGrepToolResult,
|
| 187 |
+
isLsToolResult,
|
| 188 |
+
isPowerShellToolResult,
|
| 189 |
+
isReadToolResult,
|
| 190 |
+
isToolCallEventType,
|
| 191 |
+
isWriteToolResult,
|
| 192 |
+
} from "./types.ts";
|
| 193 |
+
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.ts";
|
packages/coding-agent/src/core/extensions/loader.ts
ADDED
|
@@ -0,0 +1,809 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Extension loader - loads TypeScript extension modules using jiti.
|
| 3 |
+
*
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
import * as fs from "node:fs";
|
| 7 |
+
import { createRequire } from "node:module";
|
| 8 |
+
import * as path from "node:path";
|
| 9 |
+
import { fileURLToPath } from "node:url";
|
| 10 |
+
import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
|
| 11 |
+
import type { Provider } from "@earendil-works/pi-ai";
|
| 12 |
+
import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat";
|
| 13 |
+
import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
|
| 14 |
+
import * as _bundledPiAiProviders from "@earendil-works/pi-ai/providers/all";
|
| 15 |
+
import type { KeyId } from "@earendil-works/pi-tui";
|
| 16 |
+
import * as _bundledPiTui from "@earendil-works/pi-tui";
|
| 17 |
+
import { createJiti } from "jiti/static";
|
| 18 |
+
// Static imports of packages that extensions may use.
|
| 19 |
+
// These MUST be static so Bun bundles them into the compiled binary.
|
| 20 |
+
// The virtualModules option then makes them available to extensions.
|
| 21 |
+
import * as _bundledTypebox from "typebox";
|
| 22 |
+
import * as _bundledTypeboxCompile from "typebox/compile";
|
| 23 |
+
import * as _bundledTypeboxValue from "typebox/value";
|
| 24 |
+
import { CONFIG_DIR_NAME, getAgentDir, isBunBinary, isBundledNode } from "../../config.ts";
|
| 25 |
+
// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
|
| 26 |
+
// avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
|
| 27 |
+
import * as _bundledPiCodingAgent from "../../index.ts";
|
| 28 |
+
import { resolvePath } from "../../utils/paths.ts";
|
| 29 |
+
import { createEventBus, type EventBus } from "../event-bus.ts";
|
| 30 |
+
import type { ExecOptions } from "../exec.ts";
|
| 31 |
+
import { execCommand } from "../exec.ts";
|
| 32 |
+
import { readPiManifest } from "../pi-manifest.ts";
|
| 33 |
+
import { createSyntheticSourceInfo } from "../source-info.ts";
|
| 34 |
+
import { time } from "../timings.ts";
|
| 35 |
+
import type {
|
| 36 |
+
EntryRenderer,
|
| 37 |
+
Extension,
|
| 38 |
+
ExtensionAPI,
|
| 39 |
+
ExtensionFactory,
|
| 40 |
+
ExtensionRuntime,
|
| 41 |
+
LoadExtensionsResult,
|
| 42 |
+
MarkdownTransformer,
|
| 43 |
+
MessageRenderer,
|
| 44 |
+
ProviderConfig,
|
| 45 |
+
RegisteredCommand,
|
| 46 |
+
ToolDefinition,
|
| 47 |
+
} from "./types.ts";
|
| 48 |
+
|
| 49 |
+
/** Modules available to extensions via virtualModules (for compiled binaries) */
|
| 50 |
+
const VIRTUAL_MODULES: Record<string, unknown> = {
|
| 51 |
+
typebox: _bundledTypebox,
|
| 52 |
+
"typebox/compile": _bundledTypeboxCompile,
|
| 53 |
+
"typebox/value": _bundledTypeboxValue,
|
| 54 |
+
"@sinclair/typebox": _bundledTypebox,
|
| 55 |
+
"@sinclair/typebox/compile": _bundledTypeboxCompile,
|
| 56 |
+
"@sinclair/typebox/value": _bundledTypeboxValue,
|
| 57 |
+
"@earendil-works/pi-agent-core": _bundledPiAgentCore,
|
| 58 |
+
"@earendil-works/pi-tui": _bundledPiTui,
|
| 59 |
+
// Extensions resolve the pi-ai root to the compat entrypoint (a strict
|
| 60 |
+
// superset of the core entrypoint): existing extensions using the old
|
| 61 |
+
// global API keep working at runtime until compat is removed.
|
| 62 |
+
"@earendil-works/pi-ai": _bundledPiAiCompat,
|
| 63 |
+
"@earendil-works/pi-ai/compat": _bundledPiAiCompat,
|
| 64 |
+
"@earendil-works/pi-ai/oauth": _bundledPiAiOauth,
|
| 65 |
+
"@earendil-works/pi-ai/providers/all": _bundledPiAiProviders,
|
| 66 |
+
"@earendil-works/pi-coding-agent": _bundledPiCodingAgent,
|
| 67 |
+
"@mariozechner/pi-agent-core": _bundledPiAgentCore,
|
| 68 |
+
"@mariozechner/pi-tui": _bundledPiTui,
|
| 69 |
+
"@mariozechner/pi-ai": _bundledPiAiCompat,
|
| 70 |
+
"@mariozechner/pi-ai/compat": _bundledPiAiCompat,
|
| 71 |
+
"@mariozechner/pi-ai/oauth": _bundledPiAiOauth,
|
| 72 |
+
"@mariozechner/pi-ai/providers/all": _bundledPiAiProviders,
|
| 73 |
+
"@mariozechner/pi-coding-agent": _bundledPiCodingAgent,
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const require = createRequire(import.meta.url);
|
| 77 |
+
|
| 78 |
+
const isNodeSeaBinary =
|
| 79 |
+
("sea" in process.features && process.features.sea === true) ||
|
| 80 |
+
process.getBuiltinModule("node:sea")?.isSea() === true;
|
| 81 |
+
const isTypeScriptSourceRuntime = !isBunBinary && path.extname(fileURLToPath(import.meta.url)) === ".ts";
|
| 82 |
+
|
| 83 |
+
/**
|
| 84 |
+
* Get aliases for jiti (used in built Node.js mode).
|
| 85 |
+
* In compiled binary mode, virtualModules is used instead.
|
| 86 |
+
*/
|
| 87 |
+
let _aliases: Record<string, string> | null = null;
|
| 88 |
+
|
| 89 |
+
function getAliases(): Record<string, string> {
|
| 90 |
+
if (_aliases) return _aliases;
|
| 91 |
+
|
| 92 |
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
| 93 |
+
const packageIndex = path.resolve(__dirname, "../..", "index.js");
|
| 94 |
+
|
| 95 |
+
const typeboxEntry = require.resolve("typebox");
|
| 96 |
+
const typeboxCompileEntry = require.resolve("typebox/compile");
|
| 97 |
+
const typeboxValueEntry = require.resolve("typebox/value");
|
| 98 |
+
|
| 99 |
+
const packagesRoot = path.resolve(__dirname, "../../../../");
|
| 100 |
+
const resolveWorkspaceOrImport = (workspaceRelativePath: string, specifier: string): string => {
|
| 101 |
+
const workspacePath = path.join(packagesRoot, workspaceRelativePath);
|
| 102 |
+
if (fs.existsSync(workspacePath)) {
|
| 103 |
+
return workspacePath;
|
| 104 |
+
}
|
| 105 |
+
return fileURLToPath(import.meta.resolve(specifier));
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
const piCodingAgentEntry = packageIndex;
|
| 109 |
+
const piAgentCoreEntry = resolveWorkspaceOrImport("agent/dist/index.js", "@earendil-works/pi-agent-core");
|
| 110 |
+
const piTuiEntry = resolveWorkspaceOrImport("tui/dist/index.js", "@earendil-works/pi-tui");
|
| 111 |
+
// Extensions resolve the pi-ai root to the compat entrypoint (a strict
|
| 112 |
+
// superset of the core entrypoint): existing extensions using the old
|
| 113 |
+
// global API keep working at runtime until compat is removed.
|
| 114 |
+
const piAiCompatEntry = resolveWorkspaceOrImport("ai/dist/compat.js", "@earendil-works/pi-ai/compat");
|
| 115 |
+
const piAiOauthEntry = resolveWorkspaceOrImport("ai/dist/oauth.js", "@earendil-works/pi-ai/oauth");
|
| 116 |
+
const piAiProvidersEntry = resolveWorkspaceOrImport(
|
| 117 |
+
"ai/dist/providers/all.js",
|
| 118 |
+
"@earendil-works/pi-ai/providers/all",
|
| 119 |
+
);
|
| 120 |
+
|
| 121 |
+
_aliases = {
|
| 122 |
+
"@earendil-works/pi-coding-agent": piCodingAgentEntry,
|
| 123 |
+
"@earendil-works/pi-agent-core": piAgentCoreEntry,
|
| 124 |
+
"@earendil-works/pi-tui": piTuiEntry,
|
| 125 |
+
"@earendil-works/pi-ai/providers/all": piAiProvidersEntry,
|
| 126 |
+
"@earendil-works/pi-ai/compat": piAiCompatEntry,
|
| 127 |
+
"@earendil-works/pi-ai/oauth": piAiOauthEntry,
|
| 128 |
+
"@earendil-works/pi-ai": piAiCompatEntry,
|
| 129 |
+
"@mariozechner/pi-coding-agent": piCodingAgentEntry,
|
| 130 |
+
"@mariozechner/pi-agent-core": piAgentCoreEntry,
|
| 131 |
+
"@mariozechner/pi-tui": piTuiEntry,
|
| 132 |
+
"@mariozechner/pi-ai/providers/all": piAiProvidersEntry,
|
| 133 |
+
"@mariozechner/pi-ai/compat": piAiCompatEntry,
|
| 134 |
+
"@mariozechner/pi-ai/oauth": piAiOauthEntry,
|
| 135 |
+
"@mariozechner/pi-ai": piAiCompatEntry,
|
| 136 |
+
typebox: typeboxEntry,
|
| 137 |
+
"typebox/compile": typeboxCompileEntry,
|
| 138 |
+
"typebox/value": typeboxValueEntry,
|
| 139 |
+
"@sinclair/typebox": typeboxEntry,
|
| 140 |
+
"@sinclair/typebox/compile": typeboxCompileEntry,
|
| 141 |
+
"@sinclair/typebox/value": typeboxValueEntry,
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
return _aliases;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
| 148 |
+
|
| 149 |
+
let extensionCacheCwd: string | undefined;
|
| 150 |
+
let extensionCacheGeneration = 0;
|
| 151 |
+
const extensionCache = new Map<string, ExtensionFactory>();
|
| 152 |
+
|
| 153 |
+
interface ExtensionCacheToken {
|
| 154 |
+
cwd: string;
|
| 155 |
+
generation: number;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
export function clearExtensionCache(): void {
|
| 159 |
+
extensionCache.clear();
|
| 160 |
+
extensionCacheCwd = undefined;
|
| 161 |
+
extensionCacheGeneration++;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
function useExtensionCacheCwd(cwd: string): ExtensionCacheToken {
|
| 165 |
+
const resolvedCwd = resolvePath(cwd);
|
| 166 |
+
if (extensionCacheCwd !== undefined && extensionCacheCwd !== resolvedCwd) {
|
| 167 |
+
clearExtensionCache();
|
| 168 |
+
}
|
| 169 |
+
extensionCacheCwd = resolvedCwd;
|
| 170 |
+
return { cwd: resolvedCwd, generation: extensionCacheGeneration };
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/**
|
| 174 |
+
* Create a runtime with throwing stubs for action methods.
|
| 175 |
+
* Runner.bindCore() replaces these with real implementations.
|
| 176 |
+
*/
|
| 177 |
+
export function createExtensionRuntime(): ExtensionRuntime {
|
| 178 |
+
const notInitialized = () => {
|
| 179 |
+
throw new Error("Extension runtime not initialized. Action methods cannot be called during extension loading.");
|
| 180 |
+
};
|
| 181 |
+
const state: { staleMessage?: string } = {};
|
| 182 |
+
const eventBusUnsubscribers = new Set<() => void>();
|
| 183 |
+
const assertActive = () => {
|
| 184 |
+
if (state.staleMessage) {
|
| 185 |
+
throw new Error(state.staleMessage);
|
| 186 |
+
}
|
| 187 |
+
};
|
| 188 |
+
|
| 189 |
+
const runtime: ExtensionRuntime = {
|
| 190 |
+
sendMessage: notInitialized,
|
| 191 |
+
sendUserMessage: notInitialized,
|
| 192 |
+
appendEntry: notInitialized,
|
| 193 |
+
setSessionName: notInitialized,
|
| 194 |
+
getSessionName: notInitialized,
|
| 195 |
+
setLabel: notInitialized,
|
| 196 |
+
getActiveTools: notInitialized,
|
| 197 |
+
getAllTools: notInitialized,
|
| 198 |
+
setActiveTools: notInitialized,
|
| 199 |
+
// registerTool() is valid during extension load; refresh is only needed post-bind.
|
| 200 |
+
refreshTools: () => {},
|
| 201 |
+
getCommands: notInitialized,
|
| 202 |
+
setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
|
| 203 |
+
getThinkingLevel: notInitialized,
|
| 204 |
+
setThinkingLevel: notInitialized,
|
| 205 |
+
flagValues: new Map(),
|
| 206 |
+
pendingProviderRegistrations: [],
|
| 207 |
+
pendingNativeProviderRegistrations: [],
|
| 208 |
+
assertActive,
|
| 209 |
+
invalidate: (message) => {
|
| 210 |
+
if (state.staleMessage) return;
|
| 211 |
+
state.staleMessage =
|
| 212 |
+
message ??
|
| 213 |
+
"This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().";
|
| 214 |
+
for (const unsubscribe of eventBusUnsubscribers) unsubscribe();
|
| 215 |
+
eventBusUnsubscribers.clear();
|
| 216 |
+
},
|
| 217 |
+
trackEventBusSubscription: (unsubscribe) => {
|
| 218 |
+
let active = true;
|
| 219 |
+
const trackedUnsubscribe = () => {
|
| 220 |
+
if (!active) return;
|
| 221 |
+
active = false;
|
| 222 |
+
eventBusUnsubscribers.delete(trackedUnsubscribe);
|
| 223 |
+
unsubscribe();
|
| 224 |
+
};
|
| 225 |
+
eventBusUnsubscribers.add(trackedUnsubscribe);
|
| 226 |
+
return trackedUnsubscribe;
|
| 227 |
+
},
|
| 228 |
+
// Pre-bind: queue registrations so bindCore() can flush them once the
|
| 229 |
+
// model registry is available. bindCore() replaces both with direct calls.
|
| 230 |
+
registerProvider: (name, config, extensionPath = "<unknown>") => {
|
| 231 |
+
runtime.pendingProviderRegistrations.push({ name, config, extensionPath });
|
| 232 |
+
},
|
| 233 |
+
registerNativeProvider: (provider, extensionPath = "<unknown>") => {
|
| 234 |
+
runtime.pendingNativeProviderRegistrations.push({ provider, extensionPath });
|
| 235 |
+
},
|
| 236 |
+
unregisterProvider: (name) => {
|
| 237 |
+
runtime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);
|
| 238 |
+
runtime.pendingNativeProviderRegistrations = runtime.pendingNativeProviderRegistrations.filter(
|
| 239 |
+
(r) => r.provider.id !== name,
|
| 240 |
+
);
|
| 241 |
+
},
|
| 242 |
+
};
|
| 243 |
+
|
| 244 |
+
return runtime;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
/**
|
| 248 |
+
* Create the ExtensionAPI for an extension.
|
| 249 |
+
* Registration methods write to the extension object.
|
| 250 |
+
* Action methods delegate to the shared runtime.
|
| 251 |
+
*/
|
| 252 |
+
function createExtensionAPI(
|
| 253 |
+
extension: Extension,
|
| 254 |
+
runtime: ExtensionRuntime,
|
| 255 |
+
cwd: string,
|
| 256 |
+
eventBus: EventBus,
|
| 257 |
+
): { api: ExtensionAPI; commit: () => void; discard: () => void } {
|
| 258 |
+
const pendingFlagValues = new Map<string, boolean | string>();
|
| 259 |
+
const pendingRuntimeChanges: Array<() => void> = [];
|
| 260 |
+
const loadingUnsubscribers: Array<() => void> = [];
|
| 261 |
+
let state: "loading" | "active" | "failed" = "loading";
|
| 262 |
+
const assertActive = () => {
|
| 263 |
+
if (state === "failed") {
|
| 264 |
+
throw new Error(`Extension "${extension.path}" failed to load and its API is no longer active.`);
|
| 265 |
+
}
|
| 266 |
+
runtime.assertActive();
|
| 267 |
+
};
|
| 268 |
+
const applyRuntimeChange = (change: () => void) => {
|
| 269 |
+
if (state === "loading") pendingRuntimeChanges.push(change);
|
| 270 |
+
else change();
|
| 271 |
+
};
|
| 272 |
+
const clearPending = () => {
|
| 273 |
+
pendingFlagValues.clear();
|
| 274 |
+
pendingRuntimeChanges.length = 0;
|
| 275 |
+
loadingUnsubscribers.length = 0;
|
| 276 |
+
};
|
| 277 |
+
|
| 278 |
+
const api = {
|
| 279 |
+
// Registration methods - write to extension
|
| 280 |
+
on(event: string, handler: HandlerFn): void {
|
| 281 |
+
assertActive();
|
| 282 |
+
const list = extension.handlers.get(event) ?? [];
|
| 283 |
+
list.push(handler);
|
| 284 |
+
extension.handlers.set(event, list);
|
| 285 |
+
},
|
| 286 |
+
|
| 287 |
+
registerTool(tool: ToolDefinition): void {
|
| 288 |
+
assertActive();
|
| 289 |
+
if (typeof tool.parameters !== "object" || tool.parameters === null || Array.isArray(tool.parameters)) {
|
| 290 |
+
throw new Error(
|
| 291 |
+
`Tool "${tool.name}" registered by extension "${extension.path}" must define an object parameter schema.`,
|
| 292 |
+
);
|
| 293 |
+
}
|
| 294 |
+
extension.tools.set(tool.name, {
|
| 295 |
+
definition: tool,
|
| 296 |
+
sourceInfo: extension.sourceInfo,
|
| 297 |
+
});
|
| 298 |
+
runtime.refreshTools();
|
| 299 |
+
},
|
| 300 |
+
|
| 301 |
+
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void {
|
| 302 |
+
assertActive();
|
| 303 |
+
extension.commands.set(name, {
|
| 304 |
+
name,
|
| 305 |
+
sourceInfo: extension.sourceInfo,
|
| 306 |
+
...options,
|
| 307 |
+
});
|
| 308 |
+
},
|
| 309 |
+
|
| 310 |
+
registerShortcut(
|
| 311 |
+
shortcut: KeyId,
|
| 312 |
+
options: {
|
| 313 |
+
description?: string;
|
| 314 |
+
handler: (ctx: import("./types.ts").ExtensionContext) => Promise<void> | void;
|
| 315 |
+
},
|
| 316 |
+
): void {
|
| 317 |
+
assertActive();
|
| 318 |
+
extension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });
|
| 319 |
+
},
|
| 320 |
+
|
| 321 |
+
registerFlag(
|
| 322 |
+
name: string,
|
| 323 |
+
options: { description?: string; type: "boolean" | "string"; default?: boolean | string },
|
| 324 |
+
): void {
|
| 325 |
+
assertActive();
|
| 326 |
+
if (options.default !== undefined && typeof options.default !== options.type) {
|
| 327 |
+
throw new Error(
|
| 328 |
+
`Invalid default for flag "${name}": expected ${options.type}, got ${typeof options.default}`,
|
| 329 |
+
);
|
| 330 |
+
}
|
| 331 |
+
extension.flags.set(name, { name, extensionPath: extension.path, ...options });
|
| 332 |
+
if (options.default !== undefined && !runtime.flagValues.has(name)) {
|
| 333 |
+
if (state === "loading") {
|
| 334 |
+
if (!pendingFlagValues.has(name)) pendingFlagValues.set(name, options.default);
|
| 335 |
+
} else {
|
| 336 |
+
runtime.flagValues.set(name, options.default);
|
| 337 |
+
}
|
| 338 |
+
}
|
| 339 |
+
},
|
| 340 |
+
|
| 341 |
+
registerMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {
|
| 342 |
+
assertActive();
|
| 343 |
+
extension.messageRenderers.set(customType, renderer as MessageRenderer);
|
| 344 |
+
},
|
| 345 |
+
|
| 346 |
+
registerMarkdownTransformer(transformer: MarkdownTransformer): void {
|
| 347 |
+
assertActive();
|
| 348 |
+
extension.markdownTransformer = transformer;
|
| 349 |
+
},
|
| 350 |
+
|
| 351 |
+
registerEntryRenderer<T>(customType: string, renderer: EntryRenderer<T>): void {
|
| 352 |
+
assertActive();
|
| 353 |
+
extension.entryRenderers ??= new Map();
|
| 354 |
+
extension.entryRenderers.set(customType, renderer as EntryRenderer);
|
| 355 |
+
},
|
| 356 |
+
|
| 357 |
+
// Flag access - checks extension registered it, reads from runtime
|
| 358 |
+
getFlag(name: string): boolean | string | undefined {
|
| 359 |
+
assertActive();
|
| 360 |
+
if (!extension.flags.has(name)) return undefined;
|
| 361 |
+
return runtime.flagValues.has(name) ? runtime.flagValues.get(name) : pendingFlagValues.get(name);
|
| 362 |
+
},
|
| 363 |
+
|
| 364 |
+
// Action methods - delegate to shared runtime
|
| 365 |
+
sendMessage(message, options): void {
|
| 366 |
+
assertActive();
|
| 367 |
+
runtime.sendMessage(message, options);
|
| 368 |
+
},
|
| 369 |
+
|
| 370 |
+
sendUserMessage(content, options): void {
|
| 371 |
+
assertActive();
|
| 372 |
+
runtime.sendUserMessage(content, options);
|
| 373 |
+
},
|
| 374 |
+
|
| 375 |
+
appendEntry(customType: string, data?: unknown): void {
|
| 376 |
+
assertActive();
|
| 377 |
+
runtime.appendEntry(customType, data);
|
| 378 |
+
},
|
| 379 |
+
|
| 380 |
+
setSessionName(name: string): void {
|
| 381 |
+
assertActive();
|
| 382 |
+
runtime.setSessionName(name);
|
| 383 |
+
},
|
| 384 |
+
|
| 385 |
+
getSessionName(): string | undefined {
|
| 386 |
+
assertActive();
|
| 387 |
+
return runtime.getSessionName();
|
| 388 |
+
},
|
| 389 |
+
|
| 390 |
+
setLabel(entryId: string, label: string | undefined): void {
|
| 391 |
+
assertActive();
|
| 392 |
+
runtime.setLabel(entryId, label);
|
| 393 |
+
},
|
| 394 |
+
|
| 395 |
+
exec(command: string, args: string[], options?: ExecOptions) {
|
| 396 |
+
assertActive();
|
| 397 |
+
return execCommand(command, args, options?.cwd ?? cwd, options);
|
| 398 |
+
},
|
| 399 |
+
|
| 400 |
+
getActiveTools(): string[] {
|
| 401 |
+
assertActive();
|
| 402 |
+
return runtime.getActiveTools();
|
| 403 |
+
},
|
| 404 |
+
|
| 405 |
+
getAllTools() {
|
| 406 |
+
assertActive();
|
| 407 |
+
return runtime.getAllTools();
|
| 408 |
+
},
|
| 409 |
+
|
| 410 |
+
setActiveTools(toolNames: string[]): void {
|
| 411 |
+
assertActive();
|
| 412 |
+
runtime.setActiveTools(toolNames);
|
| 413 |
+
},
|
| 414 |
+
|
| 415 |
+
getCommands() {
|
| 416 |
+
assertActive();
|
| 417 |
+
return runtime.getCommands();
|
| 418 |
+
},
|
| 419 |
+
|
| 420 |
+
setModel(model) {
|
| 421 |
+
assertActive();
|
| 422 |
+
return runtime.setModel(model);
|
| 423 |
+
},
|
| 424 |
+
|
| 425 |
+
getThinkingLevel() {
|
| 426 |
+
assertActive();
|
| 427 |
+
return runtime.getThinkingLevel();
|
| 428 |
+
},
|
| 429 |
+
|
| 430 |
+
setThinkingLevel(level) {
|
| 431 |
+
assertActive();
|
| 432 |
+
runtime.setThinkingLevel(level);
|
| 433 |
+
},
|
| 434 |
+
|
| 435 |
+
registerProvider(providerOrName: Provider | string, config?: ProviderConfig) {
|
| 436 |
+
assertActive();
|
| 437 |
+
if (typeof providerOrName === "string") {
|
| 438 |
+
if (!config) throw new Error("Provider config is required when registering by name");
|
| 439 |
+
applyRuntimeChange(() => runtime.registerProvider(providerOrName, config, extension.path));
|
| 440 |
+
return;
|
| 441 |
+
}
|
| 442 |
+
applyRuntimeChange(() => runtime.registerNativeProvider(providerOrName, extension.path));
|
| 443 |
+
},
|
| 444 |
+
|
| 445 |
+
unregisterProvider(name: string) {
|
| 446 |
+
assertActive();
|
| 447 |
+
applyRuntimeChange(() => runtime.unregisterProvider(name, extension.path));
|
| 448 |
+
},
|
| 449 |
+
|
| 450 |
+
events: {
|
| 451 |
+
emit(channel, data) {
|
| 452 |
+
assertActive();
|
| 453 |
+
eventBus.emit(channel, data);
|
| 454 |
+
},
|
| 455 |
+
on(channel, handler) {
|
| 456 |
+
assertActive();
|
| 457 |
+
const unsubscribe = runtime.trackEventBusSubscription(eventBus.on(channel, handler));
|
| 458 |
+
if (state === "loading") loadingUnsubscribers.push(unsubscribe);
|
| 459 |
+
return unsubscribe;
|
| 460 |
+
},
|
| 461 |
+
},
|
| 462 |
+
} as ExtensionAPI;
|
| 463 |
+
|
| 464 |
+
return {
|
| 465 |
+
api,
|
| 466 |
+
commit: () => {
|
| 467 |
+
if (state !== "loading") return;
|
| 468 |
+
runtime.assertActive();
|
| 469 |
+
for (const [name, value] of pendingFlagValues) {
|
| 470 |
+
if (!runtime.flagValues.has(name)) runtime.flagValues.set(name, value);
|
| 471 |
+
}
|
| 472 |
+
for (const apply of pendingRuntimeChanges) apply();
|
| 473 |
+
state = "active";
|
| 474 |
+
clearPending();
|
| 475 |
+
},
|
| 476 |
+
discard: () => {
|
| 477 |
+
if (state !== "loading") return;
|
| 478 |
+
state = "failed";
|
| 479 |
+
for (const unsubscribe of loadingUnsubscribers) unsubscribe();
|
| 480 |
+
clearPending();
|
| 481 |
+
},
|
| 482 |
+
};
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
function isCurrentCacheToken(cacheToken: ExtensionCacheToken | undefined): cacheToken is ExtensionCacheToken {
|
| 486 |
+
return (
|
| 487 |
+
cacheToken !== undefined &&
|
| 488 |
+
extensionCacheCwd === cacheToken.cwd &&
|
| 489 |
+
extensionCacheGeneration === cacheToken.generation
|
| 490 |
+
);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
async function loadExtensionModule(extensionPath: string, cacheToken?: ExtensionCacheToken) {
|
| 494 |
+
if (isCurrentCacheToken(cacheToken)) {
|
| 495 |
+
const cachedFactory = extensionCache.get(extensionPath);
|
| 496 |
+
if (cachedFactory) {
|
| 497 |
+
return cachedFactory;
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
const jiti = createJiti(import.meta.url, {
|
| 502 |
+
moduleCache: false,
|
| 503 |
+
// Compiled binaries and the bundled Node distribution use embedded modules.
|
| 504 |
+
// Source TypeScript reuses host modules and root tsconfig paths. Unbundled
|
| 505 |
+
// Node builds use dist aliases.
|
| 506 |
+
...(isBunBinary || isNodeSeaBinary || isBundledNode
|
| 507 |
+
? { virtualModules: VIRTUAL_MODULES, tryNative: false }
|
| 508 |
+
: isTypeScriptSourceRuntime
|
| 509 |
+
? { virtualModules: VIRTUAL_MODULES, tsconfigPaths: true }
|
| 510 |
+
: { alias: getAliases() }),
|
| 511 |
+
});
|
| 512 |
+
|
| 513 |
+
const module = await jiti.import(extensionPath, { default: true });
|
| 514 |
+
const factory = module as ExtensionFactory;
|
| 515 |
+
if (typeof factory !== "function") {
|
| 516 |
+
return undefined;
|
| 517 |
+
}
|
| 518 |
+
if (isCurrentCacheToken(cacheToken)) {
|
| 519 |
+
extensionCache.set(extensionPath, factory);
|
| 520 |
+
}
|
| 521 |
+
return factory;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/**
|
| 525 |
+
* Create an Extension object with empty collections.
|
| 526 |
+
*/
|
| 527 |
+
function createExtension(extensionPath: string, resolvedPath: string): Extension {
|
| 528 |
+
const source =
|
| 529 |
+
extensionPath.startsWith("<") && extensionPath.endsWith(">")
|
| 530 |
+
? extensionPath.slice(1, -1).split(":")[0] || "temporary"
|
| 531 |
+
: "local";
|
| 532 |
+
const baseDir = extensionPath.startsWith("<") ? undefined : path.dirname(resolvedPath);
|
| 533 |
+
|
| 534 |
+
return {
|
| 535 |
+
path: extensionPath,
|
| 536 |
+
resolvedPath,
|
| 537 |
+
sourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),
|
| 538 |
+
handlers: new Map(),
|
| 539 |
+
tools: new Map(),
|
| 540 |
+
messageRenderers: new Map(),
|
| 541 |
+
entryRenderers: new Map(),
|
| 542 |
+
commands: new Map(),
|
| 543 |
+
flags: new Map(),
|
| 544 |
+
shortcuts: new Map(),
|
| 545 |
+
};
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
async function initializeExtension(
|
| 549 |
+
factory: ExtensionFactory,
|
| 550 |
+
extensionPath: string,
|
| 551 |
+
resolvedPath: string,
|
| 552 |
+
cwd: string,
|
| 553 |
+
eventBus: EventBus,
|
| 554 |
+
runtime: ExtensionRuntime,
|
| 555 |
+
): Promise<Extension> {
|
| 556 |
+
const extension = createExtension(extensionPath, resolvedPath);
|
| 557 |
+
const load = createExtensionAPI(extension, runtime, cwd, eventBus);
|
| 558 |
+
try {
|
| 559 |
+
await factory(load.api);
|
| 560 |
+
load.commit();
|
| 561 |
+
} catch (error) {
|
| 562 |
+
load.discard();
|
| 563 |
+
throw error;
|
| 564 |
+
}
|
| 565 |
+
time(`${extensionPath} factory`, "extensions");
|
| 566 |
+
return extension;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
async function loadExtension(
|
| 570 |
+
extensionPath: string,
|
| 571 |
+
cwd: string,
|
| 572 |
+
eventBus: EventBus,
|
| 573 |
+
runtime: ExtensionRuntime,
|
| 574 |
+
cacheToken?: ExtensionCacheToken,
|
| 575 |
+
): Promise<{ extension: Extension | null; error: string | null }> {
|
| 576 |
+
const resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });
|
| 577 |
+
|
| 578 |
+
try {
|
| 579 |
+
const factory = await loadExtensionModule(resolvedPath, cacheToken);
|
| 580 |
+
time(`${extensionPath} module import`, "extensions");
|
| 581 |
+
if (!factory) {
|
| 582 |
+
return { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
const extension = await initializeExtension(factory, extensionPath, resolvedPath, cwd, eventBus, runtime);
|
| 586 |
+
|
| 587 |
+
return { extension, error: null };
|
| 588 |
+
} catch (err) {
|
| 589 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 590 |
+
return { extension: null, error: `Failed to load extension: ${message}` };
|
| 591 |
+
}
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
/**
|
| 595 |
+
* Create an Extension from an inline factory function.
|
| 596 |
+
*/
|
| 597 |
+
export async function loadExtensionFromFactory(
|
| 598 |
+
factory: ExtensionFactory,
|
| 599 |
+
cwd: string,
|
| 600 |
+
eventBus: EventBus,
|
| 601 |
+
runtime: ExtensionRuntime,
|
| 602 |
+
extensionPath = "<inline>",
|
| 603 |
+
): Promise<Extension> {
|
| 604 |
+
const resolvedCwd = resolvePath(cwd);
|
| 605 |
+
return initializeExtension(factory, extensionPath, extensionPath, resolvedCwd, eventBus, runtime);
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
/**
|
| 609 |
+
* Load extensions from paths.
|
| 610 |
+
*/
|
| 611 |
+
async function loadExtensionsInternal(
|
| 612 |
+
paths: string[],
|
| 613 |
+
cwd: string,
|
| 614 |
+
eventBus?: EventBus,
|
| 615 |
+
runtime?: ExtensionRuntime,
|
| 616 |
+
useCache = false,
|
| 617 |
+
): Promise<LoadExtensionsResult> {
|
| 618 |
+
const extensions: Extension[] = [];
|
| 619 |
+
const errors: Array<{ path: string; error: string }> = [];
|
| 620 |
+
const cacheToken = useCache ? useExtensionCacheCwd(cwd) : undefined;
|
| 621 |
+
const resolvedCwd = cacheToken?.cwd ?? resolvePath(cwd);
|
| 622 |
+
const resolvedEventBus = eventBus ?? createEventBus();
|
| 623 |
+
const resolvedRuntime = runtime ?? createExtensionRuntime();
|
| 624 |
+
|
| 625 |
+
for (const extPath of paths) {
|
| 626 |
+
const { extension, error } = await loadExtension(
|
| 627 |
+
extPath,
|
| 628 |
+
resolvedCwd,
|
| 629 |
+
resolvedEventBus,
|
| 630 |
+
resolvedRuntime,
|
| 631 |
+
cacheToken,
|
| 632 |
+
);
|
| 633 |
+
|
| 634 |
+
if (error) {
|
| 635 |
+
errors.push({ path: extPath, error });
|
| 636 |
+
continue;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
if (extension) {
|
| 640 |
+
extensions.push(extension);
|
| 641 |
+
}
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
return {
|
| 645 |
+
extensions,
|
| 646 |
+
errors,
|
| 647 |
+
runtime: resolvedRuntime,
|
| 648 |
+
};
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
export async function loadExtensions(
|
| 652 |
+
paths: string[],
|
| 653 |
+
cwd: string,
|
| 654 |
+
eventBus?: EventBus,
|
| 655 |
+
runtime?: ExtensionRuntime,
|
| 656 |
+
): Promise<LoadExtensionsResult> {
|
| 657 |
+
return loadExtensionsInternal(paths, cwd, eventBus, runtime);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
export async function loadExtensionsCached(
|
| 661 |
+
paths: string[],
|
| 662 |
+
cwd: string,
|
| 663 |
+
eventBus?: EventBus,
|
| 664 |
+
runtime?: ExtensionRuntime,
|
| 665 |
+
): Promise<LoadExtensionsResult> {
|
| 666 |
+
return loadExtensionsInternal(paths, cwd, eventBus, runtime, true);
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
function isExtensionFile(name: string): boolean {
|
| 670 |
+
return name.endsWith(".ts") || name.endsWith(".js");
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
/**
|
| 674 |
+
* Resolve extension entry points from a directory.
|
| 675 |
+
*
|
| 676 |
+
* Checks for:
|
| 677 |
+
* 1. package.json with "pi.extensions" field -> returns declared paths
|
| 678 |
+
* 2. index.ts or index.js -> returns the index file
|
| 679 |
+
*
|
| 680 |
+
* Returns resolved paths or null if no entry points found.
|
| 681 |
+
*/
|
| 682 |
+
function resolveExtensionEntries(dir: string): string[] | null {
|
| 683 |
+
// Check for package.json with "pi" field first
|
| 684 |
+
const packageJsonPath = path.join(dir, "package.json");
|
| 685 |
+
if (fs.existsSync(packageJsonPath)) {
|
| 686 |
+
const manifest = readPiManifest(packageJsonPath);
|
| 687 |
+
if (manifest?.extensions?.length) {
|
| 688 |
+
const entries: string[] = [];
|
| 689 |
+
for (const extPath of manifest.extensions) {
|
| 690 |
+
const resolvedExtPath = path.resolve(dir, extPath);
|
| 691 |
+
if (fs.existsSync(resolvedExtPath)) {
|
| 692 |
+
entries.push(resolvedExtPath);
|
| 693 |
+
}
|
| 694 |
+
}
|
| 695 |
+
if (entries.length > 0) {
|
| 696 |
+
return entries;
|
| 697 |
+
}
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
// Check for index.ts or index.js
|
| 702 |
+
const indexTs = path.join(dir, "index.ts");
|
| 703 |
+
const indexJs = path.join(dir, "index.js");
|
| 704 |
+
if (fs.existsSync(indexTs)) {
|
| 705 |
+
return [indexTs];
|
| 706 |
+
}
|
| 707 |
+
if (fs.existsSync(indexJs)) {
|
| 708 |
+
return [indexJs];
|
| 709 |
+
}
|
| 710 |
+
|
| 711 |
+
return null;
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
/**
|
| 715 |
+
* Discover extensions in a directory.
|
| 716 |
+
*
|
| 717 |
+
* Discovery rules:
|
| 718 |
+
* 1. Direct files: `extensions/*.ts` or `*.js` → load
|
| 719 |
+
* 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load
|
| 720 |
+
* 3. Subdirectory with package.json: `extensions/* /package.json` with "pi" field → load what it declares
|
| 721 |
+
*
|
| 722 |
+
* No recursion beyond one level. Complex packages must use package.json manifest.
|
| 723 |
+
*/
|
| 724 |
+
function discoverExtensionsInDir(dir: string): string[] {
|
| 725 |
+
if (!fs.existsSync(dir)) {
|
| 726 |
+
return [];
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
const discovered: string[] = [];
|
| 730 |
+
|
| 731 |
+
try {
|
| 732 |
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
| 733 |
+
|
| 734 |
+
for (const entry of entries) {
|
| 735 |
+
const entryPath = path.join(dir, entry.name);
|
| 736 |
+
|
| 737 |
+
// 1. Direct files: *.ts or *.js
|
| 738 |
+
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {
|
| 739 |
+
discovered.push(entryPath);
|
| 740 |
+
continue;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
// 2 & 3. Subdirectories
|
| 744 |
+
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
| 745 |
+
const entries = resolveExtensionEntries(entryPath);
|
| 746 |
+
if (entries) {
|
| 747 |
+
discovered.push(...entries);
|
| 748 |
+
}
|
| 749 |
+
}
|
| 750 |
+
}
|
| 751 |
+
} catch {
|
| 752 |
+
return [];
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
return discovered;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
/**
|
| 759 |
+
* Discover and load extensions from standard locations.
|
| 760 |
+
*/
|
| 761 |
+
export async function discoverAndLoadExtensions(
|
| 762 |
+
configuredPaths: string[],
|
| 763 |
+
cwd: string,
|
| 764 |
+
agentDir: string = getAgentDir(),
|
| 765 |
+
eventBus?: EventBus,
|
| 766 |
+
): Promise<LoadExtensionsResult> {
|
| 767 |
+
const resolvedCwd = resolvePath(cwd);
|
| 768 |
+
const resolvedAgentDir = resolvePath(agentDir);
|
| 769 |
+
const allPaths: string[] = [];
|
| 770 |
+
const seen = new Set<string>();
|
| 771 |
+
|
| 772 |
+
const addPaths = (paths: string[]) => {
|
| 773 |
+
for (const p of paths) {
|
| 774 |
+
const resolved = path.resolve(p);
|
| 775 |
+
if (!seen.has(resolved)) {
|
| 776 |
+
seen.add(resolved);
|
| 777 |
+
allPaths.push(p);
|
| 778 |
+
}
|
| 779 |
+
}
|
| 780 |
+
};
|
| 781 |
+
|
| 782 |
+
// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/
|
| 783 |
+
const localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, "extensions");
|
| 784 |
+
addPaths(discoverExtensionsInDir(localExtDir));
|
| 785 |
+
|
| 786 |
+
// 2. Global extensions: agentDir/extensions/
|
| 787 |
+
const globalExtDir = path.join(resolvedAgentDir, "extensions");
|
| 788 |
+
addPaths(discoverExtensionsInDir(globalExtDir));
|
| 789 |
+
|
| 790 |
+
// 3. Explicitly configured paths
|
| 791 |
+
for (const p of configuredPaths) {
|
| 792 |
+
const resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });
|
| 793 |
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
| 794 |
+
// Check for package.json with pi manifest or index.ts
|
| 795 |
+
const entries = resolveExtensionEntries(resolved);
|
| 796 |
+
if (entries) {
|
| 797 |
+
addPaths(entries);
|
| 798 |
+
continue;
|
| 799 |
+
}
|
| 800 |
+
// No explicit entries - discover individual files in directory
|
| 801 |
+
addPaths(discoverExtensionsInDir(resolved));
|
| 802 |
+
continue;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
addPaths([resolved]);
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
return loadExtensions(allPaths, resolvedCwd, eventBus);
|
| 809 |
+
}
|
packages/coding-agent/src/core/extensions/runner.ts
ADDED
|
@@ -0,0 +1,1286 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Extension runner - executes extensions and manages their lifecycle.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
| 6 |
+
import type { ImageContent, Model, Provider, ProviderHeaders } from "@earendil-works/pi-ai";
|
| 7 |
+
import type { KeyId } from "@earendil-works/pi-tui";
|
| 8 |
+
import { type Theme, theme } from "../../modes/interactive/theme/theme.ts";
|
| 9 |
+
import type { ResourceDiagnostic } from "../diagnostics.ts";
|
| 10 |
+
import type { KeybindingsConfig } from "../keybindings.ts";
|
| 11 |
+
import type { ModelRegistry } from "../model-registry.ts";
|
| 12 |
+
import type { ScopedModel } from "../model-resolver.ts";
|
| 13 |
+
import type { SessionManager } from "../session-manager.ts";
|
| 14 |
+
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
| 15 |
+
import type {
|
| 16 |
+
BeforeAgentStartEvent,
|
| 17 |
+
BeforeAgentStartEventResult,
|
| 18 |
+
BeforeProviderHeadersEvent,
|
| 19 |
+
BeforeProviderRequestEvent,
|
| 20 |
+
CompactOptions,
|
| 21 |
+
ContextEvent,
|
| 22 |
+
ContextEventResult,
|
| 23 |
+
ContextUsage,
|
| 24 |
+
EntryRenderer,
|
| 25 |
+
Extension,
|
| 26 |
+
ExtensionActions,
|
| 27 |
+
ExtensionCommandContext,
|
| 28 |
+
ExtensionCommandContextActions,
|
| 29 |
+
ExtensionContext,
|
| 30 |
+
ExtensionContextActions,
|
| 31 |
+
ExtensionError,
|
| 32 |
+
ExtensionEvent,
|
| 33 |
+
ExtensionFlag,
|
| 34 |
+
ExtensionMode,
|
| 35 |
+
ExtensionRuntime,
|
| 36 |
+
ExtensionShortcut,
|
| 37 |
+
ExtensionUIContext,
|
| 38 |
+
InputEvent,
|
| 39 |
+
InputEventResult,
|
| 40 |
+
InputSource,
|
| 41 |
+
LoadExtensionsResult,
|
| 42 |
+
MarkdownTransformer,
|
| 43 |
+
MessageEndEvent,
|
| 44 |
+
MessageEndEventResult,
|
| 45 |
+
MessageRenderer,
|
| 46 |
+
ProjectTrustContext,
|
| 47 |
+
ProjectTrustEvent,
|
| 48 |
+
ProjectTrustEventResult,
|
| 49 |
+
ProviderConfig,
|
| 50 |
+
RegisteredCommand,
|
| 51 |
+
RegisteredTool,
|
| 52 |
+
ReplacedSessionContext,
|
| 53 |
+
ResolvedCommand,
|
| 54 |
+
ResourcesDiscoverEvent,
|
| 55 |
+
ResourcesDiscoverResult,
|
| 56 |
+
SessionBeforeCompactResult,
|
| 57 |
+
SessionBeforeForkResult,
|
| 58 |
+
SessionBeforeSwitchResult,
|
| 59 |
+
SessionBeforeTreeResult,
|
| 60 |
+
SessionShutdownEvent,
|
| 61 |
+
ToolCallEvent,
|
| 62 |
+
ToolCallEventResult,
|
| 63 |
+
ToolResultEvent,
|
| 64 |
+
ToolResultEventResult,
|
| 65 |
+
UIPromptKind,
|
| 66 |
+
UserBashEvent,
|
| 67 |
+
UserBashEventResult,
|
| 68 |
+
} from "./types.ts";
|
| 69 |
+
|
| 70 |
+
// Extension shortcuts compete with canonical keybinding ids from keybindings.json.
|
| 71 |
+
// Only editor-global shortcuts are reserved here. Picker-specific bindings are not.
|
| 72 |
+
const RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS = [
|
| 73 |
+
"app.interrupt",
|
| 74 |
+
"app.clear",
|
| 75 |
+
"app.exit",
|
| 76 |
+
"app.suspend",
|
| 77 |
+
"app.thinking.cycle",
|
| 78 |
+
"app.model.cycleForward",
|
| 79 |
+
"app.model.cycleBackward",
|
| 80 |
+
"app.model.select",
|
| 81 |
+
"app.tools.expand",
|
| 82 |
+
"app.thinking.toggle",
|
| 83 |
+
"app.editor.external",
|
| 84 |
+
"app.message.copy",
|
| 85 |
+
"app.message.followUp",
|
| 86 |
+
"tui.input.submit",
|
| 87 |
+
"tui.select.confirm",
|
| 88 |
+
"tui.select.cancel",
|
| 89 |
+
"tui.input.copy",
|
| 90 |
+
"tui.editor.deleteToLineEnd",
|
| 91 |
+
] as const;
|
| 92 |
+
|
| 93 |
+
type BuiltInKeyBindings = Partial<Record<KeyId, { keybinding: string; restrictOverride: boolean }>>;
|
| 94 |
+
|
| 95 |
+
const buildBuiltinKeybindings = (resolvedKeybindings: KeybindingsConfig): BuiltInKeyBindings => {
|
| 96 |
+
const builtinKeybindings = {} as BuiltInKeyBindings;
|
| 97 |
+
for (const [keybinding, keys] of Object.entries(resolvedKeybindings)) {
|
| 98 |
+
if (keys === undefined) continue;
|
| 99 |
+
const keyList = Array.isArray(keys) ? keys : [keys];
|
| 100 |
+
const restrictOverride = (RESERVED_KEYBINDINGS_FOR_EXTENSION_CONFLICTS as readonly string[]).includes(keybinding);
|
| 101 |
+
for (const key of keyList) {
|
| 102 |
+
const normalizedKey = key.toLowerCase() as KeyId;
|
| 103 |
+
// If multiple actions bind the same key, the reserved action wins so extensions
|
| 104 |
+
// remain blocked by reserved shortcuts regardless of iteration order.
|
| 105 |
+
const existing = builtinKeybindings[normalizedKey];
|
| 106 |
+
if (existing?.restrictOverride && !restrictOverride) continue;
|
| 107 |
+
builtinKeybindings[normalizedKey] = {
|
| 108 |
+
keybinding,
|
| 109 |
+
restrictOverride,
|
| 110 |
+
};
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
return builtinKeybindings;
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
/** Combined result from all before_agent_start handlers */
|
| 117 |
+
interface BeforeAgentStartCombinedResult {
|
| 118 |
+
messages?: NonNullable<BeforeAgentStartEventResult["message"]>[];
|
| 119 |
+
systemPrompt?: string;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Events handled by the generic emit() method.
|
| 124 |
+
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
| 125 |
+
*/
|
| 126 |
+
type RunnerEmitEvent = Exclude<
|
| 127 |
+
ExtensionEvent,
|
| 128 |
+
| ToolCallEvent
|
| 129 |
+
| ProjectTrustEvent
|
| 130 |
+
| ToolResultEvent
|
| 131 |
+
| UserBashEvent
|
| 132 |
+
| ContextEvent
|
| 133 |
+
| BeforeProviderRequestEvent
|
| 134 |
+
| BeforeProviderHeadersEvent
|
| 135 |
+
| BeforeAgentStartEvent
|
| 136 |
+
| MessageEndEvent
|
| 137 |
+
| ResourcesDiscoverEvent
|
| 138 |
+
| InputEvent
|
| 139 |
+
>;
|
| 140 |
+
|
| 141 |
+
type SessionBeforeEvent = Extract<
|
| 142 |
+
RunnerEmitEvent,
|
| 143 |
+
{ type: "session_before_switch" | "session_before_fork" | "session_before_compact" | "session_before_tree" }
|
| 144 |
+
>;
|
| 145 |
+
|
| 146 |
+
type SessionBeforeEventResult =
|
| 147 |
+
| SessionBeforeSwitchResult
|
| 148 |
+
| SessionBeforeForkResult
|
| 149 |
+
| SessionBeforeCompactResult
|
| 150 |
+
| SessionBeforeTreeResult;
|
| 151 |
+
|
| 152 |
+
type RunnerEmitResult<TEvent extends RunnerEmitEvent> = TEvent extends { type: "session_before_switch" }
|
| 153 |
+
? SessionBeforeSwitchResult | undefined
|
| 154 |
+
: TEvent extends { type: "session_before_fork" }
|
| 155 |
+
? SessionBeforeForkResult | undefined
|
| 156 |
+
: TEvent extends { type: "session_before_compact" }
|
| 157 |
+
? SessionBeforeCompactResult | undefined
|
| 158 |
+
: TEvent extends { type: "session_before_tree" }
|
| 159 |
+
? SessionBeforeTreeResult | undefined
|
| 160 |
+
: undefined;
|
| 161 |
+
|
| 162 |
+
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
| 163 |
+
|
| 164 |
+
export type NewSessionHandler = (options?: {
|
| 165 |
+
parentSession?: string;
|
| 166 |
+
setup?: (sessionManager: SessionManager) => Promise<void>;
|
| 167 |
+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
| 168 |
+
}) => Promise<{ cancelled: boolean }>;
|
| 169 |
+
|
| 170 |
+
export type ForkHandler = (
|
| 171 |
+
entryId: string,
|
| 172 |
+
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 173 |
+
) => Promise<{ cancelled: boolean }>;
|
| 174 |
+
|
| 175 |
+
export type NavigateTreeHandler = (
|
| 176 |
+
targetId: string,
|
| 177 |
+
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
| 178 |
+
) => Promise<{ cancelled: boolean }>;
|
| 179 |
+
|
| 180 |
+
export type SwitchSessionHandler = (
|
| 181 |
+
sessionPath: string,
|
| 182 |
+
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 183 |
+
) => Promise<{ cancelled: boolean }>;
|
| 184 |
+
|
| 185 |
+
export type ReloadHandler = () => Promise<void>;
|
| 186 |
+
|
| 187 |
+
export type ShutdownHandler = () => void;
|
| 188 |
+
|
| 189 |
+
/**
|
| 190 |
+
* Helper function to emit session_shutdown event to extensions.
|
| 191 |
+
* Returns true if the event was emitted, false if there were no handlers.
|
| 192 |
+
*/
|
| 193 |
+
export async function emitSessionShutdownEvent(
|
| 194 |
+
extensionRunner: ExtensionRunner,
|
| 195 |
+
event: SessionShutdownEvent,
|
| 196 |
+
): Promise<boolean> {
|
| 197 |
+
if (extensionRunner.hasHandlers("session_shutdown")) {
|
| 198 |
+
await extensionRunner.emit(event);
|
| 199 |
+
return true;
|
| 200 |
+
}
|
| 201 |
+
return false;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
export async function emitProjectTrustEvent(
|
| 205 |
+
extensionsResult: LoadExtensionsResult,
|
| 206 |
+
event: ProjectTrustEvent,
|
| 207 |
+
ctx: ProjectTrustContext,
|
| 208 |
+
): Promise<{ result?: ProjectTrustEventResult; errors: ExtensionError[] }> {
|
| 209 |
+
const errors: ExtensionError[] = [];
|
| 210 |
+
for (const ext of extensionsResult.extensions) {
|
| 211 |
+
// A single extension may register multiple handlers for the same event.
|
| 212 |
+
// The first project_trust handler that returns yes/no wins; undecided falls through.
|
| 213 |
+
const handlers = ext.handlers.get("project_trust");
|
| 214 |
+
if (!handlers || handlers.length === 0) continue;
|
| 215 |
+
|
| 216 |
+
for (const handler of handlers) {
|
| 217 |
+
try {
|
| 218 |
+
const handlerResult = (await handler(event, ctx)) as ProjectTrustEventResult;
|
| 219 |
+
if (handlerResult.trusted === "undecided") {
|
| 220 |
+
continue;
|
| 221 |
+
}
|
| 222 |
+
return { result: handlerResult, errors };
|
| 223 |
+
} catch (error) {
|
| 224 |
+
errors.push({
|
| 225 |
+
extensionPath: ext.path,
|
| 226 |
+
event: event.type,
|
| 227 |
+
error: error instanceof Error ? error.message : String(error),
|
| 228 |
+
stack: error instanceof Error ? error.stack : undefined,
|
| 229 |
+
});
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
return { errors };
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
const noOpUIContext: ExtensionUIContext = {
|
| 237 |
+
select: async () => undefined,
|
| 238 |
+
confirm: async () => false,
|
| 239 |
+
input: async () => undefined,
|
| 240 |
+
notify: () => {},
|
| 241 |
+
onTerminalInput: () => () => {},
|
| 242 |
+
setStatus: () => {},
|
| 243 |
+
setWorkingMessage: () => {},
|
| 244 |
+
setWorkingVisible: () => {},
|
| 245 |
+
setWorkingIndicator: () => {},
|
| 246 |
+
setHiddenThinkingLabel: () => {},
|
| 247 |
+
setWidget: () => {},
|
| 248 |
+
setFooter: () => {},
|
| 249 |
+
setHeader: () => {},
|
| 250 |
+
setTitle: () => {},
|
| 251 |
+
custom: async () => undefined as never,
|
| 252 |
+
pasteToEditor: () => {},
|
| 253 |
+
setEditorText: () => {},
|
| 254 |
+
getEditorText: () => "",
|
| 255 |
+
editor: async () => undefined,
|
| 256 |
+
addAutocompleteProvider: () => {},
|
| 257 |
+
setEditorComponent: () => {},
|
| 258 |
+
getEditorComponent: () => undefined,
|
| 259 |
+
get theme() {
|
| 260 |
+
return theme;
|
| 261 |
+
},
|
| 262 |
+
getAllThemes: () => [],
|
| 263 |
+
getTheme: () => undefined,
|
| 264 |
+
setTheme: (_theme: string | Theme) => ({ success: false, error: "UI not available" }),
|
| 265 |
+
getToolsExpanded: () => false,
|
| 266 |
+
setToolsExpanded: () => {},
|
| 267 |
+
};
|
| 268 |
+
|
| 269 |
+
export class ExtensionRunner {
|
| 270 |
+
private extensions: Extension[];
|
| 271 |
+
private runtime: ExtensionRuntime;
|
| 272 |
+
private uiContext: ExtensionUIContext;
|
| 273 |
+
private mode: ExtensionMode = "print";
|
| 274 |
+
private cwd: string;
|
| 275 |
+
private sessionManager: SessionManager;
|
| 276 |
+
private modelRegistry: ModelRegistry;
|
| 277 |
+
private errorListeners: Set<ExtensionErrorListener> = new Set();
|
| 278 |
+
private getModel: () => Model<any> | undefined = () => undefined;
|
| 279 |
+
private getScopedModels: () => readonly ScopedModel[] = () => [];
|
| 280 |
+
private isIdleFn: () => boolean = () => true;
|
| 281 |
+
private isProjectTrustedFn: () => boolean = () => true;
|
| 282 |
+
private getSignalFn: () => AbortSignal | undefined = () => undefined;
|
| 283 |
+
private waitForIdleFn: () => Promise<void> = async () => {};
|
| 284 |
+
private abortFn: () => void = () => {};
|
| 285 |
+
private hasPendingMessagesFn: () => boolean = () => false;
|
| 286 |
+
private getContextUsageFn: () => ContextUsage | undefined = () => undefined;
|
| 287 |
+
private compactFn: (options?: CompactOptions) => void = () => {};
|
| 288 |
+
private getSystemPromptFn: () => string = () => "";
|
| 289 |
+
private getSystemPromptOptionsFn: () => BuildSystemPromptOptions = () => ({ cwd: this.cwd });
|
| 290 |
+
private newSessionHandler: NewSessionHandler = async () => ({ cancelled: false });
|
| 291 |
+
private forkHandler: ForkHandler = async () => ({ cancelled: false });
|
| 292 |
+
private navigateTreeHandler: NavigateTreeHandler = async () => ({ cancelled: false });
|
| 293 |
+
private switchSessionHandler: SwitchSessionHandler = async () => ({ cancelled: false });
|
| 294 |
+
private reloadHandler: ReloadHandler = async () => {};
|
| 295 |
+
private shutdownHandler: ShutdownHandler = () => {};
|
| 296 |
+
private shortcutDiagnostics: ResourceDiagnostic[] = [];
|
| 297 |
+
private commandDiagnostics: ResourceDiagnostic[] = [];
|
| 298 |
+
private staleMessage: string | undefined;
|
| 299 |
+
private uiPromptDepth = 0;
|
| 300 |
+
private activeUIPrompt: { kind: UIPromptKind; title?: string } | undefined;
|
| 301 |
+
|
| 302 |
+
constructor(
|
| 303 |
+
extensions: Extension[],
|
| 304 |
+
runtime: ExtensionRuntime,
|
| 305 |
+
cwd: string,
|
| 306 |
+
sessionManager: SessionManager,
|
| 307 |
+
modelRegistry: ModelRegistry,
|
| 308 |
+
) {
|
| 309 |
+
this.extensions = extensions;
|
| 310 |
+
this.runtime = runtime;
|
| 311 |
+
this.uiContext = noOpUIContext;
|
| 312 |
+
this.cwd = cwd;
|
| 313 |
+
this.sessionManager = sessionManager;
|
| 314 |
+
this.modelRegistry = modelRegistry;
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
bindCore(
|
| 318 |
+
actions: ExtensionActions,
|
| 319 |
+
contextActions: ExtensionContextActions,
|
| 320 |
+
providerActions?: {
|
| 321 |
+
registerProvider?: (name: string, config: ProviderConfig) => void;
|
| 322 |
+
registerNativeProvider?: (provider: Provider) => void;
|
| 323 |
+
unregisterProvider?: (name: string) => void;
|
| 324 |
+
},
|
| 325 |
+
): void {
|
| 326 |
+
// Copy actions into the shared runtime (all extension APIs reference this)
|
| 327 |
+
this.runtime.sendMessage = actions.sendMessage;
|
| 328 |
+
this.runtime.sendUserMessage = actions.sendUserMessage;
|
| 329 |
+
this.runtime.appendEntry = actions.appendEntry;
|
| 330 |
+
this.runtime.setSessionName = actions.setSessionName;
|
| 331 |
+
this.runtime.getSessionName = actions.getSessionName;
|
| 332 |
+
this.runtime.setLabel = actions.setLabel;
|
| 333 |
+
this.runtime.getActiveTools = actions.getActiveTools;
|
| 334 |
+
this.runtime.getAllTools = actions.getAllTools;
|
| 335 |
+
this.runtime.setActiveTools = actions.setActiveTools;
|
| 336 |
+
this.runtime.refreshTools = actions.refreshTools;
|
| 337 |
+
this.runtime.getCommands = actions.getCommands;
|
| 338 |
+
this.runtime.setModel = actions.setModel;
|
| 339 |
+
this.runtime.getThinkingLevel = actions.getThinkingLevel;
|
| 340 |
+
this.runtime.setThinkingLevel = actions.setThinkingLevel;
|
| 341 |
+
|
| 342 |
+
// Context actions (required)
|
| 343 |
+
this.getModel = contextActions.getModel;
|
| 344 |
+
this.getScopedModels = contextActions.getScopedModels;
|
| 345 |
+
this.isIdleFn = contextActions.isIdle;
|
| 346 |
+
this.isProjectTrustedFn = contextActions.isProjectTrusted;
|
| 347 |
+
this.getSignalFn = contextActions.getSignal;
|
| 348 |
+
this.abortFn = contextActions.abort;
|
| 349 |
+
this.hasPendingMessagesFn = contextActions.hasPendingMessages;
|
| 350 |
+
this.shutdownHandler = contextActions.shutdown;
|
| 351 |
+
this.getContextUsageFn = contextActions.getContextUsage;
|
| 352 |
+
this.compactFn = contextActions.compact;
|
| 353 |
+
this.getSystemPromptFn = contextActions.getSystemPrompt;
|
| 354 |
+
this.getSystemPromptOptionsFn = contextActions.getSystemPromptOptions ?? (() => ({ cwd: this.cwd }));
|
| 355 |
+
|
| 356 |
+
// Flush provider registrations queued during extension loading
|
| 357 |
+
for (const { name, config, extensionPath } of this.runtime.pendingProviderRegistrations) {
|
| 358 |
+
try {
|
| 359 |
+
if (providerActions?.registerProvider) {
|
| 360 |
+
providerActions.registerProvider(name, config);
|
| 361 |
+
} else {
|
| 362 |
+
this.modelRegistry.registerProvider(name, config);
|
| 363 |
+
}
|
| 364 |
+
} catch (err) {
|
| 365 |
+
this.emitError({
|
| 366 |
+
extensionPath,
|
| 367 |
+
event: "register_provider",
|
| 368 |
+
error: err instanceof Error ? err.message : String(err),
|
| 369 |
+
stack: err instanceof Error ? err.stack : undefined,
|
| 370 |
+
});
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
this.runtime.pendingProviderRegistrations = [];
|
| 374 |
+
for (const { provider, extensionPath } of this.runtime.pendingNativeProviderRegistrations) {
|
| 375 |
+
try {
|
| 376 |
+
if (providerActions?.registerNativeProvider) {
|
| 377 |
+
providerActions.registerNativeProvider(provider);
|
| 378 |
+
} else {
|
| 379 |
+
this.modelRegistry.registerProvider(provider);
|
| 380 |
+
}
|
| 381 |
+
} catch (err) {
|
| 382 |
+
this.emitError({
|
| 383 |
+
extensionPath,
|
| 384 |
+
event: "register_provider",
|
| 385 |
+
error: err instanceof Error ? err.message : String(err),
|
| 386 |
+
stack: err instanceof Error ? err.stack : undefined,
|
| 387 |
+
});
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
this.runtime.pendingNativeProviderRegistrations = [];
|
| 391 |
+
|
| 392 |
+
// From this point on, provider registration/unregistration takes effect immediately
|
| 393 |
+
// without requiring a /reload.
|
| 394 |
+
this.runtime.registerProvider = (name, config) => {
|
| 395 |
+
if (providerActions?.registerProvider) {
|
| 396 |
+
providerActions.registerProvider(name, config);
|
| 397 |
+
return;
|
| 398 |
+
}
|
| 399 |
+
this.modelRegistry.registerProvider(name, config);
|
| 400 |
+
};
|
| 401 |
+
this.runtime.registerNativeProvider = (provider) => {
|
| 402 |
+
if (providerActions?.registerNativeProvider) {
|
| 403 |
+
providerActions.registerNativeProvider(provider);
|
| 404 |
+
return;
|
| 405 |
+
}
|
| 406 |
+
this.modelRegistry.registerProvider(provider);
|
| 407 |
+
};
|
| 408 |
+
this.runtime.unregisterProvider = (name) => {
|
| 409 |
+
if (providerActions?.unregisterProvider) {
|
| 410 |
+
providerActions.unregisterProvider(name);
|
| 411 |
+
return;
|
| 412 |
+
}
|
| 413 |
+
this.modelRegistry.unregisterProvider(name);
|
| 414 |
+
};
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
bindCommandContext(actions?: ExtensionCommandContextActions): void {
|
| 418 |
+
if (actions) {
|
| 419 |
+
this.waitForIdleFn = actions.waitForIdle;
|
| 420 |
+
this.newSessionHandler = actions.newSession;
|
| 421 |
+
this.forkHandler = actions.fork;
|
| 422 |
+
this.navigateTreeHandler = actions.navigateTree;
|
| 423 |
+
this.switchSessionHandler = actions.switchSession;
|
| 424 |
+
this.reloadHandler = actions.reload;
|
| 425 |
+
return;
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
this.waitForIdleFn = async () => {};
|
| 429 |
+
this.newSessionHandler = async () => ({ cancelled: false });
|
| 430 |
+
this.forkHandler = async () => ({ cancelled: false });
|
| 431 |
+
this.navigateTreeHandler = async () => ({ cancelled: false });
|
| 432 |
+
this.switchSessionHandler = async () => ({ cancelled: false });
|
| 433 |
+
this.reloadHandler = async () => {};
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
setUIContext(uiContext?: ExtensionUIContext, mode: ExtensionMode = "print"): void {
|
| 437 |
+
this.uiContext = uiContext ? this.wrapUIPromptContext(uiContext) : noOpUIContext;
|
| 438 |
+
this.mode = mode;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
private wrapUIPromptContext(ui: ExtensionUIContext): ExtensionUIContext {
|
| 442 |
+
return {
|
| 443 |
+
...ui,
|
| 444 |
+
select: (title, options, opts) => this.withUIPrompt("select", title, () => ui.select(title, options, opts)),
|
| 445 |
+
confirm: (title, message, opts) => this.withUIPrompt("confirm", title, () => ui.confirm(title, message, opts)),
|
| 446 |
+
input: (title, placeholder, opts) =>
|
| 447 |
+
this.withUIPrompt("input", title, () => ui.input(title, placeholder, opts)),
|
| 448 |
+
editor: (title, prefill) => this.withUIPrompt("editor", title, () => ui.editor(title, prefill)),
|
| 449 |
+
custom: (factory, options) => this.withUIPrompt("custom", undefined, () => ui.custom(factory, options)),
|
| 450 |
+
};
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
private withUIPrompt<T>(kind: UIPromptKind, title: string | undefined, run: () => Promise<T>): Promise<T> {
|
| 454 |
+
const outerPrompt = this.uiPromptDepth++ === 0;
|
| 455 |
+
if (outerPrompt) {
|
| 456 |
+
this.activeUIPrompt = { kind, title };
|
| 457 |
+
this.emitUIPromptEvent({ type: "ui_prompt_start", reason: "ui_prompt", kind, ...(title ? { title } : {}) });
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
const finish = () => {
|
| 461 |
+
if (--this.uiPromptDepth > 0) return;
|
| 462 |
+
this.uiPromptDepth = 0;
|
| 463 |
+
|
| 464 |
+
const prompt = this.activeUIPrompt ?? { kind, title };
|
| 465 |
+
this.activeUIPrompt = undefined;
|
| 466 |
+
this.emitUIPromptEvent({
|
| 467 |
+
type: "ui_prompt_end",
|
| 468 |
+
reason: "ui_prompt",
|
| 469 |
+
kind: prompt.kind,
|
| 470 |
+
...(prompt.title ? { title: prompt.title } : {}),
|
| 471 |
+
});
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
try {
|
| 475 |
+
return run().finally(finish);
|
| 476 |
+
} catch (err) {
|
| 477 |
+
finish();
|
| 478 |
+
throw err;
|
| 479 |
+
}
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
private emitUIPromptEvent(event: Extract<RunnerEmitEvent, { type: "ui_prompt_start" | "ui_prompt_end" }>): void {
|
| 483 |
+
queueMicrotask(() => {
|
| 484 |
+
void this.emit(event);
|
| 485 |
+
});
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
getUIContext(): ExtensionUIContext {
|
| 489 |
+
return this.uiContext;
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
hasUI(): boolean {
|
| 493 |
+
return this.uiContext !== noOpUIContext;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
getExtensionPaths(): string[] {
|
| 497 |
+
return this.extensions.map((e) => e.path);
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
/** Get all registered tools from all extensions (first registration per name wins). */
|
| 501 |
+
getAllRegisteredTools(): RegisteredTool[] {
|
| 502 |
+
const toolsByName = new Map<string, RegisteredTool>();
|
| 503 |
+
for (const ext of this.extensions) {
|
| 504 |
+
for (const tool of ext.tools.values()) {
|
| 505 |
+
if (!toolsByName.has(tool.definition.name)) {
|
| 506 |
+
toolsByName.set(tool.definition.name, tool);
|
| 507 |
+
}
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
return Array.from(toolsByName.values());
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
/** Get a tool definition by name. Returns undefined if not found. */
|
| 514 |
+
getToolDefinition(toolName: string): RegisteredTool["definition"] | undefined {
|
| 515 |
+
for (const ext of this.extensions) {
|
| 516 |
+
const tool = ext.tools.get(toolName);
|
| 517 |
+
if (tool) {
|
| 518 |
+
return tool.definition;
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
return undefined;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
getFlags(): Map<string, ExtensionFlag> {
|
| 525 |
+
const allFlags = new Map<string, ExtensionFlag>();
|
| 526 |
+
for (const ext of this.extensions) {
|
| 527 |
+
for (const [name, flag] of ext.flags) {
|
| 528 |
+
if (!allFlags.has(name)) {
|
| 529 |
+
allFlags.set(name, flag);
|
| 530 |
+
}
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
return allFlags;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
setFlagValue(name: string, value: boolean | string): void {
|
| 537 |
+
this.runtime.flagValues.set(name, value);
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
getFlagValues(): Map<string, boolean | string> {
|
| 541 |
+
return new Map(this.runtime.flagValues);
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
getShortcuts(resolvedKeybindings: KeybindingsConfig): Map<KeyId, ExtensionShortcut> {
|
| 545 |
+
this.shortcutDiagnostics = [];
|
| 546 |
+
const builtinKeybindings = buildBuiltinKeybindings(resolvedKeybindings);
|
| 547 |
+
const extensionShortcuts = new Map<KeyId, ExtensionShortcut>();
|
| 548 |
+
|
| 549 |
+
const addDiagnostic = (message: string, extensionPath: string) => {
|
| 550 |
+
this.shortcutDiagnostics.push({ type: "warning", message, path: extensionPath });
|
| 551 |
+
if (!this.hasUI()) {
|
| 552 |
+
console.warn(message);
|
| 553 |
+
}
|
| 554 |
+
};
|
| 555 |
+
|
| 556 |
+
for (const ext of this.extensions) {
|
| 557 |
+
for (const [key, shortcut] of ext.shortcuts) {
|
| 558 |
+
const normalizedKey = key.toLowerCase() as KeyId;
|
| 559 |
+
|
| 560 |
+
const builtInKeybinding = builtinKeybindings[normalizedKey];
|
| 561 |
+
if (builtInKeybinding?.restrictOverride === true) {
|
| 562 |
+
addDiagnostic(
|
| 563 |
+
`Extension shortcut '${key}' from ${shortcut.extensionPath} conflicts with built-in shortcut. Skipping.`,
|
| 564 |
+
shortcut.extensionPath,
|
| 565 |
+
);
|
| 566 |
+
continue;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
if (builtInKeybinding?.restrictOverride === false) {
|
| 570 |
+
addDiagnostic(
|
| 571 |
+
`Extension shortcut conflict: '${key}' is built-in shortcut for ${builtInKeybinding.keybinding} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
|
| 572 |
+
shortcut.extensionPath,
|
| 573 |
+
);
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
const existingExtensionShortcut = extensionShortcuts.get(normalizedKey);
|
| 577 |
+
if (existingExtensionShortcut) {
|
| 578 |
+
addDiagnostic(
|
| 579 |
+
`Extension shortcut conflict: '${key}' registered by both ${existingExtensionShortcut.extensionPath} and ${shortcut.extensionPath}. Using ${shortcut.extensionPath}.`,
|
| 580 |
+
shortcut.extensionPath,
|
| 581 |
+
);
|
| 582 |
+
}
|
| 583 |
+
extensionShortcuts.set(normalizedKey, shortcut);
|
| 584 |
+
}
|
| 585 |
+
}
|
| 586 |
+
return extensionShortcuts;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
getShortcutDiagnostics(): ResourceDiagnostic[] {
|
| 590 |
+
return this.shortcutDiagnostics;
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
invalidate(
|
| 594 |
+
message = "This extension ctx is stale after session replacement or reload. Do not use a captured pi or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().",
|
| 595 |
+
): void {
|
| 596 |
+
if (!this.staleMessage) {
|
| 597 |
+
this.staleMessage = message;
|
| 598 |
+
this.runtime.invalidate(message);
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
private assertActive(): void {
|
| 603 |
+
if (this.staleMessage) {
|
| 604 |
+
throw new Error(this.staleMessage);
|
| 605 |
+
}
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
onError(listener: ExtensionErrorListener): () => void {
|
| 609 |
+
this.errorListeners.add(listener);
|
| 610 |
+
return () => this.errorListeners.delete(listener);
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
emitError(error: ExtensionError): void {
|
| 614 |
+
for (const listener of this.errorListeners) {
|
| 615 |
+
listener(error);
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
hasHandlers(eventType: string): boolean {
|
| 620 |
+
for (const ext of this.extensions) {
|
| 621 |
+
const handlers = ext.handlers.get(eventType);
|
| 622 |
+
if (handlers && handlers.length > 0) {
|
| 623 |
+
return true;
|
| 624 |
+
}
|
| 625 |
+
}
|
| 626 |
+
return false;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
getMessageRenderer(customType: string): MessageRenderer | undefined {
|
| 630 |
+
for (const ext of this.extensions) {
|
| 631 |
+
const renderer = ext.messageRenderers.get(customType);
|
| 632 |
+
if (renderer) {
|
| 633 |
+
return renderer;
|
| 634 |
+
}
|
| 635 |
+
}
|
| 636 |
+
return undefined;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
getMarkdownTransformers(): MarkdownTransformer[] {
|
| 640 |
+
return this.extensions.flatMap((ext) => (ext.markdownTransformer ? [ext.markdownTransformer] : []));
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
getEntryRenderer(customType: string): EntryRenderer | undefined {
|
| 644 |
+
for (const ext of this.extensions) {
|
| 645 |
+
const renderer = ext.entryRenderers?.get(customType);
|
| 646 |
+
if (renderer) {
|
| 647 |
+
return renderer;
|
| 648 |
+
}
|
| 649 |
+
}
|
| 650 |
+
return undefined;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
private resolveRegisteredCommands(): ResolvedCommand[] {
|
| 654 |
+
const commands: RegisteredCommand[] = [];
|
| 655 |
+
const counts = new Map<string, number>();
|
| 656 |
+
|
| 657 |
+
for (const ext of this.extensions) {
|
| 658 |
+
for (const command of ext.commands.values()) {
|
| 659 |
+
commands.push(command);
|
| 660 |
+
counts.set(command.name, (counts.get(command.name) ?? 0) + 1);
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
const seen = new Map<string, number>();
|
| 665 |
+
const takenInvocationNames = new Set<string>();
|
| 666 |
+
|
| 667 |
+
return commands.map((command) => {
|
| 668 |
+
const occurrence = (seen.get(command.name) ?? 0) + 1;
|
| 669 |
+
seen.set(command.name, occurrence);
|
| 670 |
+
|
| 671 |
+
let invocationName = (counts.get(command.name) ?? 0) > 1 ? `${command.name}:${occurrence}` : command.name;
|
| 672 |
+
|
| 673 |
+
if (takenInvocationNames.has(invocationName)) {
|
| 674 |
+
let suffix = occurrence;
|
| 675 |
+
do {
|
| 676 |
+
suffix++;
|
| 677 |
+
invocationName = `${command.name}:${suffix}`;
|
| 678 |
+
} while (takenInvocationNames.has(invocationName));
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
takenInvocationNames.add(invocationName);
|
| 682 |
+
return {
|
| 683 |
+
...command,
|
| 684 |
+
invocationName,
|
| 685 |
+
};
|
| 686 |
+
});
|
| 687 |
+
}
|
| 688 |
+
|
| 689 |
+
getModelRegistry(): ModelRegistry {
|
| 690 |
+
return this.modelRegistry;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
getRegisteredCommands(): ResolvedCommand[] {
|
| 694 |
+
this.commandDiagnostics = [];
|
| 695 |
+
return this.resolveRegisteredCommands();
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
getCommandDiagnostics(): ResourceDiagnostic[] {
|
| 699 |
+
return this.commandDiagnostics;
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
getCommand(name: string): ResolvedCommand | undefined {
|
| 703 |
+
return this.resolveRegisteredCommands().find((command) => command.invocationName === name);
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
/**
|
| 707 |
+
* Request a graceful shutdown. Called by extension tools and event handlers.
|
| 708 |
+
* The actual shutdown behavior is provided by the mode via bindExtensions().
|
| 709 |
+
*/
|
| 710 |
+
shutdown(): void {
|
| 711 |
+
this.shutdownHandler();
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
getActiveTools(): string[] {
|
| 715 |
+
this.assertActive();
|
| 716 |
+
return this.runtime.getActiveTools();
|
| 717 |
+
}
|
| 718 |
+
|
| 719 |
+
/**
|
| 720 |
+
* Create an ExtensionContext for use in event handlers and tool execution.
|
| 721 |
+
* Context values are resolved at call time, so changes via bindCore/bindUI are reflected.
|
| 722 |
+
*/
|
| 723 |
+
createContext(): ExtensionContext {
|
| 724 |
+
const runner = this;
|
| 725 |
+
const getModel = this.getModel;
|
| 726 |
+
const getScopedModels = this.getScopedModels;
|
| 727 |
+
return {
|
| 728 |
+
get ui() {
|
| 729 |
+
runner.assertActive();
|
| 730 |
+
return runner.uiContext;
|
| 731 |
+
},
|
| 732 |
+
get mode() {
|
| 733 |
+
runner.assertActive();
|
| 734 |
+
return runner.mode;
|
| 735 |
+
},
|
| 736 |
+
get hasUI() {
|
| 737 |
+
runner.assertActive();
|
| 738 |
+
return runner.hasUI();
|
| 739 |
+
},
|
| 740 |
+
get cwd() {
|
| 741 |
+
runner.assertActive();
|
| 742 |
+
return runner.cwd;
|
| 743 |
+
},
|
| 744 |
+
get sessionManager() {
|
| 745 |
+
runner.assertActive();
|
| 746 |
+
return runner.sessionManager;
|
| 747 |
+
},
|
| 748 |
+
get modelRegistry() {
|
| 749 |
+
runner.assertActive();
|
| 750 |
+
return runner.modelRegistry;
|
| 751 |
+
},
|
| 752 |
+
get model() {
|
| 753 |
+
runner.assertActive();
|
| 754 |
+
return getModel();
|
| 755 |
+
},
|
| 756 |
+
get scopedModels() {
|
| 757 |
+
runner.assertActive();
|
| 758 |
+
return getScopedModels();
|
| 759 |
+
},
|
| 760 |
+
get thinkingLevel() {
|
| 761 |
+
runner.assertActive();
|
| 762 |
+
return runner.runtime.getThinkingLevel();
|
| 763 |
+
},
|
| 764 |
+
isIdle: () => {
|
| 765 |
+
runner.assertActive();
|
| 766 |
+
return runner.isIdleFn();
|
| 767 |
+
},
|
| 768 |
+
isProjectTrusted: () => {
|
| 769 |
+
runner.assertActive();
|
| 770 |
+
return runner.isProjectTrustedFn();
|
| 771 |
+
},
|
| 772 |
+
get signal() {
|
| 773 |
+
runner.assertActive();
|
| 774 |
+
return runner.getSignalFn();
|
| 775 |
+
},
|
| 776 |
+
abort: () => {
|
| 777 |
+
runner.assertActive();
|
| 778 |
+
runner.abortFn();
|
| 779 |
+
},
|
| 780 |
+
hasPendingMessages: () => {
|
| 781 |
+
runner.assertActive();
|
| 782 |
+
return runner.hasPendingMessagesFn();
|
| 783 |
+
},
|
| 784 |
+
shutdown: () => {
|
| 785 |
+
runner.assertActive();
|
| 786 |
+
runner.shutdownHandler();
|
| 787 |
+
},
|
| 788 |
+
getContextUsage: () => {
|
| 789 |
+
runner.assertActive();
|
| 790 |
+
return runner.getContextUsageFn();
|
| 791 |
+
},
|
| 792 |
+
compact: (options) => {
|
| 793 |
+
runner.assertActive();
|
| 794 |
+
runner.compactFn(options);
|
| 795 |
+
},
|
| 796 |
+
getSystemPrompt: () => {
|
| 797 |
+
runner.assertActive();
|
| 798 |
+
return runner.getSystemPromptFn();
|
| 799 |
+
},
|
| 800 |
+
};
|
| 801 |
+
}
|
| 802 |
+
|
| 803 |
+
createCommandContext(): ExtensionCommandContext {
|
| 804 |
+
// Use property descriptors instead of object spread so the guarded getters from
|
| 805 |
+
// createContext() stay lazy. A spread would eagerly read them once and freeze the
|
| 806 |
+
// old values into the returned object, bypassing stale-instance checks.
|
| 807 |
+
const context = Object.defineProperties(
|
| 808 |
+
{},
|
| 809 |
+
Object.getOwnPropertyDescriptors(this.createContext()),
|
| 810 |
+
) as ExtensionCommandContext;
|
| 811 |
+
context.getSystemPromptOptions = () => {
|
| 812 |
+
this.assertActive();
|
| 813 |
+
return this.getSystemPromptOptionsFn();
|
| 814 |
+
};
|
| 815 |
+
context.waitForIdle = () => {
|
| 816 |
+
this.assertActive();
|
| 817 |
+
return this.waitForIdleFn();
|
| 818 |
+
};
|
| 819 |
+
context.newSession = (options) => {
|
| 820 |
+
this.assertActive();
|
| 821 |
+
return this.newSessionHandler(options);
|
| 822 |
+
};
|
| 823 |
+
context.fork = (entryId, options) => {
|
| 824 |
+
this.assertActive();
|
| 825 |
+
return this.forkHandler(entryId, options);
|
| 826 |
+
};
|
| 827 |
+
context.navigateTree = (targetId, options) => {
|
| 828 |
+
this.assertActive();
|
| 829 |
+
return this.navigateTreeHandler(targetId, options);
|
| 830 |
+
};
|
| 831 |
+
context.switchSession = (sessionPath, options) => {
|
| 832 |
+
this.assertActive();
|
| 833 |
+
return this.switchSessionHandler(sessionPath, options);
|
| 834 |
+
};
|
| 835 |
+
context.reload = () => {
|
| 836 |
+
this.assertActive();
|
| 837 |
+
return this.reloadHandler();
|
| 838 |
+
};
|
| 839 |
+
return context;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
private isSessionBeforeEvent(event: RunnerEmitEvent): event is SessionBeforeEvent {
|
| 843 |
+
return (
|
| 844 |
+
event.type === "session_before_switch" ||
|
| 845 |
+
event.type === "session_before_fork" ||
|
| 846 |
+
event.type === "session_before_compact" ||
|
| 847 |
+
event.type === "session_before_tree"
|
| 848 |
+
);
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
| 852 |
+
const ctx = this.createContext();
|
| 853 |
+
let result: SessionBeforeEventResult | undefined;
|
| 854 |
+
|
| 855 |
+
for (const ext of this.extensions) {
|
| 856 |
+
const handlers = ext.handlers.get(event.type);
|
| 857 |
+
if (!handlers || handlers.length === 0) continue;
|
| 858 |
+
|
| 859 |
+
for (const handler of handlers) {
|
| 860 |
+
try {
|
| 861 |
+
const handlerResult = await handler(event, ctx);
|
| 862 |
+
|
| 863 |
+
if (this.isSessionBeforeEvent(event) && handlerResult) {
|
| 864 |
+
result = handlerResult as SessionBeforeEventResult;
|
| 865 |
+
if (result.cancel) {
|
| 866 |
+
return result as RunnerEmitResult<TEvent>;
|
| 867 |
+
}
|
| 868 |
+
}
|
| 869 |
+
} catch (err) {
|
| 870 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 871 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 872 |
+
this.emitError({
|
| 873 |
+
extensionPath: ext.path,
|
| 874 |
+
event: event.type,
|
| 875 |
+
error: message,
|
| 876 |
+
stack,
|
| 877 |
+
});
|
| 878 |
+
}
|
| 879 |
+
}
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
return result as RunnerEmitResult<TEvent>;
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
async emitMessageEnd(event: MessageEndEvent): Promise<AgentMessage | undefined> {
|
| 886 |
+
const ctx = this.createContext();
|
| 887 |
+
let currentMessage = event.message;
|
| 888 |
+
let modified = false;
|
| 889 |
+
|
| 890 |
+
for (const ext of this.extensions) {
|
| 891 |
+
const handlers = ext.handlers.get("message_end");
|
| 892 |
+
if (!handlers || handlers.length === 0) continue;
|
| 893 |
+
|
| 894 |
+
for (const handler of handlers) {
|
| 895 |
+
try {
|
| 896 |
+
const currentEvent: MessageEndEvent = { ...event, message: currentMessage };
|
| 897 |
+
const handlerResult = (await handler(currentEvent, ctx)) as MessageEndEventResult | undefined;
|
| 898 |
+
if (!handlerResult?.message) continue;
|
| 899 |
+
|
| 900 |
+
if (handlerResult.message.role !== currentMessage.role) {
|
| 901 |
+
this.emitError({
|
| 902 |
+
extensionPath: ext.path,
|
| 903 |
+
event: "message_end",
|
| 904 |
+
error: "message_end handlers must return a message with the same role",
|
| 905 |
+
});
|
| 906 |
+
continue;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
currentMessage = handlerResult.message;
|
| 910 |
+
modified = true;
|
| 911 |
+
} catch (err) {
|
| 912 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 913 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 914 |
+
this.emitError({
|
| 915 |
+
extensionPath: ext.path,
|
| 916 |
+
event: "message_end",
|
| 917 |
+
error: message,
|
| 918 |
+
stack,
|
| 919 |
+
});
|
| 920 |
+
}
|
| 921 |
+
}
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
return modified ? currentMessage : undefined;
|
| 925 |
+
}
|
| 926 |
+
|
| 927 |
+
async emitToolResult(event: ToolResultEvent): Promise<ToolResultEventResult | undefined> {
|
| 928 |
+
const ctx = this.createContext();
|
| 929 |
+
const currentEvent: ToolResultEvent = { ...event };
|
| 930 |
+
let modified = false;
|
| 931 |
+
|
| 932 |
+
for (const ext of this.extensions) {
|
| 933 |
+
const handlers = ext.handlers.get("tool_result");
|
| 934 |
+
if (!handlers || handlers.length === 0) continue;
|
| 935 |
+
|
| 936 |
+
for (const handler of handlers) {
|
| 937 |
+
try {
|
| 938 |
+
const handlerResult = (await handler(currentEvent, ctx)) as ToolResultEventResult | undefined;
|
| 939 |
+
if (!handlerResult) continue;
|
| 940 |
+
|
| 941 |
+
if (handlerResult.content !== undefined) {
|
| 942 |
+
currentEvent.content = handlerResult.content;
|
| 943 |
+
modified = true;
|
| 944 |
+
}
|
| 945 |
+
if (handlerResult.details !== undefined) {
|
| 946 |
+
currentEvent.details = handlerResult.details;
|
| 947 |
+
modified = true;
|
| 948 |
+
}
|
| 949 |
+
if (handlerResult.isError !== undefined) {
|
| 950 |
+
currentEvent.isError = handlerResult.isError;
|
| 951 |
+
modified = true;
|
| 952 |
+
}
|
| 953 |
+
if (handlerResult.usage !== undefined) {
|
| 954 |
+
currentEvent.usage = handlerResult.usage;
|
| 955 |
+
modified = true;
|
| 956 |
+
}
|
| 957 |
+
} catch (err) {
|
| 958 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 959 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 960 |
+
this.emitError({
|
| 961 |
+
extensionPath: ext.path,
|
| 962 |
+
event: "tool_result",
|
| 963 |
+
error: message,
|
| 964 |
+
stack,
|
| 965 |
+
});
|
| 966 |
+
}
|
| 967 |
+
}
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
if (!modified) {
|
| 971 |
+
return undefined;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
return {
|
| 975 |
+
content: currentEvent.content,
|
| 976 |
+
details: currentEvent.details,
|
| 977 |
+
isError: currentEvent.isError,
|
| 978 |
+
usage: currentEvent.usage,
|
| 979 |
+
};
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
async emitToolCall(event: ToolCallEvent): Promise<ToolCallEventResult | undefined> {
|
| 983 |
+
const ctx = this.createContext();
|
| 984 |
+
let result: ToolCallEventResult | undefined;
|
| 985 |
+
|
| 986 |
+
for (const ext of this.extensions) {
|
| 987 |
+
const handlers = ext.handlers.get("tool_call");
|
| 988 |
+
if (!handlers || handlers.length === 0) continue;
|
| 989 |
+
|
| 990 |
+
for (const handler of handlers) {
|
| 991 |
+
const handlerResult = await handler(event, ctx);
|
| 992 |
+
|
| 993 |
+
if (handlerResult) {
|
| 994 |
+
result = handlerResult as ToolCallEventResult;
|
| 995 |
+
if (result.block) {
|
| 996 |
+
return result;
|
| 997 |
+
}
|
| 998 |
+
}
|
| 999 |
+
}
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
return result;
|
| 1003 |
+
}
|
| 1004 |
+
|
| 1005 |
+
async emitUserBash(event: UserBashEvent): Promise<UserBashEventResult | undefined> {
|
| 1006 |
+
const ctx = this.createContext();
|
| 1007 |
+
|
| 1008 |
+
for (const ext of this.extensions) {
|
| 1009 |
+
const handlers = ext.handlers.get("user_bash");
|
| 1010 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1011 |
+
|
| 1012 |
+
for (const handler of handlers) {
|
| 1013 |
+
try {
|
| 1014 |
+
const handlerResult = await handler(event, ctx);
|
| 1015 |
+
if (handlerResult) {
|
| 1016 |
+
return handlerResult as UserBashEventResult;
|
| 1017 |
+
}
|
| 1018 |
+
} catch (err) {
|
| 1019 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1020 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1021 |
+
this.emitError({
|
| 1022 |
+
extensionPath: ext.path,
|
| 1023 |
+
event: "user_bash",
|
| 1024 |
+
error: message,
|
| 1025 |
+
stack,
|
| 1026 |
+
});
|
| 1027 |
+
}
|
| 1028 |
+
}
|
| 1029 |
+
}
|
| 1030 |
+
|
| 1031 |
+
return undefined;
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
|
| 1035 |
+
const ctx = this.createContext();
|
| 1036 |
+
let currentMessages = structuredClone(messages);
|
| 1037 |
+
|
| 1038 |
+
for (const ext of this.extensions) {
|
| 1039 |
+
const handlers = ext.handlers.get("context");
|
| 1040 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1041 |
+
|
| 1042 |
+
for (const handler of handlers) {
|
| 1043 |
+
try {
|
| 1044 |
+
const event: ContextEvent = { type: "context", messages: currentMessages };
|
| 1045 |
+
const handlerResult = await handler(event, ctx);
|
| 1046 |
+
|
| 1047 |
+
if (handlerResult && (handlerResult as ContextEventResult).messages) {
|
| 1048 |
+
currentMessages = (handlerResult as ContextEventResult).messages!;
|
| 1049 |
+
}
|
| 1050 |
+
} catch (err) {
|
| 1051 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1052 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1053 |
+
this.emitError({
|
| 1054 |
+
extensionPath: ext.path,
|
| 1055 |
+
event: "context",
|
| 1056 |
+
error: message,
|
| 1057 |
+
stack,
|
| 1058 |
+
});
|
| 1059 |
+
}
|
| 1060 |
+
}
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
return currentMessages;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
async emitBeforeProviderRequest(payload: unknown): Promise<unknown> {
|
| 1067 |
+
const ctx = this.createContext();
|
| 1068 |
+
let currentPayload = payload;
|
| 1069 |
+
|
| 1070 |
+
for (const ext of this.extensions) {
|
| 1071 |
+
const handlers = ext.handlers.get("before_provider_request");
|
| 1072 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1073 |
+
|
| 1074 |
+
for (const handler of handlers) {
|
| 1075 |
+
try {
|
| 1076 |
+
const event: BeforeProviderRequestEvent = {
|
| 1077 |
+
type: "before_provider_request",
|
| 1078 |
+
payload: currentPayload,
|
| 1079 |
+
};
|
| 1080 |
+
const handlerResult = await handler(event, ctx);
|
| 1081 |
+
if (handlerResult !== undefined) {
|
| 1082 |
+
currentPayload = handlerResult;
|
| 1083 |
+
}
|
| 1084 |
+
} catch (err) {
|
| 1085 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1086 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1087 |
+
this.emitError({
|
| 1088 |
+
extensionPath: ext.path,
|
| 1089 |
+
event: "before_provider_request",
|
| 1090 |
+
error: message,
|
| 1091 |
+
stack,
|
| 1092 |
+
});
|
| 1093 |
+
}
|
| 1094 |
+
}
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
return currentPayload;
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
async emitBeforeProviderHeaders(headers: ProviderHeaders): Promise<ProviderHeaders> {
|
| 1101 |
+
const ctx = this.createContext();
|
| 1102 |
+
|
| 1103 |
+
for (const ext of this.extensions) {
|
| 1104 |
+
const handlers = ext.handlers.get("before_provider_headers");
|
| 1105 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1106 |
+
|
| 1107 |
+
for (const handler of handlers) {
|
| 1108 |
+
try {
|
| 1109 |
+
// Handlers mutate `headers` in place; the return value is ignored.
|
| 1110 |
+
const event: BeforeProviderHeadersEvent = {
|
| 1111 |
+
type: "before_provider_headers",
|
| 1112 |
+
headers,
|
| 1113 |
+
};
|
| 1114 |
+
await handler(event, ctx);
|
| 1115 |
+
} catch (err) {
|
| 1116 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1117 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1118 |
+
this.emitError({
|
| 1119 |
+
extensionPath: ext.path,
|
| 1120 |
+
event: "before_provider_headers",
|
| 1121 |
+
error: message,
|
| 1122 |
+
stack,
|
| 1123 |
+
});
|
| 1124 |
+
}
|
| 1125 |
+
}
|
| 1126 |
+
}
|
| 1127 |
+
|
| 1128 |
+
return headers;
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
async emitBeforeAgentStart(
|
| 1132 |
+
prompt: string,
|
| 1133 |
+
images: ImageContent[] | undefined,
|
| 1134 |
+
systemPrompt: string,
|
| 1135 |
+
systemPromptOptions: BuildSystemPromptOptions,
|
| 1136 |
+
): Promise<BeforeAgentStartCombinedResult | undefined> {
|
| 1137 |
+
let currentSystemPrompt = systemPrompt;
|
| 1138 |
+
const ctx = Object.defineProperties(
|
| 1139 |
+
{},
|
| 1140 |
+
Object.getOwnPropertyDescriptors(this.createContext()),
|
| 1141 |
+
) as ExtensionContext;
|
| 1142 |
+
ctx.getSystemPrompt = () => {
|
| 1143 |
+
this.assertActive();
|
| 1144 |
+
return currentSystemPrompt;
|
| 1145 |
+
};
|
| 1146 |
+
const messages: NonNullable<BeforeAgentStartEventResult["message"]>[] = [];
|
| 1147 |
+
let systemPromptModified = false;
|
| 1148 |
+
|
| 1149 |
+
for (const ext of this.extensions) {
|
| 1150 |
+
const handlers = ext.handlers.get("before_agent_start");
|
| 1151 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1152 |
+
|
| 1153 |
+
for (const handler of handlers) {
|
| 1154 |
+
try {
|
| 1155 |
+
const event: BeforeAgentStartEvent = {
|
| 1156 |
+
type: "before_agent_start",
|
| 1157 |
+
prompt,
|
| 1158 |
+
images,
|
| 1159 |
+
systemPrompt: currentSystemPrompt,
|
| 1160 |
+
systemPromptOptions,
|
| 1161 |
+
};
|
| 1162 |
+
const handlerResult = await handler(event, ctx);
|
| 1163 |
+
|
| 1164 |
+
if (handlerResult) {
|
| 1165 |
+
const result = handlerResult as BeforeAgentStartEventResult;
|
| 1166 |
+
if (result.message) {
|
| 1167 |
+
messages.push(result.message);
|
| 1168 |
+
}
|
| 1169 |
+
if (result.systemPrompt !== undefined) {
|
| 1170 |
+
currentSystemPrompt = result.systemPrompt;
|
| 1171 |
+
systemPromptModified = true;
|
| 1172 |
+
}
|
| 1173 |
+
}
|
| 1174 |
+
} catch (err) {
|
| 1175 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1176 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1177 |
+
this.emitError({
|
| 1178 |
+
extensionPath: ext.path,
|
| 1179 |
+
event: "before_agent_start",
|
| 1180 |
+
error: message,
|
| 1181 |
+
stack,
|
| 1182 |
+
});
|
| 1183 |
+
}
|
| 1184 |
+
}
|
| 1185 |
+
}
|
| 1186 |
+
|
| 1187 |
+
if (messages.length > 0 || systemPromptModified) {
|
| 1188 |
+
return {
|
| 1189 |
+
messages: messages.length > 0 ? messages : undefined,
|
| 1190 |
+
systemPrompt: systemPromptModified ? currentSystemPrompt : undefined,
|
| 1191 |
+
};
|
| 1192 |
+
}
|
| 1193 |
+
|
| 1194 |
+
return undefined;
|
| 1195 |
+
}
|
| 1196 |
+
|
| 1197 |
+
async emitResourcesDiscover(
|
| 1198 |
+
cwd: string,
|
| 1199 |
+
reason: ResourcesDiscoverEvent["reason"],
|
| 1200 |
+
): Promise<{
|
| 1201 |
+
skillPaths: Array<{ path: string; extensionPath: string }>;
|
| 1202 |
+
promptPaths: Array<{ path: string; extensionPath: string }>;
|
| 1203 |
+
themePaths: Array<{ path: string; extensionPath: string }>;
|
| 1204 |
+
}> {
|
| 1205 |
+
const ctx = this.createContext();
|
| 1206 |
+
const skillPaths: Array<{ path: string; extensionPath: string }> = [];
|
| 1207 |
+
const promptPaths: Array<{ path: string; extensionPath: string }> = [];
|
| 1208 |
+
const themePaths: Array<{ path: string; extensionPath: string }> = [];
|
| 1209 |
+
|
| 1210 |
+
for (const ext of this.extensions) {
|
| 1211 |
+
const handlers = ext.handlers.get("resources_discover");
|
| 1212 |
+
if (!handlers || handlers.length === 0) continue;
|
| 1213 |
+
|
| 1214 |
+
for (const handler of handlers) {
|
| 1215 |
+
try {
|
| 1216 |
+
const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
|
| 1217 |
+
const handlerResult = await handler(event, ctx);
|
| 1218 |
+
const result = handlerResult as ResourcesDiscoverResult | undefined;
|
| 1219 |
+
|
| 1220 |
+
if (result?.skillPaths?.length) {
|
| 1221 |
+
skillPaths.push(...result.skillPaths.map((path) => ({ path, extensionPath: ext.path })));
|
| 1222 |
+
}
|
| 1223 |
+
if (result?.promptPaths?.length) {
|
| 1224 |
+
promptPaths.push(...result.promptPaths.map((path) => ({ path, extensionPath: ext.path })));
|
| 1225 |
+
}
|
| 1226 |
+
if (result?.themePaths?.length) {
|
| 1227 |
+
themePaths.push(...result.themePaths.map((path) => ({ path, extensionPath: ext.path })));
|
| 1228 |
+
}
|
| 1229 |
+
} catch (err) {
|
| 1230 |
+
const message = err instanceof Error ? err.message : String(err);
|
| 1231 |
+
const stack = err instanceof Error ? err.stack : undefined;
|
| 1232 |
+
this.emitError({
|
| 1233 |
+
extensionPath: ext.path,
|
| 1234 |
+
event: "resources_discover",
|
| 1235 |
+
error: message,
|
| 1236 |
+
stack,
|
| 1237 |
+
});
|
| 1238 |
+
}
|
| 1239 |
+
}
|
| 1240 |
+
}
|
| 1241 |
+
|
| 1242 |
+
return { skillPaths, promptPaths, themePaths };
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
/** Emit input event. Transforms chain, "handled" short-circuits. */
|
| 1246 |
+
async emitInput(
|
| 1247 |
+
text: string,
|
| 1248 |
+
images: ImageContent[] | undefined,
|
| 1249 |
+
source: InputSource,
|
| 1250 |
+
streamingBehavior?: "steer" | "followUp",
|
| 1251 |
+
): Promise<InputEventResult> {
|
| 1252 |
+
const ctx = this.createContext();
|
| 1253 |
+
let currentText = text;
|
| 1254 |
+
let currentImages = images;
|
| 1255 |
+
|
| 1256 |
+
for (const ext of this.extensions) {
|
| 1257 |
+
for (const handler of ext.handlers.get("input") ?? []) {
|
| 1258 |
+
try {
|
| 1259 |
+
const event: InputEvent = {
|
| 1260 |
+
type: "input",
|
| 1261 |
+
text: currentText,
|
| 1262 |
+
images: currentImages,
|
| 1263 |
+
source,
|
| 1264 |
+
streamingBehavior,
|
| 1265 |
+
};
|
| 1266 |
+
const result = (await handler(event, ctx)) as InputEventResult | undefined;
|
| 1267 |
+
if (result?.action === "handled") return result;
|
| 1268 |
+
if (result?.action === "transform") {
|
| 1269 |
+
currentText = result.text;
|
| 1270 |
+
currentImages = result.images ?? currentImages;
|
| 1271 |
+
}
|
| 1272 |
+
} catch (err) {
|
| 1273 |
+
this.emitError({
|
| 1274 |
+
extensionPath: ext.path,
|
| 1275 |
+
event: "input",
|
| 1276 |
+
error: err instanceof Error ? err.message : String(err),
|
| 1277 |
+
stack: err instanceof Error ? err.stack : undefined,
|
| 1278 |
+
});
|
| 1279 |
+
}
|
| 1280 |
+
}
|
| 1281 |
+
}
|
| 1282 |
+
return currentText !== text || currentImages !== images
|
| 1283 |
+
? { action: "transform", text: currentText, images: currentImages }
|
| 1284 |
+
: { action: "continue" };
|
| 1285 |
+
}
|
| 1286 |
+
}
|
packages/coding-agent/src/core/extensions/types.ts
ADDED
|
@@ -0,0 +1,1797 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Extension system types.
|
| 3 |
+
*
|
| 4 |
+
* Extensions are TypeScript modules that can:
|
| 5 |
+
* - Subscribe to agent lifecycle events
|
| 6 |
+
* - Register LLM-callable tools
|
| 7 |
+
* - Register commands, keyboard shortcuts, and CLI flags
|
| 8 |
+
* - Interact with the user via UI primitives
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
import type {
|
| 12 |
+
AgentMessage,
|
| 13 |
+
AgentToolResult,
|
| 14 |
+
AgentToolUpdateCallback,
|
| 15 |
+
ThinkingLevel,
|
| 16 |
+
ToolExecutionMode,
|
| 17 |
+
} from "@earendil-works/pi-agent-core";
|
| 18 |
+
import type {
|
| 19 |
+
Api,
|
| 20 |
+
AssistantMessageEvent,
|
| 21 |
+
AssistantMessageEventStream,
|
| 22 |
+
ConstrainedSamplingConfig,
|
| 23 |
+
Context,
|
| 24 |
+
ImageContent,
|
| 25 |
+
Model,
|
| 26 |
+
OAuthCredentials,
|
| 27 |
+
OAuthLoginCallbacks,
|
| 28 |
+
Provider,
|
| 29 |
+
ProviderHeaders,
|
| 30 |
+
RefreshModelsContext,
|
| 31 |
+
SimpleStreamOptions,
|
| 32 |
+
TextContent,
|
| 33 |
+
ToolResultMessage,
|
| 34 |
+
Usage,
|
| 35 |
+
} from "@earendil-works/pi-ai";
|
| 36 |
+
import type {
|
| 37 |
+
AutocompleteItem,
|
| 38 |
+
AutocompleteProvider,
|
| 39 |
+
Component,
|
| 40 |
+
EditorComponent,
|
| 41 |
+
EditorTheme,
|
| 42 |
+
KeyId,
|
| 43 |
+
OverlayHandle,
|
| 44 |
+
OverlayOptions,
|
| 45 |
+
TUI,
|
| 46 |
+
} from "@earendil-works/pi-tui";
|
| 47 |
+
import type { Static, TSchema } from "typebox";
|
| 48 |
+
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
| 49 |
+
import type { BashResult } from "../bash-executor.ts";
|
| 50 |
+
import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
|
| 51 |
+
import type { EventBus } from "../event-bus.ts";
|
| 52 |
+
import type { ExecOptions, ExecResult } from "../exec.ts";
|
| 53 |
+
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.ts";
|
| 54 |
+
import type { KeybindingsManager } from "../keybindings.ts";
|
| 55 |
+
import type { CustomMessage } from "../messages.ts";
|
| 56 |
+
import type { ModelRegistry } from "../model-registry.ts";
|
| 57 |
+
import type { ScopedModel } from "../model-resolver.ts";
|
| 58 |
+
import type {
|
| 59 |
+
BranchSummaryEntry,
|
| 60 |
+
CompactionEntry,
|
| 61 |
+
CustomEntry,
|
| 62 |
+
ReadonlySessionManager,
|
| 63 |
+
SessionEntry,
|
| 64 |
+
SessionManager,
|
| 65 |
+
} from "../session-manager.ts";
|
| 66 |
+
import type { SlashCommandInfo } from "../slash-commands.ts";
|
| 67 |
+
import type { SourceInfo } from "../source-info.ts";
|
| 68 |
+
import type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
| 69 |
+
import type { BashOperations } from "../tools/bash.ts";
|
| 70 |
+
import type { EditToolDetails } from "../tools/edit.ts";
|
| 71 |
+
import type {
|
| 72 |
+
BashToolDetails,
|
| 73 |
+
BashToolInput,
|
| 74 |
+
EditToolInput,
|
| 75 |
+
FindToolDetails,
|
| 76 |
+
FindToolInput,
|
| 77 |
+
GrepToolDetails,
|
| 78 |
+
GrepToolInput,
|
| 79 |
+
LsToolDetails,
|
| 80 |
+
LsToolInput,
|
| 81 |
+
PowerShellToolDetails,
|
| 82 |
+
PowerShellToolInput,
|
| 83 |
+
ReadToolDetails,
|
| 84 |
+
ReadToolInput,
|
| 85 |
+
WriteToolInput,
|
| 86 |
+
} from "../tools/index.ts";
|
| 87 |
+
|
| 88 |
+
export type { ExecOptions, ExecResult } from "../exec.ts";
|
| 89 |
+
export type { BuildSystemPromptOptions } from "../system-prompt.ts";
|
| 90 |
+
export type { AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode };
|
| 91 |
+
export type { AppKeybinding, KeybindingsManager } from "../keybindings.ts";
|
| 92 |
+
|
| 93 |
+
// ============================================================================
|
| 94 |
+
// UI Context
|
| 95 |
+
// ============================================================================
|
| 96 |
+
|
| 97 |
+
/** Options for extension UI dialogs. */
|
| 98 |
+
export interface ExtensionUIDialogOptions {
|
| 99 |
+
/** AbortSignal to programmatically dismiss the dialog. */
|
| 100 |
+
signal?: AbortSignal;
|
| 101 |
+
/** Timeout in milliseconds. Dialog auto-dismisses with live countdown display. */
|
| 102 |
+
timeout?: number;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/** Placement for extension widgets. */
|
| 106 |
+
export type WidgetPlacement = "aboveEditor" | "belowEditor";
|
| 107 |
+
|
| 108 |
+
/** Options for extension widgets. */
|
| 109 |
+
export interface ExtensionWidgetOptions {
|
| 110 |
+
/** Where the widget is rendered. Defaults to "aboveEditor". */
|
| 111 |
+
placement?: WidgetPlacement;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/** Raw terminal input listener for extensions. */
|
| 115 |
+
export type TerminalInputHandler = (data: string) => { consume?: boolean; data?: string } | undefined;
|
| 116 |
+
|
| 117 |
+
/** Working indicator configuration for the interactive streaming loader. */
|
| 118 |
+
export interface WorkingIndicatorOptions {
|
| 119 |
+
/** Animation frames. Use an empty array to hide the indicator entirely. Custom frames are rendered verbatim. */
|
| 120 |
+
frames?: string[];
|
| 121 |
+
/** Frame interval in milliseconds for animated indicators. */
|
| 122 |
+
intervalMs?: number;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/** Wrap the current autocomplete provider with additional behavior. */
|
| 126 |
+
export type AutocompleteProviderFactory = (current: AutocompleteProvider) => AutocompleteProvider;
|
| 127 |
+
export type EditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
|
| 128 |
+
|
| 129 |
+
/**
|
| 130 |
+
* UI context for extensions to request interactive UI.
|
| 131 |
+
* Each mode (interactive, RPC, print) provides its own implementation.
|
| 132 |
+
*/
|
| 133 |
+
export interface ExtensionUIContext {
|
| 134 |
+
/** Show a selector and return the user's choice. */
|
| 135 |
+
select(title: string, options: string[], opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
| 136 |
+
|
| 137 |
+
/** Show a confirmation dialog. */
|
| 138 |
+
confirm(title: string, message: string, opts?: ExtensionUIDialogOptions): Promise<boolean>;
|
| 139 |
+
|
| 140 |
+
/** Show a text input dialog. */
|
| 141 |
+
input(title: string, placeholder?: string, opts?: ExtensionUIDialogOptions): Promise<string | undefined>;
|
| 142 |
+
|
| 143 |
+
/** Show a notification to the user. */
|
| 144 |
+
notify(message: string, type?: "info" | "warning" | "error"): void;
|
| 145 |
+
|
| 146 |
+
/** Listen to raw terminal input (interactive mode only). Returns an unsubscribe function. */
|
| 147 |
+
onTerminalInput(handler: TerminalInputHandler): () => void;
|
| 148 |
+
|
| 149 |
+
/** Set status text in the footer/status bar. Pass undefined to clear. */
|
| 150 |
+
setStatus(key: string, text: string | undefined): void;
|
| 151 |
+
|
| 152 |
+
/** Set the working/loading message shown during streaming. Call with no argument to restore default. */
|
| 153 |
+
setWorkingMessage(message?: string): void;
|
| 154 |
+
|
| 155 |
+
/** Show or hide the built-in interactive working loader row during streaming. */
|
| 156 |
+
setWorkingVisible(visible: boolean): void;
|
| 157 |
+
|
| 158 |
+
/**
|
| 159 |
+
* Configure the interactive working indicator shown during streaming.
|
| 160 |
+
*
|
| 161 |
+
* - Omit the argument to restore the default animated spinner.
|
| 162 |
+
* - Use `frames: ["●"]` for a static indicator.
|
| 163 |
+
* - Use `frames: []` to hide the indicator entirely.
|
| 164 |
+
* - Custom frames are rendered as provided, so extensions must add their own colors.
|
| 165 |
+
*/
|
| 166 |
+
setWorkingIndicator(options?: WorkingIndicatorOptions): void;
|
| 167 |
+
|
| 168 |
+
/** Set the label shown for hidden thinking blocks. Call with no argument to restore default. */
|
| 169 |
+
setHiddenThinkingLabel(label?: string): void;
|
| 170 |
+
|
| 171 |
+
/** Set a widget to display above or below the editor. Accepts string array or component factory. */
|
| 172 |
+
setWidget(key: string, content: string[] | undefined, options?: ExtensionWidgetOptions): void;
|
| 173 |
+
setWidget(
|
| 174 |
+
key: string,
|
| 175 |
+
content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
|
| 176 |
+
options?: ExtensionWidgetOptions,
|
| 177 |
+
): void;
|
| 178 |
+
|
| 179 |
+
/** Set a custom footer component, or undefined to restore the built-in footer.
|
| 180 |
+
*
|
| 181 |
+
* The factory receives a FooterDataProvider for data not otherwise accessible:
|
| 182 |
+
* git branch and extension statuses from setStatus(). Context usage is on
|
| 183 |
+
* ctx.getContextUsage(), token stats on ctx.sessionManager.getEntries(), model info on ctx.model.
|
| 184 |
+
*/
|
| 185 |
+
setFooter(
|
| 186 |
+
factory:
|
| 187 |
+
| ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
|
| 188 |
+
| undefined,
|
| 189 |
+
): void;
|
| 190 |
+
|
| 191 |
+
/** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */
|
| 192 |
+
setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
|
| 193 |
+
|
| 194 |
+
/** Set the terminal window/tab title. */
|
| 195 |
+
setTitle(title: string): void;
|
| 196 |
+
|
| 197 |
+
/** Show a custom component with keyboard focus. */
|
| 198 |
+
custom<T>(
|
| 199 |
+
factory: (
|
| 200 |
+
tui: TUI,
|
| 201 |
+
theme: Theme,
|
| 202 |
+
keybindings: KeybindingsManager,
|
| 203 |
+
done: (result: T) => void,
|
| 204 |
+
) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
|
| 205 |
+
options?: {
|
| 206 |
+
overlay?: boolean;
|
| 207 |
+
/** Overlay positioning/sizing options. Can be static or a function for dynamic updates. */
|
| 208 |
+
overlayOptions?: OverlayOptions | (() => OverlayOptions);
|
| 209 |
+
/** Called with the overlay handle after the overlay is shown. Use to control visibility. */
|
| 210 |
+
onHandle?: (handle: OverlayHandle) => void;
|
| 211 |
+
},
|
| 212 |
+
): Promise<T>;
|
| 213 |
+
|
| 214 |
+
/** Paste text into the editor, triggering paste handling (collapse for large content). */
|
| 215 |
+
pasteToEditor(text: string): void;
|
| 216 |
+
|
| 217 |
+
/** Set the text in the core input editor. */
|
| 218 |
+
setEditorText(text: string): void;
|
| 219 |
+
|
| 220 |
+
/** Get the current text from the core input editor. */
|
| 221 |
+
getEditorText(): string;
|
| 222 |
+
|
| 223 |
+
/** Show a multi-line editor for text editing. */
|
| 224 |
+
editor(title: string, prefill?: string): Promise<string | undefined>;
|
| 225 |
+
|
| 226 |
+
/** Stack additional autocomplete behavior on top of the built-in provider. */
|
| 227 |
+
addAutocompleteProvider(factory: AutocompleteProviderFactory): void;
|
| 228 |
+
|
| 229 |
+
/**
|
| 230 |
+
* Set a custom editor component via factory function.
|
| 231 |
+
* Pass undefined to restore the default editor.
|
| 232 |
+
*
|
| 233 |
+
* The factory receives:
|
| 234 |
+
* - `theme`: EditorTheme for styling borders and autocomplete
|
| 235 |
+
* - `keybindings`: KeybindingsManager for app-level keybindings
|
| 236 |
+
*
|
| 237 |
+
* For full app keybinding support (escape, ctrl+d, model switching, etc.),
|
| 238 |
+
* extend `CustomEditor` from `@earendil-works/pi-coding-agent` and call
|
| 239 |
+
* `super.handleInput(data)` for keys you don't handle.
|
| 240 |
+
*
|
| 241 |
+
* @example
|
| 242 |
+
* ```ts
|
| 243 |
+
* import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
| 244 |
+
*
|
| 245 |
+
* class VimEditor extends CustomEditor {
|
| 246 |
+
* private mode: "normal" | "insert" = "insert";
|
| 247 |
+
*
|
| 248 |
+
* handleInput(data: string): void {
|
| 249 |
+
* if (this.mode === "normal") {
|
| 250 |
+
* // Handle vim normal mode keys...
|
| 251 |
+
* if (data === "i") { this.mode = "insert"; return; }
|
| 252 |
+
* }
|
| 253 |
+
* super.handleInput(data); // App keybindings + text editing
|
| 254 |
+
* }
|
| 255 |
+
* }
|
| 256 |
+
*
|
| 257 |
+
* ctx.ui.setEditorComponent((tui, theme, keybindings) =>
|
| 258 |
+
* new VimEditor(tui, theme, keybindings)
|
| 259 |
+
* );
|
| 260 |
+
* ```
|
| 261 |
+
*/
|
| 262 |
+
setEditorComponent(factory: EditorFactory | undefined): void;
|
| 263 |
+
|
| 264 |
+
/** Get the currently configured custom editor factory, or undefined when using the default editor. */
|
| 265 |
+
getEditorComponent(): EditorFactory | undefined;
|
| 266 |
+
|
| 267 |
+
/** Get the current theme for styling. */
|
| 268 |
+
readonly theme: Theme;
|
| 269 |
+
|
| 270 |
+
/** Get all available themes with their names and file paths. */
|
| 271 |
+
getAllThemes(): { name: string; path: string | undefined }[];
|
| 272 |
+
|
| 273 |
+
/** Load a theme by name without switching to it. Returns undefined if not found. */
|
| 274 |
+
getTheme(name: string): Theme | undefined;
|
| 275 |
+
|
| 276 |
+
/** Set the current theme by name or Theme object. */
|
| 277 |
+
setTheme(theme: string | Theme): { success: boolean; error?: string };
|
| 278 |
+
|
| 279 |
+
/** Get current tool output expansion state. */
|
| 280 |
+
getToolsExpanded(): boolean;
|
| 281 |
+
|
| 282 |
+
/** Set tool output expansion state. */
|
| 283 |
+
setToolsExpanded(expanded: boolean): void;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
// ============================================================================
|
| 287 |
+
// Extension Context
|
| 288 |
+
// ============================================================================
|
| 289 |
+
|
| 290 |
+
export interface ContextUsage {
|
| 291 |
+
/** Estimated context tokens, or null if unknown (e.g. right after compaction, before next LLM response). */
|
| 292 |
+
tokens: number | null;
|
| 293 |
+
contextWindow: number;
|
| 294 |
+
/** Context usage as percentage of context window, or null if tokens is unknown. */
|
| 295 |
+
percent: number | null;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
export interface CompactOptions {
|
| 299 |
+
customInstructions?: string;
|
| 300 |
+
onComplete?: (result: CompactionResult) => void;
|
| 301 |
+
onError?: (error: Error) => void;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
/**
|
| 305 |
+
* Context passed to extension event handlers.
|
| 306 |
+
*/
|
| 307 |
+
export type ExtensionMode = "tui" | "rpc" | "json" | "print";
|
| 308 |
+
|
| 309 |
+
export interface ExtensionContext {
|
| 310 |
+
/** UI methods for user interaction */
|
| 311 |
+
ui: ExtensionUIContext;
|
| 312 |
+
/** Current run mode. Use "tui" to guard terminal-only UI such as custom components. */
|
| 313 |
+
mode: ExtensionMode;
|
| 314 |
+
/** Whether dialog-capable UI is available (true in TUI and RPC modes) */
|
| 315 |
+
hasUI: boolean;
|
| 316 |
+
/** Current working directory */
|
| 317 |
+
cwd: string;
|
| 318 |
+
/** Session manager (read-only) */
|
| 319 |
+
sessionManager: ReadonlySessionManager;
|
| 320 |
+
/** Model registry for API key resolution */
|
| 321 |
+
modelRegistry: ModelRegistry;
|
| 322 |
+
/** Current model (may be undefined) */
|
| 323 |
+
model: Model<any> | undefined;
|
| 324 |
+
/** Models scoped to this session (resolved from `--models` /
|
| 325 |
+
* `enabledModels` settings against the available catalogue). Same set
|
| 326 |
+
* the `/scoped-models` command shows. Empty when no scoping is
|
| 327 |
+
* configured (all available models are usable). Read-only snapshot. */
|
| 328 |
+
scopedModels: readonly ScopedModel[];
|
| 329 |
+
/** Current thinking level, when provided by the session runtime. */
|
| 330 |
+
thinkingLevel?: ThinkingLevel;
|
| 331 |
+
/** Whether the agent is idle (not streaming) */
|
| 332 |
+
isIdle(): boolean;
|
| 333 |
+
/** Whether project-local trust is active for this context. */
|
| 334 |
+
isProjectTrusted(): boolean;
|
| 335 |
+
/** The current abort signal, or undefined when the agent is not streaming. */
|
| 336 |
+
signal: AbortSignal | undefined;
|
| 337 |
+
/** Abort the current agent operation */
|
| 338 |
+
abort(): void;
|
| 339 |
+
/** Whether there are queued messages waiting */
|
| 340 |
+
hasPendingMessages(): boolean;
|
| 341 |
+
/** Gracefully shutdown pi and exit. Available in all contexts. */
|
| 342 |
+
shutdown(): void;
|
| 343 |
+
/** Get current context usage for the active model. */
|
| 344 |
+
getContextUsage(): ContextUsage | undefined;
|
| 345 |
+
/** Trigger compaction without awaiting completion. */
|
| 346 |
+
compact(options?: CompactOptions): void;
|
| 347 |
+
/** Get the current effective system prompt. */
|
| 348 |
+
getSystemPrompt(): string;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
/**
|
| 352 |
+
* Extended context for command handlers.
|
| 353 |
+
* Includes session control methods only safe in user-initiated commands.
|
| 354 |
+
*/
|
| 355 |
+
export interface ExtensionCommandContext extends ExtensionContext {
|
| 356 |
+
/** Get the current base system-prompt construction options. */
|
| 357 |
+
getSystemPromptOptions(): BuildSystemPromptOptions;
|
| 358 |
+
|
| 359 |
+
/** Wait for the agent to finish streaming */
|
| 360 |
+
waitForIdle(): Promise<void>;
|
| 361 |
+
|
| 362 |
+
/** Start a new session, optionally with initialization. */
|
| 363 |
+
newSession(options?: {
|
| 364 |
+
parentSession?: string;
|
| 365 |
+
setup?: (sessionManager: SessionManager) => Promise<void>;
|
| 366 |
+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
| 367 |
+
}): Promise<{ cancelled: boolean }>;
|
| 368 |
+
|
| 369 |
+
/** Fork from a specific entry, creating a new session file. */
|
| 370 |
+
fork(
|
| 371 |
+
entryId: string,
|
| 372 |
+
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 373 |
+
): Promise<{ cancelled: boolean }>;
|
| 374 |
+
|
| 375 |
+
/** Navigate to a different point in the session tree. */
|
| 376 |
+
navigateTree(
|
| 377 |
+
targetId: string,
|
| 378 |
+
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
| 379 |
+
): Promise<{ cancelled: boolean }>;
|
| 380 |
+
|
| 381 |
+
/** Switch to a different session file. */
|
| 382 |
+
switchSession(
|
| 383 |
+
sessionPath: string,
|
| 384 |
+
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 385 |
+
): Promise<{ cancelled: boolean }>;
|
| 386 |
+
|
| 387 |
+
/** Reload extensions, skills, prompts, themes, and context files. */
|
| 388 |
+
reload(): Promise<void>;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
/**
|
| 392 |
+
* Fresh command-capable context bound to the replacement session after a session switch.
|
| 393 |
+
*
|
| 394 |
+
* This is passed to `withSession()` callbacks on `newSession()`, `fork()`, and `switchSession()`.
|
| 395 |
+
*/
|
| 396 |
+
export interface ReplacedSessionContext extends ExtensionCommandContext {
|
| 397 |
+
sendMessage<T = unknown>(
|
| 398 |
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
| 399 |
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
| 400 |
+
): Promise<void>;
|
| 401 |
+
|
| 402 |
+
sendUserMessage(
|
| 403 |
+
content: string | (TextContent | ImageContent)[],
|
| 404 |
+
options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
|
| 405 |
+
): Promise<void>;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
// ============================================================================
|
| 409 |
+
// Tool Types
|
| 410 |
+
// ============================================================================
|
| 411 |
+
|
| 412 |
+
/** Rendering options for tool results */
|
| 413 |
+
export interface ToolRenderResultOptions {
|
| 414 |
+
/** Whether the result view is expanded */
|
| 415 |
+
expanded: boolean;
|
| 416 |
+
/** Whether this is a partial/streaming result */
|
| 417 |
+
isPartial: boolean;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
/** Context passed to tool renderers. */
|
| 421 |
+
export interface ToolRenderContext<TState = any, TArgs = any> {
|
| 422 |
+
/** Current tool call arguments. Shared across call/result renders for the same tool call. */
|
| 423 |
+
args: TArgs;
|
| 424 |
+
/** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
|
| 425 |
+
toolCallId: string;
|
| 426 |
+
/** Invalidate just this tool execution component for redraw. */
|
| 427 |
+
invalidate: () => void;
|
| 428 |
+
/** Previously returned component for this render slot, if any. */
|
| 429 |
+
lastComponent: Component | undefined;
|
| 430 |
+
/** Shared renderer state for this tool row. Initialized by tool-execution.ts. */
|
| 431 |
+
state: TState;
|
| 432 |
+
/** Working directory for this tool execution. */
|
| 433 |
+
cwd: string;
|
| 434 |
+
/** Whether the tool execution has started. */
|
| 435 |
+
executionStarted: boolean;
|
| 436 |
+
/** Whether the tool call arguments are complete. */
|
| 437 |
+
argsComplete: boolean;
|
| 438 |
+
/** Whether the tool result is partial/streaming. */
|
| 439 |
+
isPartial: boolean;
|
| 440 |
+
/** Whether the result view is expanded. */
|
| 441 |
+
expanded: boolean;
|
| 442 |
+
/** Whether inline images are currently shown in the TUI. */
|
| 443 |
+
showImages: boolean;
|
| 444 |
+
/** Whether the current result is an error. */
|
| 445 |
+
isError: boolean;
|
| 446 |
+
}
|
| 447 |
+
|
| 448 |
+
/**
|
| 449 |
+
* Tool definition for registerTool().
|
| 450 |
+
*/
|
| 451 |
+
export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = unknown, TState = any> {
|
| 452 |
+
/** Tool name (used in LLM tool calls) */
|
| 453 |
+
name: string;
|
| 454 |
+
/** Human-readable label for UI */
|
| 455 |
+
label: string;
|
| 456 |
+
/** Description for LLM */
|
| 457 |
+
description: string;
|
| 458 |
+
/** Optional one-line snippet for the Available tools section in the default system prompt. Custom tools are omitted from that section when this is not provided. */
|
| 459 |
+
promptSnippet?: string;
|
| 460 |
+
/** Optional guideline bullets appended to the default system prompt Guidelines section when this tool is active. */
|
| 461 |
+
promptGuidelines?: string[];
|
| 462 |
+
/** Parameter schema (TypeBox) */
|
| 463 |
+
parameters: TParams;
|
| 464 |
+
/** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
|
| 465 |
+
constrainedSampling?: false | ConstrainedSamplingConfig;
|
| 466 |
+
/** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
|
| 467 |
+
renderShell?: "default" | "self";
|
| 468 |
+
|
| 469 |
+
/** Optional compatibility shim to prepare raw tool call arguments before schema validation. Must return an object conforming to TParams. */
|
| 470 |
+
prepareArguments?: (args: unknown) => Static<TParams>;
|
| 471 |
+
|
| 472 |
+
/**
|
| 473 |
+
* Per-tool execution mode override.
|
| 474 |
+
* - "sequential": this tool must execute one at a time with other tool calls.
|
| 475 |
+
* - "parallel": this tool can execute concurrently with other tool calls.
|
| 476 |
+
*
|
| 477 |
+
* If omitted, the default execution mode applies.
|
| 478 |
+
*/
|
| 479 |
+
executionMode?: ToolExecutionMode;
|
| 480 |
+
|
| 481 |
+
/** Execute the tool. */
|
| 482 |
+
execute(
|
| 483 |
+
toolCallId: string,
|
| 484 |
+
params: Static<TParams>,
|
| 485 |
+
signal: AbortSignal | undefined,
|
| 486 |
+
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
| 487 |
+
ctx: ExtensionContext,
|
| 488 |
+
): Promise<AgentToolResult<TDetails>>;
|
| 489 |
+
|
| 490 |
+
/** Custom rendering for tool call display */
|
| 491 |
+
renderCall?: (args: Static<TParams>, theme: Theme, context: ToolRenderContext<TState, Static<TParams>>) => Component;
|
| 492 |
+
|
| 493 |
+
/** Custom rendering for tool result display */
|
| 494 |
+
renderResult?: (
|
| 495 |
+
result: AgentToolResult<TDetails>,
|
| 496 |
+
options: ToolRenderResultOptions,
|
| 497 |
+
theme: Theme,
|
| 498 |
+
context: ToolRenderContext<TState, Static<TParams>>,
|
| 499 |
+
) => Component;
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
type AnyToolDefinition = ToolDefinition<any, any, any>;
|
| 503 |
+
|
| 504 |
+
/**
|
| 505 |
+
* Preserve parameter inference for standalone tool definitions.
|
| 506 |
+
*
|
| 507 |
+
* Use this when assigning a tool to a variable or passing it through arrays such
|
| 508 |
+
* as `customTools`, where contextual typing would otherwise widen params to
|
| 509 |
+
* `unknown`.
|
| 510 |
+
*/
|
| 511 |
+
export function defineTool<TParams extends TSchema, TDetails = unknown, TState = any>(
|
| 512 |
+
tool: ToolDefinition<TParams, TDetails, TState>,
|
| 513 |
+
): ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition {
|
| 514 |
+
return tool as ToolDefinition<TParams, TDetails, TState> & AnyToolDefinition;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
// ============================================================================
|
| 518 |
+
// Startup/Resource Events
|
| 519 |
+
// ============================================================================
|
| 520 |
+
|
| 521 |
+
export interface ProjectTrustEvent {
|
| 522 |
+
type: "project_trust";
|
| 523 |
+
cwd: string;
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
export type ProjectTrustEventDecision = "yes" | "no" | "undecided";
|
| 527 |
+
|
| 528 |
+
export interface ProjectTrustEventResult {
|
| 529 |
+
trusted: ProjectTrustEventDecision;
|
| 530 |
+
remember?: boolean;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
export interface ProjectTrustContext {
|
| 534 |
+
cwd: string;
|
| 535 |
+
mode: ExtensionMode;
|
| 536 |
+
hasUI: boolean;
|
| 537 |
+
ui: Pick<ExtensionUIContext, "select" | "confirm" | "input" | "notify">;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
export type ProjectTrustHandler = (
|
| 541 |
+
event: ProjectTrustEvent,
|
| 542 |
+
ctx: ProjectTrustContext,
|
| 543 |
+
) => Promise<ProjectTrustEventResult> | ProjectTrustEventResult;
|
| 544 |
+
|
| 545 |
+
/** Fired after session_start to allow extensions to provide additional resource paths. */
|
| 546 |
+
export interface ResourcesDiscoverEvent {
|
| 547 |
+
type: "resources_discover";
|
| 548 |
+
cwd: string;
|
| 549 |
+
reason: "startup" | "reload";
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
/** Result from resources_discover event handler */
|
| 553 |
+
export interface ResourcesDiscoverResult {
|
| 554 |
+
skillPaths?: string[];
|
| 555 |
+
promptPaths?: string[];
|
| 556 |
+
themePaths?: string[];
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
// ============================================================================
|
| 560 |
+
// Session Events
|
| 561 |
+
// ============================================================================
|
| 562 |
+
|
| 563 |
+
/** Fired when a session is started, loaded, or reloaded */
|
| 564 |
+
export interface SessionStartEvent {
|
| 565 |
+
type: "session_start";
|
| 566 |
+
/** Why this session start happened. */
|
| 567 |
+
reason: "startup" | "reload" | "new" | "resume" | "fork";
|
| 568 |
+
/** Previously active session file. Present for "new", "resume", and "fork". */
|
| 569 |
+
previousSessionFile?: string;
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
/** Fired when the current session metadata changes. */
|
| 573 |
+
export interface SessionInfoChangedEvent {
|
| 574 |
+
type: "session_info_changed";
|
| 575 |
+
/** Current normalized session name. Undefined when the name is cleared. */
|
| 576 |
+
name: string | undefined;
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
/** Fired before switching to another session (can be cancelled) */
|
| 580 |
+
export interface SessionBeforeSwitchEvent {
|
| 581 |
+
type: "session_before_switch";
|
| 582 |
+
reason: "new" | "resume";
|
| 583 |
+
targetSessionFile?: string;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
/** Fired before forking a session (can be cancelled) */
|
| 587 |
+
export interface SessionBeforeForkEvent {
|
| 588 |
+
type: "session_before_fork";
|
| 589 |
+
entryId: string;
|
| 590 |
+
position: "before" | "at";
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
/** Fired before context compaction (can be cancelled or customized) */
|
| 594 |
+
export interface SessionBeforeCompactEvent {
|
| 595 |
+
type: "session_before_compact";
|
| 596 |
+
preparation: CompactionPreparation;
|
| 597 |
+
branchEntries: SessionEntry[];
|
| 598 |
+
customInstructions?: string;
|
| 599 |
+
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
|
| 600 |
+
reason: "manual" | "threshold" | "overflow";
|
| 601 |
+
/** True when the aborted turn is retried after this compaction (overflow recovery) */
|
| 602 |
+
willRetry: boolean;
|
| 603 |
+
signal: AbortSignal;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
/** Fired after context compaction succeeds */
|
| 607 |
+
export interface SessionCompactEvent {
|
| 608 |
+
type: "session_compact";
|
| 609 |
+
compactionEntry: CompactionEntry;
|
| 610 |
+
fromExtension: boolean;
|
| 611 |
+
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
|
| 612 |
+
reason: "manual" | "threshold" | "overflow";
|
| 613 |
+
/** True when the aborted turn is retried after this compaction (overflow recovery) */
|
| 614 |
+
willRetry: boolean;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
/** Fired after context compaction fails or is aborted */
|
| 618 |
+
export interface SessionCompactFailedEvent {
|
| 619 |
+
type: "session_compact_failed";
|
| 620 |
+
/** What triggered the compaction: manual /compact, the context threshold, or context overflow recovery */
|
| 621 |
+
reason: "manual" | "threshold" | "overflow";
|
| 622 |
+
/** Error text when compaction failed for a non-abort reason. */
|
| 623 |
+
errorMessage?: string;
|
| 624 |
+
/** True when compaction was cancelled or aborted. */
|
| 625 |
+
aborted: boolean;
|
| 626 |
+
/** True when the aborted turn would have been retried after this compaction (overflow recovery) */
|
| 627 |
+
willRetry: boolean;
|
| 628 |
+
/** True when the failing compaction content came from a session_before_compact handler. */
|
| 629 |
+
fromExtension: boolean;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
/** Fired before an extension runtime is torn down due to quit, reload, or session replacement. */
|
| 633 |
+
export interface SessionShutdownEvent {
|
| 634 |
+
type: "session_shutdown";
|
| 635 |
+
reason: "quit" | "reload" | "new" | "resume" | "fork";
|
| 636 |
+
/** Destination session file when shutting down due to session replacement. */
|
| 637 |
+
targetSessionFile?: string;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
/** Preparation data for tree navigation */
|
| 641 |
+
export interface TreePreparation {
|
| 642 |
+
targetId: string;
|
| 643 |
+
oldLeafId: string | null;
|
| 644 |
+
commonAncestorId: string | null;
|
| 645 |
+
entriesToSummarize: SessionEntry[];
|
| 646 |
+
userWantsSummary: boolean;
|
| 647 |
+
/** Custom instructions for summarization */
|
| 648 |
+
customInstructions?: string;
|
| 649 |
+
/** If true, customInstructions replaces the default prompt instead of being appended */
|
| 650 |
+
replaceInstructions?: boolean;
|
| 651 |
+
/** Label to attach to the branch summary entry */
|
| 652 |
+
label?: string;
|
| 653 |
+
}
|
| 654 |
+
|
| 655 |
+
/** Fired before navigating in the session tree (can be cancelled) */
|
| 656 |
+
export interface SessionBeforeTreeEvent {
|
| 657 |
+
type: "session_before_tree";
|
| 658 |
+
preparation: TreePreparation;
|
| 659 |
+
signal: AbortSignal;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
/** Fired after navigating in the session tree */
|
| 663 |
+
export interface SessionTreeEvent {
|
| 664 |
+
type: "session_tree";
|
| 665 |
+
newLeafId: string | null;
|
| 666 |
+
oldLeafId: string | null;
|
| 667 |
+
summaryEntry?: BranchSummaryEntry;
|
| 668 |
+
fromExtension?: boolean;
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
export type SessionEvent =
|
| 672 |
+
| SessionStartEvent
|
| 673 |
+
| SessionInfoChangedEvent
|
| 674 |
+
| SessionBeforeSwitchEvent
|
| 675 |
+
| SessionBeforeForkEvent
|
| 676 |
+
| SessionBeforeCompactEvent
|
| 677 |
+
| SessionCompactEvent
|
| 678 |
+
| SessionCompactFailedEvent
|
| 679 |
+
| SessionShutdownEvent
|
| 680 |
+
| SessionBeforeTreeEvent
|
| 681 |
+
| SessionTreeEvent;
|
| 682 |
+
|
| 683 |
+
// ============================================================================
|
| 684 |
+
// Agent Events
|
| 685 |
+
// ============================================================================
|
| 686 |
+
|
| 687 |
+
/** Fired before each LLM call. Can modify messages. */
|
| 688 |
+
export interface ContextEvent {
|
| 689 |
+
type: "context";
|
| 690 |
+
messages: AgentMessage[];
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
/** Fired before a provider request is sent. Can replace the payload. */
|
| 694 |
+
export interface BeforeProviderRequestEvent {
|
| 695 |
+
type: "before_provider_request";
|
| 696 |
+
payload: unknown;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
/**
|
| 700 |
+
* Fired after request headers are assembled, before the provider HTTP call.
|
| 701 |
+
* Handlers mutate `headers` in place (e.g. to inject tracing/session headers);
|
| 702 |
+
* the return value is ignored. A `null` value deletes that header.
|
| 703 |
+
*/
|
| 704 |
+
export interface BeforeProviderHeadersEvent {
|
| 705 |
+
type: "before_provider_headers";
|
| 706 |
+
headers: ProviderHeaders;
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
/** Fired after a provider response is received and before the response stream is consumed. */
|
| 710 |
+
export interface AfterProviderResponseEvent {
|
| 711 |
+
type: "after_provider_response";
|
| 712 |
+
status: number;
|
| 713 |
+
headers: Record<string, string>;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
/** Fired after user submits prompt but before agent loop. */
|
| 717 |
+
export interface BeforeAgentStartEvent {
|
| 718 |
+
type: "before_agent_start";
|
| 719 |
+
/** The raw user prompt text (after expansion). */
|
| 720 |
+
prompt: string;
|
| 721 |
+
/** Images attached to the user prompt, if any. */
|
| 722 |
+
images?: ImageContent[];
|
| 723 |
+
/** The fully assembled system prompt string. */
|
| 724 |
+
systemPrompt: string;
|
| 725 |
+
/** Structured options used to build the system prompt. Extensions can inspect this to understand what Pi loaded without re-discovering resources. */
|
| 726 |
+
systemPromptOptions: BuildSystemPromptOptions;
|
| 727 |
+
}
|
| 728 |
+
|
| 729 |
+
/** Fired when an agent loop starts */
|
| 730 |
+
export interface AgentStartEvent {
|
| 731 |
+
type: "agent_start";
|
| 732 |
+
}
|
| 733 |
+
|
| 734 |
+
/** Fired when an agent loop ends */
|
| 735 |
+
export interface AgentEndEvent {
|
| 736 |
+
type: "agent_end";
|
| 737 |
+
messages: AgentMessage[];
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
/** Fired after an agent run has fully settled and no automatic retry, compaction, or queued continuation will run. */
|
| 741 |
+
export interface AgentSettledEvent {
|
| 742 |
+
type: "agent_settled";
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
export type UIPromptKind = "select" | "confirm" | "input" | "editor" | "custom";
|
| 746 |
+
|
| 747 |
+
/** Fired when Pi starts waiting on a blocking user-facing extension UI prompt. */
|
| 748 |
+
export interface UIPromptStartEvent {
|
| 749 |
+
type: "ui_prompt_start";
|
| 750 |
+
reason: "ui_prompt";
|
| 751 |
+
kind: UIPromptKind;
|
| 752 |
+
title?: string;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
/** Fired when Pi is no longer waiting on a blocking user-facing extension UI prompt. */
|
| 756 |
+
export interface UIPromptEndEvent {
|
| 757 |
+
type: "ui_prompt_end";
|
| 758 |
+
reason: "ui_prompt";
|
| 759 |
+
kind: UIPromptKind;
|
| 760 |
+
title?: string;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
/** Fired at the start of each turn */
|
| 764 |
+
export interface TurnStartEvent {
|
| 765 |
+
type: "turn_start";
|
| 766 |
+
turnIndex: number;
|
| 767 |
+
timestamp: number;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
/** Fired at the end of each turn */
|
| 771 |
+
export interface TurnEndEvent {
|
| 772 |
+
type: "turn_end";
|
| 773 |
+
turnIndex: number;
|
| 774 |
+
message: AgentMessage;
|
| 775 |
+
toolResults: ToolResultMessage[];
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
/** Fired when a message starts (user, assistant, or toolResult) */
|
| 779 |
+
export interface MessageStartEvent {
|
| 780 |
+
type: "message_start";
|
| 781 |
+
message: AgentMessage;
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
/** Fired during assistant message streaming with token-by-token updates */
|
| 785 |
+
export interface MessageUpdateEvent {
|
| 786 |
+
type: "message_update";
|
| 787 |
+
message: AgentMessage;
|
| 788 |
+
assistantMessageEvent: AssistantMessageEvent;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
/** Fired when a message ends */
|
| 792 |
+
export interface MessageEndEvent {
|
| 793 |
+
type: "message_end";
|
| 794 |
+
message: AgentMessage;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
/** Fired when a tool starts executing */
|
| 798 |
+
export interface ToolExecutionStartEvent {
|
| 799 |
+
type: "tool_execution_start";
|
| 800 |
+
toolCallId: string;
|
| 801 |
+
toolName: string;
|
| 802 |
+
args: any;
|
| 803 |
+
}
|
| 804 |
+
|
| 805 |
+
/** Fired during tool execution with partial/streaming output */
|
| 806 |
+
export interface ToolExecutionUpdateEvent {
|
| 807 |
+
type: "tool_execution_update";
|
| 808 |
+
toolCallId: string;
|
| 809 |
+
toolName: string;
|
| 810 |
+
args: any;
|
| 811 |
+
partialResult: any;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
/** Fired when a tool finishes executing */
|
| 815 |
+
export interface ToolExecutionEndEvent {
|
| 816 |
+
type: "tool_execution_end";
|
| 817 |
+
toolCallId: string;
|
| 818 |
+
toolName: string;
|
| 819 |
+
result: any;
|
| 820 |
+
isError: boolean;
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
// ============================================================================
|
| 824 |
+
// Model Events
|
| 825 |
+
// ============================================================================
|
| 826 |
+
|
| 827 |
+
export type ModelSelectSource = "set" | "cycle" | "restore";
|
| 828 |
+
|
| 829 |
+
/** Fired when a new model is selected */
|
| 830 |
+
export interface ModelSelectEvent {
|
| 831 |
+
type: "model_select";
|
| 832 |
+
model: Model<any>;
|
| 833 |
+
previousModel: Model<any> | undefined;
|
| 834 |
+
source: ModelSelectSource;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
/** Fired when a new thinking level is selected */
|
| 838 |
+
export interface ThinkingLevelSelectEvent {
|
| 839 |
+
type: "thinking_level_select";
|
| 840 |
+
level: ThinkingLevel;
|
| 841 |
+
previousLevel: ThinkingLevel;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
// ============================================================================
|
| 845 |
+
// User Bash Events
|
| 846 |
+
// ============================================================================
|
| 847 |
+
|
| 848 |
+
/** Fired when user executes a bash command via ! or !! prefix */
|
| 849 |
+
export interface UserBashEvent {
|
| 850 |
+
type: "user_bash";
|
| 851 |
+
/** The command to execute */
|
| 852 |
+
command: string;
|
| 853 |
+
/** True if !! prefix was used (excluded from LLM context) */
|
| 854 |
+
excludeFromContext: boolean;
|
| 855 |
+
/** Current working directory */
|
| 856 |
+
cwd: string;
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// ============================================================================
|
| 860 |
+
// Input Events
|
| 861 |
+
// ============================================================================
|
| 862 |
+
|
| 863 |
+
/** Source of user input */
|
| 864 |
+
export type InputSource = "interactive" | "rpc" | "extension";
|
| 865 |
+
|
| 866 |
+
/** Fired when user input is received, before agent processing */
|
| 867 |
+
export interface InputEvent {
|
| 868 |
+
type: "input";
|
| 869 |
+
/** The input text */
|
| 870 |
+
text: string;
|
| 871 |
+
/** Attached images, if any */
|
| 872 |
+
images?: ImageContent[];
|
| 873 |
+
/** Where the input came from */
|
| 874 |
+
source: InputSource;
|
| 875 |
+
/** How the input will be delivered during streaming, or undefined when idle */
|
| 876 |
+
streamingBehavior?: "steer" | "followUp";
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
/** Result from input event handler */
|
| 880 |
+
export type InputEventResult =
|
| 881 |
+
| { action: "continue" }
|
| 882 |
+
| { action: "transform"; text: string; images?: ImageContent[] }
|
| 883 |
+
| { action: "handled" };
|
| 884 |
+
|
| 885 |
+
// ============================================================================
|
| 886 |
+
// Tool Events
|
| 887 |
+
// ============================================================================
|
| 888 |
+
|
| 889 |
+
interface ToolCallEventBase {
|
| 890 |
+
type: "tool_call";
|
| 891 |
+
toolCallId: string;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
export interface BashToolCallEvent extends ToolCallEventBase {
|
| 895 |
+
toolName: "bash";
|
| 896 |
+
input: BashToolInput;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
export interface PowerShellToolCallEvent extends ToolCallEventBase {
|
| 900 |
+
toolName: "powershell";
|
| 901 |
+
input: PowerShellToolInput;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
export interface ReadToolCallEvent extends ToolCallEventBase {
|
| 905 |
+
toolName: "read";
|
| 906 |
+
input: ReadToolInput;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
export interface EditToolCallEvent extends ToolCallEventBase {
|
| 910 |
+
toolName: "edit";
|
| 911 |
+
input: EditToolInput;
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
export interface WriteToolCallEvent extends ToolCallEventBase {
|
| 915 |
+
toolName: "write";
|
| 916 |
+
input: WriteToolInput;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
export interface GrepToolCallEvent extends ToolCallEventBase {
|
| 920 |
+
toolName: "grep";
|
| 921 |
+
input: GrepToolInput;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
export interface FindToolCallEvent extends ToolCallEventBase {
|
| 925 |
+
toolName: "find";
|
| 926 |
+
input: FindToolInput;
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
export interface LsToolCallEvent extends ToolCallEventBase {
|
| 930 |
+
toolName: "ls";
|
| 931 |
+
input: LsToolInput;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
export interface CustomToolCallEvent extends ToolCallEventBase {
|
| 935 |
+
toolName: string;
|
| 936 |
+
input: Record<string, unknown>;
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
/**
|
| 940 |
+
* Fired before a tool executes. Can block.
|
| 941 |
+
*
|
| 942 |
+
* `event.input` is mutable. Mutate it in place to patch tool arguments before execution.
|
| 943 |
+
* Later `tool_call` handlers see earlier mutations. No re-validation is performed after mutation.
|
| 944 |
+
*/
|
| 945 |
+
export type ToolCallEvent =
|
| 946 |
+
| BashToolCallEvent
|
| 947 |
+
| PowerShellToolCallEvent
|
| 948 |
+
| ReadToolCallEvent
|
| 949 |
+
| EditToolCallEvent
|
| 950 |
+
| WriteToolCallEvent
|
| 951 |
+
| GrepToolCallEvent
|
| 952 |
+
| FindToolCallEvent
|
| 953 |
+
| LsToolCallEvent
|
| 954 |
+
| CustomToolCallEvent;
|
| 955 |
+
|
| 956 |
+
interface ToolResultEventBase {
|
| 957 |
+
type: "tool_result";
|
| 958 |
+
toolCallId: string;
|
| 959 |
+
input: Record<string, unknown>;
|
| 960 |
+
content: (TextContent | ImageContent)[];
|
| 961 |
+
isError: boolean;
|
| 962 |
+
/** Usage from the tool execution itself, if available. */
|
| 963 |
+
usage?: Usage;
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
export interface BashToolResultEvent extends ToolResultEventBase {
|
| 967 |
+
toolName: "bash";
|
| 968 |
+
details: BashToolDetails | undefined;
|
| 969 |
+
}
|
| 970 |
+
|
| 971 |
+
export interface PowerShellToolResultEvent extends ToolResultEventBase {
|
| 972 |
+
toolName: "powershell";
|
| 973 |
+
details: PowerShellToolDetails | undefined;
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
export interface ReadToolResultEvent extends ToolResultEventBase {
|
| 977 |
+
toolName: "read";
|
| 978 |
+
details: ReadToolDetails | undefined;
|
| 979 |
+
}
|
| 980 |
+
|
| 981 |
+
export interface EditToolResultEvent extends ToolResultEventBase {
|
| 982 |
+
toolName: "edit";
|
| 983 |
+
details: EditToolDetails | undefined;
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
export interface WriteToolResultEvent extends ToolResultEventBase {
|
| 987 |
+
toolName: "write";
|
| 988 |
+
details: undefined;
|
| 989 |
+
}
|
| 990 |
+
|
| 991 |
+
export interface GrepToolResultEvent extends ToolResultEventBase {
|
| 992 |
+
toolName: "grep";
|
| 993 |
+
details: GrepToolDetails | undefined;
|
| 994 |
+
}
|
| 995 |
+
|
| 996 |
+
export interface FindToolResultEvent extends ToolResultEventBase {
|
| 997 |
+
toolName: "find";
|
| 998 |
+
details: FindToolDetails | undefined;
|
| 999 |
+
}
|
| 1000 |
+
|
| 1001 |
+
export interface LsToolResultEvent extends ToolResultEventBase {
|
| 1002 |
+
toolName: "ls";
|
| 1003 |
+
details: LsToolDetails | undefined;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
export interface CustomToolResultEvent extends ToolResultEventBase {
|
| 1007 |
+
toolName: string;
|
| 1008 |
+
details: unknown;
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
/** Fired after a tool executes. Can modify result. */
|
| 1012 |
+
export type ToolResultEvent =
|
| 1013 |
+
| BashToolResultEvent
|
| 1014 |
+
| PowerShellToolResultEvent
|
| 1015 |
+
| ReadToolResultEvent
|
| 1016 |
+
| EditToolResultEvent
|
| 1017 |
+
| WriteToolResultEvent
|
| 1018 |
+
| GrepToolResultEvent
|
| 1019 |
+
| FindToolResultEvent
|
| 1020 |
+
| LsToolResultEvent
|
| 1021 |
+
| CustomToolResultEvent;
|
| 1022 |
+
|
| 1023 |
+
// Type guards for ToolResultEvent
|
| 1024 |
+
export function isBashToolResult(e: ToolResultEvent): e is BashToolResultEvent {
|
| 1025 |
+
return e.toolName === "bash";
|
| 1026 |
+
}
|
| 1027 |
+
export function isPowerShellToolResult(e: ToolResultEvent): e is PowerShellToolResultEvent {
|
| 1028 |
+
return e.toolName === "powershell";
|
| 1029 |
+
}
|
| 1030 |
+
export function isReadToolResult(e: ToolResultEvent): e is ReadToolResultEvent {
|
| 1031 |
+
return e.toolName === "read";
|
| 1032 |
+
}
|
| 1033 |
+
export function isEditToolResult(e: ToolResultEvent): e is EditToolResultEvent {
|
| 1034 |
+
return e.toolName === "edit";
|
| 1035 |
+
}
|
| 1036 |
+
export function isWriteToolResult(e: ToolResultEvent): e is WriteToolResultEvent {
|
| 1037 |
+
return e.toolName === "write";
|
| 1038 |
+
}
|
| 1039 |
+
export function isGrepToolResult(e: ToolResultEvent): e is GrepToolResultEvent {
|
| 1040 |
+
return e.toolName === "grep";
|
| 1041 |
+
}
|
| 1042 |
+
export function isFindToolResult(e: ToolResultEvent): e is FindToolResultEvent {
|
| 1043 |
+
return e.toolName === "find";
|
| 1044 |
+
}
|
| 1045 |
+
export function isLsToolResult(e: ToolResultEvent): e is LsToolResultEvent {
|
| 1046 |
+
return e.toolName === "ls";
|
| 1047 |
+
}
|
| 1048 |
+
|
| 1049 |
+
/**
|
| 1050 |
+
* Type guard for narrowing ToolCallEvent by tool name.
|
| 1051 |
+
*
|
| 1052 |
+
* Built-in tools narrow automatically (no type params needed):
|
| 1053 |
+
* ```ts
|
| 1054 |
+
* if (isToolCallEventType("bash", event)) {
|
| 1055 |
+
* event.input.command; // string
|
| 1056 |
+
* }
|
| 1057 |
+
* ```
|
| 1058 |
+
*
|
| 1059 |
+
* Custom tools require explicit type parameters:
|
| 1060 |
+
* ```ts
|
| 1061 |
+
* if (isToolCallEventType<"my_tool", MyToolInput>("my_tool", event)) {
|
| 1062 |
+
* event.input.action; // typed
|
| 1063 |
+
* }
|
| 1064 |
+
* ```
|
| 1065 |
+
*
|
| 1066 |
+
* Note: Direct narrowing via `event.toolName === "bash"` doesn't work because
|
| 1067 |
+
* CustomToolCallEvent.toolName is `string` which overlaps with all literals.
|
| 1068 |
+
*/
|
| 1069 |
+
export function isToolCallEventType(toolName: "bash", event: ToolCallEvent): event is BashToolCallEvent;
|
| 1070 |
+
export function isToolCallEventType(toolName: "powershell", event: ToolCallEvent): event is PowerShellToolCallEvent;
|
| 1071 |
+
export function isToolCallEventType(toolName: "read", event: ToolCallEvent): event is ReadToolCallEvent;
|
| 1072 |
+
export function isToolCallEventType(toolName: "edit", event: ToolCallEvent): event is EditToolCallEvent;
|
| 1073 |
+
export function isToolCallEventType(toolName: "write", event: ToolCallEvent): event is WriteToolCallEvent;
|
| 1074 |
+
export function isToolCallEventType(toolName: "grep", event: ToolCallEvent): event is GrepToolCallEvent;
|
| 1075 |
+
export function isToolCallEventType(toolName: "find", event: ToolCallEvent): event is FindToolCallEvent;
|
| 1076 |
+
export function isToolCallEventType(toolName: "ls", event: ToolCallEvent): event is LsToolCallEvent;
|
| 1077 |
+
export function isToolCallEventType<TName extends string, TInput extends Record<string, unknown>>(
|
| 1078 |
+
toolName: TName,
|
| 1079 |
+
event: ToolCallEvent,
|
| 1080 |
+
): event is ToolCallEvent & { toolName: TName; input: TInput };
|
| 1081 |
+
export function isToolCallEventType(toolName: string, event: ToolCallEvent): boolean {
|
| 1082 |
+
return event.toolName === toolName;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
/** Union of all event types */
|
| 1086 |
+
export type ExtensionEvent =
|
| 1087 |
+
| ProjectTrustEvent
|
| 1088 |
+
| ResourcesDiscoverEvent
|
| 1089 |
+
| SessionEvent
|
| 1090 |
+
| ContextEvent
|
| 1091 |
+
| BeforeProviderRequestEvent
|
| 1092 |
+
| BeforeProviderHeadersEvent
|
| 1093 |
+
| AfterProviderResponseEvent
|
| 1094 |
+
| BeforeAgentStartEvent
|
| 1095 |
+
| AgentStartEvent
|
| 1096 |
+
| AgentEndEvent
|
| 1097 |
+
| AgentSettledEvent
|
| 1098 |
+
| UIPromptStartEvent
|
| 1099 |
+
| UIPromptEndEvent
|
| 1100 |
+
| TurnStartEvent
|
| 1101 |
+
| TurnEndEvent
|
| 1102 |
+
| MessageStartEvent
|
| 1103 |
+
| MessageUpdateEvent
|
| 1104 |
+
| MessageEndEvent
|
| 1105 |
+
| ToolExecutionStartEvent
|
| 1106 |
+
| ToolExecutionUpdateEvent
|
| 1107 |
+
| ToolExecutionEndEvent
|
| 1108 |
+
| ModelSelectEvent
|
| 1109 |
+
| ThinkingLevelSelectEvent
|
| 1110 |
+
| UserBashEvent
|
| 1111 |
+
| InputEvent
|
| 1112 |
+
| ToolCallEvent
|
| 1113 |
+
| ToolResultEvent;
|
| 1114 |
+
|
| 1115 |
+
// ============================================================================
|
| 1116 |
+
// Event Results
|
| 1117 |
+
// ============================================================================
|
| 1118 |
+
|
| 1119 |
+
export interface ContextEventResult {
|
| 1120 |
+
messages?: AgentMessage[];
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
export type BeforeProviderRequestEventResult = unknown;
|
| 1124 |
+
|
| 1125 |
+
export interface ToolCallEventResult {
|
| 1126 |
+
/** Block tool execution. To modify arguments, mutate `event.input` in place instead. */
|
| 1127 |
+
block?: boolean;
|
| 1128 |
+
reason?: string;
|
| 1129 |
+
/**
|
| 1130 |
+
* Hint that the agent should stop after the current tool batch when this call is blocked.
|
| 1131 |
+
* Early termination only happens when every finalized tool result in the batch sets this to true.
|
| 1132 |
+
*/
|
| 1133 |
+
terminate?: boolean;
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
/** Result from user_bash event handler */
|
| 1137 |
+
export interface UserBashEventResult {
|
| 1138 |
+
/** Custom operations to use for execution */
|
| 1139 |
+
operations?: BashOperations;
|
| 1140 |
+
/** Full replacement: extension handled execution, use this result */
|
| 1141 |
+
result?: BashResult;
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
export interface ToolResultEventResult {
|
| 1145 |
+
content?: (TextContent | ImageContent)[];
|
| 1146 |
+
details?: unknown;
|
| 1147 |
+
isError?: boolean;
|
| 1148 |
+
usage?: Usage;
|
| 1149 |
+
}
|
| 1150 |
+
|
| 1151 |
+
export interface MessageEndEventResult {
|
| 1152 |
+
/** Replace the finalized message. The replacement must keep the original message role. */
|
| 1153 |
+
message?: AgentMessage;
|
| 1154 |
+
}
|
| 1155 |
+
|
| 1156 |
+
export interface BeforeAgentStartEventResult {
|
| 1157 |
+
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
|
| 1158 |
+
/** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
|
| 1159 |
+
systemPrompt?: string;
|
| 1160 |
+
}
|
| 1161 |
+
|
| 1162 |
+
export interface SessionBeforeSwitchResult {
|
| 1163 |
+
cancel?: boolean;
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
export interface SessionBeforeForkResult {
|
| 1167 |
+
cancel?: boolean;
|
| 1168 |
+
skipConversationRestore?: boolean;
|
| 1169 |
+
}
|
| 1170 |
+
|
| 1171 |
+
export interface SessionBeforeCompactResult {
|
| 1172 |
+
cancel?: boolean;
|
| 1173 |
+
compaction?: CompactionResult;
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
export interface SessionBeforeTreeResult {
|
| 1177 |
+
cancel?: boolean;
|
| 1178 |
+
summary?: {
|
| 1179 |
+
summary: string;
|
| 1180 |
+
details?: unknown;
|
| 1181 |
+
usage?: Usage;
|
| 1182 |
+
};
|
| 1183 |
+
/** Override custom instructions for summarization */
|
| 1184 |
+
customInstructions?: string;
|
| 1185 |
+
/** Override whether customInstructions replaces the default prompt */
|
| 1186 |
+
replaceInstructions?: boolean;
|
| 1187 |
+
/** Override label to attach to the branch summary entry */
|
| 1188 |
+
label?: string;
|
| 1189 |
+
}
|
| 1190 |
+
|
| 1191 |
+
// ============================================================================
|
| 1192 |
+
// Message and Entry Rendering
|
| 1193 |
+
// ============================================================================
|
| 1194 |
+
|
| 1195 |
+
export interface MessageRenderOptions {
|
| 1196 |
+
expanded: boolean;
|
| 1197 |
+
/** Horizontal padding configured by the outputPad setting. */
|
| 1198 |
+
outputPad: number;
|
| 1199 |
+
}
|
| 1200 |
+
|
| 1201 |
+
export interface MarkdownTransformContext {
|
| 1202 |
+
messageType: "user" | "assistant" | "assistant-thinking";
|
| 1203 |
+
isStreaming: boolean;
|
| 1204 |
+
availableWidth: number;
|
| 1205 |
+
}
|
| 1206 |
+
|
| 1207 |
+
export type MarkdownTransformer = (markdown: string, context: MarkdownTransformContext) => string;
|
| 1208 |
+
|
| 1209 |
+
export interface EntryRenderOptions {
|
| 1210 |
+
expanded: boolean;
|
| 1211 |
+
}
|
| 1212 |
+
|
| 1213 |
+
export type MessageRenderer<T = unknown> = (
|
| 1214 |
+
message: CustomMessage<T>,
|
| 1215 |
+
options: MessageRenderOptions,
|
| 1216 |
+
theme: Theme,
|
| 1217 |
+
) => Component | undefined;
|
| 1218 |
+
|
| 1219 |
+
export type EntryRenderer<T = unknown> = (
|
| 1220 |
+
entry: CustomEntry<T>,
|
| 1221 |
+
options: EntryRenderOptions,
|
| 1222 |
+
theme: Theme,
|
| 1223 |
+
) => Component | undefined;
|
| 1224 |
+
|
| 1225 |
+
// ============================================================================
|
| 1226 |
+
// Command Registration
|
| 1227 |
+
// ============================================================================
|
| 1228 |
+
|
| 1229 |
+
export interface RegisteredCommand {
|
| 1230 |
+
name: string;
|
| 1231 |
+
sourceInfo: SourceInfo;
|
| 1232 |
+
description?: string;
|
| 1233 |
+
getArgumentCompletions?: (argumentPrefix: string) => AutocompleteItem[] | null | Promise<AutocompleteItem[] | null>;
|
| 1234 |
+
handler: (args: string, ctx: ExtensionCommandContext) => Promise<void>;
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
export interface ResolvedCommand extends RegisteredCommand {
|
| 1238 |
+
invocationName: string;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
// ============================================================================
|
| 1242 |
+
// Extension API
|
| 1243 |
+
// ============================================================================
|
| 1244 |
+
|
| 1245 |
+
/** Handler function type for events */
|
| 1246 |
+
// biome-ignore lint/suspicious/noConfusingVoidType: void allows bare return statements
|
| 1247 |
+
export type ExtensionHandler<E, R = undefined> = (event: E, ctx: ExtensionContext) => Promise<R | void> | R | void;
|
| 1248 |
+
|
| 1249 |
+
/**
|
| 1250 |
+
* ExtensionAPI passed to extension factory functions.
|
| 1251 |
+
*/
|
| 1252 |
+
export interface ExtensionAPI {
|
| 1253 |
+
// =========================================================================
|
| 1254 |
+
// Event Subscription
|
| 1255 |
+
// =========================================================================
|
| 1256 |
+
|
| 1257 |
+
on(event: "project_trust", handler: ProjectTrustHandler): void;
|
| 1258 |
+
on(event: "resources_discover", handler: ExtensionHandler<ResourcesDiscoverEvent, ResourcesDiscoverResult>): void;
|
| 1259 |
+
on(event: "session_start", handler: ExtensionHandler<SessionStartEvent>): void;
|
| 1260 |
+
on(event: "session_info_changed", handler: ExtensionHandler<SessionInfoChangedEvent>): void;
|
| 1261 |
+
on(
|
| 1262 |
+
event: "session_before_switch",
|
| 1263 |
+
handler: ExtensionHandler<SessionBeforeSwitchEvent, SessionBeforeSwitchResult>,
|
| 1264 |
+
): void;
|
| 1265 |
+
on(event: "session_before_fork", handler: ExtensionHandler<SessionBeforeForkEvent, SessionBeforeForkResult>): void;
|
| 1266 |
+
on(
|
| 1267 |
+
event: "session_before_compact",
|
| 1268 |
+
handler: ExtensionHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
|
| 1269 |
+
): void;
|
| 1270 |
+
on(event: "session_compact", handler: ExtensionHandler<SessionCompactEvent>): void;
|
| 1271 |
+
on(event: "session_compact_failed", handler: ExtensionHandler<SessionCompactFailedEvent>): void;
|
| 1272 |
+
on(event: "session_shutdown", handler: ExtensionHandler<SessionShutdownEvent>): void;
|
| 1273 |
+
on(event: "session_before_tree", handler: ExtensionHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;
|
| 1274 |
+
on(event: "session_tree", handler: ExtensionHandler<SessionTreeEvent>): void;
|
| 1275 |
+
on(event: "context", handler: ExtensionHandler<ContextEvent, ContextEventResult>): void;
|
| 1276 |
+
on(
|
| 1277 |
+
event: "before_provider_request",
|
| 1278 |
+
handler: ExtensionHandler<BeforeProviderRequestEvent, BeforeProviderRequestEventResult>,
|
| 1279 |
+
): void;
|
| 1280 |
+
on(event: "before_provider_headers", handler: ExtensionHandler<BeforeProviderHeadersEvent>): void;
|
| 1281 |
+
on(event: "after_provider_response", handler: ExtensionHandler<AfterProviderResponseEvent>): void;
|
| 1282 |
+
on(event: "before_agent_start", handler: ExtensionHandler<BeforeAgentStartEvent, BeforeAgentStartEventResult>): void;
|
| 1283 |
+
on(event: "agent_start", handler: ExtensionHandler<AgentStartEvent>): void;
|
| 1284 |
+
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
| 1285 |
+
on(event: "agent_settled", handler: ExtensionHandler<AgentSettledEvent>): void;
|
| 1286 |
+
on(event: "ui_prompt_start", handler: ExtensionHandler<UIPromptStartEvent>): void;
|
| 1287 |
+
on(event: "ui_prompt_end", handler: ExtensionHandler<UIPromptEndEvent>): void;
|
| 1288 |
+
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
| 1289 |
+
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
| 1290 |
+
on(event: "message_start", handler: ExtensionHandler<MessageStartEvent>): void;
|
| 1291 |
+
on(event: "message_update", handler: ExtensionHandler<MessageUpdateEvent>): void;
|
| 1292 |
+
on(event: "message_end", handler: ExtensionHandler<MessageEndEvent, MessageEndEventResult>): void;
|
| 1293 |
+
on(event: "tool_execution_start", handler: ExtensionHandler<ToolExecutionStartEvent>): void;
|
| 1294 |
+
on(event: "tool_execution_update", handler: ExtensionHandler<ToolExecutionUpdateEvent>): void;
|
| 1295 |
+
on(event: "tool_execution_end", handler: ExtensionHandler<ToolExecutionEndEvent>): void;
|
| 1296 |
+
on(event: "model_select", handler: ExtensionHandler<ModelSelectEvent>): void;
|
| 1297 |
+
on(event: "thinking_level_select", handler: ExtensionHandler<ThinkingLevelSelectEvent>): void;
|
| 1298 |
+
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
| 1299 |
+
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|
| 1300 |
+
on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
|
| 1301 |
+
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
|
| 1302 |
+
|
| 1303 |
+
// =========================================================================
|
| 1304 |
+
// Tool Registration
|
| 1305 |
+
// =========================================================================
|
| 1306 |
+
|
| 1307 |
+
/** Register a tool that the LLM can call. */
|
| 1308 |
+
registerTool<TParams extends TSchema = TSchema, TDetails = unknown, TState = any>(
|
| 1309 |
+
tool: ToolDefinition<TParams, TDetails, TState>,
|
| 1310 |
+
): void;
|
| 1311 |
+
|
| 1312 |
+
// =========================================================================
|
| 1313 |
+
// Command, Shortcut, Flag Registration
|
| 1314 |
+
// =========================================================================
|
| 1315 |
+
|
| 1316 |
+
/** Register a custom command. */
|
| 1317 |
+
registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">): void;
|
| 1318 |
+
|
| 1319 |
+
/** Register a keyboard shortcut. */
|
| 1320 |
+
registerShortcut(
|
| 1321 |
+
shortcut: KeyId,
|
| 1322 |
+
options: {
|
| 1323 |
+
description?: string;
|
| 1324 |
+
handler: (ctx: ExtensionContext) => Promise<void> | void;
|
| 1325 |
+
},
|
| 1326 |
+
): void;
|
| 1327 |
+
|
| 1328 |
+
/** Register a CLI flag. */
|
| 1329 |
+
registerFlag(
|
| 1330 |
+
name: string,
|
| 1331 |
+
options:
|
| 1332 |
+
| {
|
| 1333 |
+
description?: string;
|
| 1334 |
+
type: "boolean";
|
| 1335 |
+
default?: boolean;
|
| 1336 |
+
}
|
| 1337 |
+
| {
|
| 1338 |
+
description?: string;
|
| 1339 |
+
type: "string";
|
| 1340 |
+
default?: string;
|
| 1341 |
+
},
|
| 1342 |
+
): void;
|
| 1343 |
+
|
| 1344 |
+
/** Get the value of a registered CLI flag. */
|
| 1345 |
+
getFlag(name: string): boolean | string | undefined;
|
| 1346 |
+
|
| 1347 |
+
// =========================================================================
|
| 1348 |
+
// Message Rendering
|
| 1349 |
+
// =========================================================================
|
| 1350 |
+
|
| 1351 |
+
/** Register a custom renderer for CustomMessageEntry. */
|
| 1352 |
+
registerMessageRenderer<T = unknown>(customType: string, renderer: MessageRenderer<T>): void;
|
| 1353 |
+
|
| 1354 |
+
/** Register a transformer for user and assistant Markdown before Pi renders it in the interactive transcript. */
|
| 1355 |
+
registerMarkdownTransformer(transformer: MarkdownTransformer): void;
|
| 1356 |
+
|
| 1357 |
+
/** Register a custom renderer for CustomEntry. Custom entries do not participate in LLM context. */
|
| 1358 |
+
registerEntryRenderer<T = unknown>(customType: string, renderer: EntryRenderer<T>): void;
|
| 1359 |
+
|
| 1360 |
+
// =========================================================================
|
| 1361 |
+
// Actions
|
| 1362 |
+
// =========================================================================
|
| 1363 |
+
|
| 1364 |
+
/** Send a custom message to the session. */
|
| 1365 |
+
sendMessage<T = unknown>(
|
| 1366 |
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
| 1367 |
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
| 1368 |
+
): void;
|
| 1369 |
+
|
| 1370 |
+
/**
|
| 1371 |
+
* Send a user message to the agent. Always triggers a turn.
|
| 1372 |
+
* When the agent is streaming, use deliverAs to specify how to queue the message.
|
| 1373 |
+
* Set expandPromptTemplates to dispatch extension commands and expand skill commands and prompt templates.
|
| 1374 |
+
*/
|
| 1375 |
+
sendUserMessage(
|
| 1376 |
+
content: string | (TextContent | ImageContent)[],
|
| 1377 |
+
options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
|
| 1378 |
+
): void;
|
| 1379 |
+
|
| 1380 |
+
/** Append a custom entry to the session for state persistence (not sent to LLM). */
|
| 1381 |
+
appendEntry<T = unknown>(customType: string, data?: T): void;
|
| 1382 |
+
|
| 1383 |
+
// =========================================================================
|
| 1384 |
+
// Session Metadata
|
| 1385 |
+
// =========================================================================
|
| 1386 |
+
|
| 1387 |
+
/** Set the session display name (shown in session selector). */
|
| 1388 |
+
setSessionName(name: string): void;
|
| 1389 |
+
|
| 1390 |
+
/** Get the current session name, if set. */
|
| 1391 |
+
getSessionName(): string | undefined;
|
| 1392 |
+
|
| 1393 |
+
/** Set or clear a label on an entry. Labels are user-defined markers for bookmarking/navigation. */
|
| 1394 |
+
setLabel(entryId: string, label: string | undefined): void;
|
| 1395 |
+
|
| 1396 |
+
/** Execute a shell command. */
|
| 1397 |
+
exec(command: string, args: string[], options?: ExecOptions): Promise<ExecResult>;
|
| 1398 |
+
|
| 1399 |
+
/** Get the list of currently active tool names. */
|
| 1400 |
+
getActiveTools(): string[];
|
| 1401 |
+
|
| 1402 |
+
/** Get all configured tools with parameter schema, prompt guidelines, and source metadata. */
|
| 1403 |
+
getAllTools(): ToolInfo[];
|
| 1404 |
+
|
| 1405 |
+
/** Set the active tools by name. */
|
| 1406 |
+
setActiveTools(toolNames: string[]): void;
|
| 1407 |
+
|
| 1408 |
+
/** Get available slash commands in the current session. */
|
| 1409 |
+
getCommands(): SlashCommandInfo[];
|
| 1410 |
+
|
| 1411 |
+
// =========================================================================
|
| 1412 |
+
// Model and Thinking Level
|
| 1413 |
+
// =========================================================================
|
| 1414 |
+
|
| 1415 |
+
/**
|
| 1416 |
+
* Set the model for the current session without changing the configured default for new sessions.
|
| 1417 |
+
* Returns false if authentication is not configured for the model's provider.
|
| 1418 |
+
*/
|
| 1419 |
+
setModel(model: Model<any>): Promise<boolean>;
|
| 1420 |
+
|
| 1421 |
+
/** Get current thinking level. */
|
| 1422 |
+
getThinkingLevel(): ThinkingLevel;
|
| 1423 |
+
|
| 1424 |
+
/**
|
| 1425 |
+
* Set the thinking level (clamped to model capabilities) for the current session without changing the configured default
|
| 1426 |
+
* for new sessions.
|
| 1427 |
+
*/
|
| 1428 |
+
setThinkingLevel(level: ThinkingLevel): void;
|
| 1429 |
+
|
| 1430 |
+
// =========================================================================
|
| 1431 |
+
// Provider Registration
|
| 1432 |
+
// =========================================================================
|
| 1433 |
+
|
| 1434 |
+
/**
|
| 1435 |
+
* Register or override a model provider.
|
| 1436 |
+
*
|
| 1437 |
+
* If `models` is provided: replaces all existing models for this provider.
|
| 1438 |
+
* If only `baseUrl` is provided: overrides the URL for existing models.
|
| 1439 |
+
* If `oauth` is provided: registers OAuth provider for /login support.
|
| 1440 |
+
* If `streamSimple` is provided: registers a custom API stream handler.
|
| 1441 |
+
*
|
| 1442 |
+
* During initial extension load this call is queued and applied once the
|
| 1443 |
+
* runner has bound its context. After that it takes effect immediately, so
|
| 1444 |
+
* it is safe to call from command handlers or event callbacks without
|
| 1445 |
+
* requiring a `/reload`.
|
| 1446 |
+
*
|
| 1447 |
+
* @example
|
| 1448 |
+
* // Register a new provider with custom models
|
| 1449 |
+
* pi.registerProvider("my-proxy", {
|
| 1450 |
+
* baseUrl: "https://proxy.example.com",
|
| 1451 |
+
* apiKey: "$PROXY_API_KEY",
|
| 1452 |
+
* api: "anthropic-messages",
|
| 1453 |
+
* models: [
|
| 1454 |
+
* {
|
| 1455 |
+
* id: "claude-sonnet-4-20250514",
|
| 1456 |
+
* name: "Claude 4 Sonnet (proxy)",
|
| 1457 |
+
* reasoning: false,
|
| 1458 |
+
* input: ["text", "image"],
|
| 1459 |
+
* cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
| 1460 |
+
* contextWindow: 200000,
|
| 1461 |
+
* maxTokens: 16384
|
| 1462 |
+
* }
|
| 1463 |
+
* ]
|
| 1464 |
+
* });
|
| 1465 |
+
*
|
| 1466 |
+
* @example
|
| 1467 |
+
* // Override baseUrl for an existing provider
|
| 1468 |
+
* pi.registerProvider("anthropic", {
|
| 1469 |
+
* baseUrl: "https://proxy.example.com"
|
| 1470 |
+
* });
|
| 1471 |
+
*
|
| 1472 |
+
* @example
|
| 1473 |
+
* // Register provider with OAuth support
|
| 1474 |
+
* pi.registerProvider("corporate-ai", {
|
| 1475 |
+
* baseUrl: "https://ai.corp.com",
|
| 1476 |
+
* api: "openai-responses",
|
| 1477 |
+
* models: [...],
|
| 1478 |
+
* oauth: {
|
| 1479 |
+
* name: "Corporate AI (SSO)",
|
| 1480 |
+
* async login(callbacks) { ... },
|
| 1481 |
+
* async refreshToken(credentials) { ... },
|
| 1482 |
+
* getApiKey(credentials) { return credentials.access; }
|
| 1483 |
+
* }
|
| 1484 |
+
* });
|
| 1485 |
+
*/
|
| 1486 |
+
registerProvider(provider: Provider): void;
|
| 1487 |
+
registerProvider(name: string, config: ProviderConfig): void;
|
| 1488 |
+
|
| 1489 |
+
/**
|
| 1490 |
+
* Unregister a previously registered provider.
|
| 1491 |
+
*
|
| 1492 |
+
* Removes all models belonging to the named provider and restores any
|
| 1493 |
+
* built-in models that were overridden by it. Has no effect if the provider
|
| 1494 |
+
* is not currently registered.
|
| 1495 |
+
*
|
| 1496 |
+
* Like `registerProvider`, this takes effect immediately when called after
|
| 1497 |
+
* the initial load phase.
|
| 1498 |
+
*
|
| 1499 |
+
* @example
|
| 1500 |
+
* pi.unregisterProvider("my-proxy");
|
| 1501 |
+
*/
|
| 1502 |
+
unregisterProvider(name: string): void;
|
| 1503 |
+
|
| 1504 |
+
/** Shared event bus for extension communication. */
|
| 1505 |
+
events: EventBus;
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
// ============================================================================
|
| 1509 |
+
// Provider Registration Types
|
| 1510 |
+
// ============================================================================
|
| 1511 |
+
|
| 1512 |
+
/** Configuration for registering a provider via pi.registerProvider(). */
|
| 1513 |
+
export interface ProviderConfig {
|
| 1514 |
+
/** Display name for the provider in UI. */
|
| 1515 |
+
name?: string;
|
| 1516 |
+
/** Base URL for the API endpoint. Required when defining models. */
|
| 1517 |
+
baseUrl?: string;
|
| 1518 |
+
/** API key literal, env interpolation ($ENV_VAR or ${ENV_VAR}), or leading !command. Required when defining models (unless oauth provided). */
|
| 1519 |
+
apiKey?: string;
|
| 1520 |
+
/** API type. Required at provider or model level when defining models. */
|
| 1521 |
+
api?: Api;
|
| 1522 |
+
/**
|
| 1523 |
+
* Optional streamSimple handler for custom APIs.
|
| 1524 |
+
* Implementations must invoke `options.onPayload` before sending the provider request and use any
|
| 1525 |
+
* returned replacement payload. They must invoke `options.onResponse` after receiving the response
|
| 1526 |
+
* and before consuming its body, matching built-in providers.
|
| 1527 |
+
*/
|
| 1528 |
+
streamSimple?: (model: Model<Api>, context: Context, options?: SimpleStreamOptions) => AssistantMessageEventStream;
|
| 1529 |
+
/** Custom headers to include in requests. */
|
| 1530 |
+
headers?: Record<string, string>;
|
| 1531 |
+
/** If true, adds Authorization: Bearer header with the resolved API key. */
|
| 1532 |
+
authHeader?: boolean;
|
| 1533 |
+
/** Models to register. If provided, replaces all existing models for this provider. */
|
| 1534 |
+
models?: ProviderModelConfig[];
|
| 1535 |
+
/**
|
| 1536 |
+
* Refresh this provider's model list. The returned list replaces extension-provided models.
|
| 1537 |
+
* Use context.publish({ persist: entry }) when the catalog should persist across sessions.
|
| 1538 |
+
*/
|
| 1539 |
+
refreshModels?(context: RefreshModelsContext): Promise<ProviderModelConfig[]>;
|
| 1540 |
+
/** OAuth provider for /login support. The `id` is set automatically from the provider name. */
|
| 1541 |
+
oauth?: {
|
| 1542 |
+
/** Display name for the provider in login UI. */
|
| 1543 |
+
name: string;
|
| 1544 |
+
/** Whether access through this auth method is backed by a provider subscription. */
|
| 1545 |
+
isSubscription?: boolean;
|
| 1546 |
+
/** @deprecated Retained for source compatibility; canonical auth flows ignore it. */
|
| 1547 |
+
usesCallbackServer?: boolean;
|
| 1548 |
+
/** Run the login flow, return credentials to persist. */
|
| 1549 |
+
login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials>;
|
| 1550 |
+
/** Refresh expired credentials, return updated credentials to persist. */
|
| 1551 |
+
refreshToken(credentials: OAuthCredentials, signal: AbortSignal): Promise<OAuthCredentials>;
|
| 1552 |
+
/** Convert credentials to API key string for the provider. */
|
| 1553 |
+
getApiKey(credentials: OAuthCredentials): string;
|
| 1554 |
+
/** Legacy synchronous credential-dependent model projection. */
|
| 1555 |
+
modifyModels?(models: Model<Api>[], credentials: OAuthCredentials): Model<Api>[];
|
| 1556 |
+
};
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
/** Configuration for a model within a provider. */
|
| 1560 |
+
export interface ProviderModelConfig {
|
| 1561 |
+
/** Model ID (e.g., "claude-sonnet-4-20250514"). */
|
| 1562 |
+
id: string;
|
| 1563 |
+
/** Display name (e.g., "Claude 4 Sonnet"). */
|
| 1564 |
+
name: string;
|
| 1565 |
+
/** API type override for this model. */
|
| 1566 |
+
api?: Api;
|
| 1567 |
+
/** API endpoint URL override for this model. */
|
| 1568 |
+
baseUrl?: string;
|
| 1569 |
+
/** Whether the model supports extended thinking. */
|
| 1570 |
+
reasoning: boolean;
|
| 1571 |
+
/** Maps pi thinking levels to provider/model-specific values; null marks a level unsupported. */
|
| 1572 |
+
thinkingLevelMap?: Model<Api>["thinkingLevelMap"];
|
| 1573 |
+
/** Supported input types. */
|
| 1574 |
+
input: ("text" | "image")[];
|
| 1575 |
+
/** Per-million-token cost rates and optional request-wide input pricing tiers. */
|
| 1576 |
+
cost: Model<Api>["cost"];
|
| 1577 |
+
/** Maximum context window size in tokens. */
|
| 1578 |
+
contextWindow: number;
|
| 1579 |
+
/** Maximum output tokens. */
|
| 1580 |
+
maxTokens: number;
|
| 1581 |
+
/** Custom headers for this model. */
|
| 1582 |
+
headers?: Record<string, string>;
|
| 1583 |
+
/** OpenAI compatibility settings. */
|
| 1584 |
+
compat?: Model<Api>["compat"];
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
/** Extension factory function type. Supports both sync and async initialization. */
|
| 1588 |
+
export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
|
| 1589 |
+
|
| 1590 |
+
export type InlineExtension =
|
| 1591 |
+
| ExtensionFactory
|
| 1592 |
+
| {
|
| 1593 |
+
/** Display name shown as `<inline:name>` in the startup Extensions list. */
|
| 1594 |
+
name: string;
|
| 1595 |
+
factory: ExtensionFactory;
|
| 1596 |
+
/** Omit this extension from the startup Extensions list. */
|
| 1597 |
+
hidden?: boolean;
|
| 1598 |
+
};
|
| 1599 |
+
|
| 1600 |
+
// ============================================================================
|
| 1601 |
+
// Loaded Extension Types
|
| 1602 |
+
// ============================================================================
|
| 1603 |
+
|
| 1604 |
+
export interface RegisteredTool {
|
| 1605 |
+
definition: ToolDefinition;
|
| 1606 |
+
sourceInfo: SourceInfo;
|
| 1607 |
+
}
|
| 1608 |
+
|
| 1609 |
+
export interface ExtensionFlag {
|
| 1610 |
+
name: string;
|
| 1611 |
+
description?: string;
|
| 1612 |
+
type: "boolean" | "string";
|
| 1613 |
+
default?: boolean | string;
|
| 1614 |
+
extensionPath: string;
|
| 1615 |
+
}
|
| 1616 |
+
|
| 1617 |
+
export interface ExtensionShortcut {
|
| 1618 |
+
shortcut: KeyId;
|
| 1619 |
+
description?: string;
|
| 1620 |
+
handler: (ctx: ExtensionContext) => Promise<void> | void;
|
| 1621 |
+
extensionPath: string;
|
| 1622 |
+
}
|
| 1623 |
+
|
| 1624 |
+
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
| 1625 |
+
|
| 1626 |
+
export type SendMessageHandler = <T = unknown>(
|
| 1627 |
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
| 1628 |
+
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
| 1629 |
+
) => void;
|
| 1630 |
+
|
| 1631 |
+
export type SendUserMessageHandler = (
|
| 1632 |
+
content: string | (TextContent | ImageContent)[],
|
| 1633 |
+
options?: { deliverAs?: "steer" | "followUp"; expandPromptTemplates?: boolean },
|
| 1634 |
+
) => void;
|
| 1635 |
+
|
| 1636 |
+
export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
|
| 1637 |
+
|
| 1638 |
+
export type SetSessionNameHandler = (name: string) => void;
|
| 1639 |
+
|
| 1640 |
+
export type GetSessionNameHandler = () => string | undefined;
|
| 1641 |
+
|
| 1642 |
+
export type GetActiveToolsHandler = () => string[];
|
| 1643 |
+
|
| 1644 |
+
/** Tool info with name, description, parameter schema, prompt guidelines, and source metadata. */
|
| 1645 |
+
export type ToolInfo = Pick<ToolDefinition, "name" | "description" | "parameters" | "promptGuidelines"> & {
|
| 1646 |
+
sourceInfo: SourceInfo;
|
| 1647 |
+
};
|
| 1648 |
+
|
| 1649 |
+
export type GetAllToolsHandler = () => ToolInfo[];
|
| 1650 |
+
|
| 1651 |
+
export type GetCommandsHandler = () => SlashCommandInfo[];
|
| 1652 |
+
|
| 1653 |
+
export type SetActiveToolsHandler = (toolNames: string[]) => void;
|
| 1654 |
+
|
| 1655 |
+
export type RefreshToolsHandler = () => void;
|
| 1656 |
+
|
| 1657 |
+
export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
|
| 1658 |
+
|
| 1659 |
+
export type GetThinkingLevelHandler = () => ThinkingLevel;
|
| 1660 |
+
|
| 1661 |
+
export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
|
| 1662 |
+
|
| 1663 |
+
export type SetLabelHandler = (entryId: string, label: string | undefined) => void;
|
| 1664 |
+
|
| 1665 |
+
/**
|
| 1666 |
+
* Shared state created by loader, used during registration and runtime.
|
| 1667 |
+
* Contains flag values (defaults set during registration, CLI values set after).
|
| 1668 |
+
*/
|
| 1669 |
+
export interface ExtensionRuntimeState {
|
| 1670 |
+
flagValues: Map<string, boolean | string>;
|
| 1671 |
+
/** Legacy provider-config registrations queued during extension loading, processed when runner binds. */
|
| 1672 |
+
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; extensionPath: string }>;
|
| 1673 |
+
/** Native pi-ai provider registrations queued during extension loading, processed when runner binds. */
|
| 1674 |
+
pendingNativeProviderRegistrations: Array<{ provider: Provider; extensionPath: string }>;
|
| 1675 |
+
/** Throws when this extension instance is stale after runtime replacement. */
|
| 1676 |
+
assertActive: () => void;
|
| 1677 |
+
/** Marks this extension instance as stale after runtime replacement or reload. */
|
| 1678 |
+
invalidate: (message?: string) => void;
|
| 1679 |
+
/** Retain an event-bus subscription until this runtime is invalidated. */
|
| 1680 |
+
trackEventBusSubscription: (unsubscribe: () => void) => () => void;
|
| 1681 |
+
/**
|
| 1682 |
+
* Register or unregister a provider.
|
| 1683 |
+
*
|
| 1684 |
+
* Before bindCore(): queues registrations / removes from queue.
|
| 1685 |
+
* After bindCore(): calls ModelRegistry directly for immediate effect.
|
| 1686 |
+
*/
|
| 1687 |
+
registerProvider: (name: string, config: ProviderConfig, extensionPath?: string) => void;
|
| 1688 |
+
registerNativeProvider: (provider: Provider, extensionPath?: string) => void;
|
| 1689 |
+
unregisterProvider: (name: string, extensionPath?: string) => void;
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
/**
|
| 1693 |
+
* Action implementations for pi.* API methods.
|
| 1694 |
+
* Provided to runner.initialize(), copied into the shared runtime.
|
| 1695 |
+
*/
|
| 1696 |
+
export interface ExtensionActions {
|
| 1697 |
+
sendMessage: SendMessageHandler;
|
| 1698 |
+
sendUserMessage: SendUserMessageHandler;
|
| 1699 |
+
appendEntry: AppendEntryHandler;
|
| 1700 |
+
setSessionName: SetSessionNameHandler;
|
| 1701 |
+
getSessionName: GetSessionNameHandler;
|
| 1702 |
+
setLabel: SetLabelHandler;
|
| 1703 |
+
getActiveTools: GetActiveToolsHandler;
|
| 1704 |
+
getAllTools: GetAllToolsHandler;
|
| 1705 |
+
setActiveTools: SetActiveToolsHandler;
|
| 1706 |
+
refreshTools: RefreshToolsHandler;
|
| 1707 |
+
getCommands: GetCommandsHandler;
|
| 1708 |
+
setModel: SetModelHandler;
|
| 1709 |
+
getThinkingLevel: GetThinkingLevelHandler;
|
| 1710 |
+
setThinkingLevel: SetThinkingLevelHandler;
|
| 1711 |
+
}
|
| 1712 |
+
|
| 1713 |
+
/**
|
| 1714 |
+
* Actions for ExtensionContext (ctx.* in event handlers).
|
| 1715 |
+
* Required by all modes.
|
| 1716 |
+
*/
|
| 1717 |
+
export interface ExtensionContextActions {
|
| 1718 |
+
getModel: () => Model<any> | undefined;
|
| 1719 |
+
getScopedModels: () => readonly ScopedModel[];
|
| 1720 |
+
isIdle: () => boolean;
|
| 1721 |
+
isProjectTrusted: () => boolean;
|
| 1722 |
+
getSignal: () => AbortSignal | undefined;
|
| 1723 |
+
abort: () => void;
|
| 1724 |
+
hasPendingMessages: () => boolean;
|
| 1725 |
+
shutdown: () => void;
|
| 1726 |
+
getContextUsage: () => ContextUsage | undefined;
|
| 1727 |
+
compact: (options?: CompactOptions) => void;
|
| 1728 |
+
getSystemPrompt: () => string;
|
| 1729 |
+
getSystemPromptOptions?: () => BuildSystemPromptOptions;
|
| 1730 |
+
}
|
| 1731 |
+
|
| 1732 |
+
/**
|
| 1733 |
+
* Actions for ExtensionCommandContext (ctx.* in command handlers).
|
| 1734 |
+
* Only needed for interactive mode where extension commands are invokable.
|
| 1735 |
+
*/
|
| 1736 |
+
export interface ExtensionCommandContextActions {
|
| 1737 |
+
waitForIdle: () => Promise<void>;
|
| 1738 |
+
newSession: (options?: {
|
| 1739 |
+
parentSession?: string;
|
| 1740 |
+
setup?: (sessionManager: SessionManager) => Promise<void>;
|
| 1741 |
+
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
|
| 1742 |
+
}) => Promise<{ cancelled: boolean }>;
|
| 1743 |
+
fork: (
|
| 1744 |
+
entryId: string,
|
| 1745 |
+
options?: { position?: "before" | "at"; withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 1746 |
+
) => Promise<{ cancelled: boolean }>;
|
| 1747 |
+
navigateTree: (
|
| 1748 |
+
targetId: string,
|
| 1749 |
+
options?: { summarize?: boolean; customInstructions?: string; replaceInstructions?: boolean; label?: string },
|
| 1750 |
+
) => Promise<{ cancelled: boolean }>;
|
| 1751 |
+
switchSession: (
|
| 1752 |
+
sessionPath: string,
|
| 1753 |
+
options?: { withSession?: (ctx: ReplacedSessionContext) => Promise<void> },
|
| 1754 |
+
) => Promise<{ cancelled: boolean }>;
|
| 1755 |
+
reload: () => Promise<void>;
|
| 1756 |
+
}
|
| 1757 |
+
|
| 1758 |
+
/**
|
| 1759 |
+
* Full runtime = state + actions.
|
| 1760 |
+
* Created by loader with throwing action stubs, completed by runner.initialize().
|
| 1761 |
+
*/
|
| 1762 |
+
export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
|
| 1763 |
+
|
| 1764 |
+
/** Loaded extension with all registered items. */
|
| 1765 |
+
export interface Extension {
|
| 1766 |
+
path: string;
|
| 1767 |
+
resolvedPath: string;
|
| 1768 |
+
hidden?: boolean;
|
| 1769 |
+
sourceInfo: SourceInfo;
|
| 1770 |
+
handlers: Map<string, HandlerFn[]>;
|
| 1771 |
+
tools: Map<string, RegisteredTool>;
|
| 1772 |
+
messageRenderers: Map<string, MessageRenderer>;
|
| 1773 |
+
markdownTransformer?: MarkdownTransformer;
|
| 1774 |
+
entryRenderers?: Map<string, EntryRenderer>;
|
| 1775 |
+
commands: Map<string, RegisteredCommand>;
|
| 1776 |
+
flags: Map<string, ExtensionFlag>;
|
| 1777 |
+
shortcuts: Map<KeyId, ExtensionShortcut>;
|
| 1778 |
+
}
|
| 1779 |
+
|
| 1780 |
+
/** Result of loading extensions. */
|
| 1781 |
+
export interface LoadExtensionsResult {
|
| 1782 |
+
extensions: Extension[];
|
| 1783 |
+
errors: Array<{ path: string; error: string }>;
|
| 1784 |
+
/** Shared runtime - actions are throwing stubs until runner.initialize() */
|
| 1785 |
+
runtime: ExtensionRuntime;
|
| 1786 |
+
}
|
| 1787 |
+
|
| 1788 |
+
// ============================================================================
|
| 1789 |
+
// Extension Error
|
| 1790 |
+
// ============================================================================
|
| 1791 |
+
|
| 1792 |
+
export interface ExtensionError {
|
| 1793 |
+
extensionPath: string;
|
| 1794 |
+
event: string;
|
| 1795 |
+
error: string;
|
| 1796 |
+
stack?: string;
|
| 1797 |
+
}
|
packages/coding-agent/src/core/extensions/wrapper.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Tool wrappers for extension-registered tools.
|
| 3 |
+
*
|
| 4 |
+
* These wrappers only adapt tool execution so extension tools receive the runner context.
|
| 5 |
+
* Tool call and tool result interception is handled by AgentSession via agent-core hooks.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 9 |
+
import { wrapToolDefinition } from "../tools/tool-definition-wrapper.ts";
|
| 10 |
+
import type { ExtensionRunner } from "./runner.ts";
|
| 11 |
+
import type { RegisteredTool } from "./types.ts";
|
| 12 |
+
|
| 13 |
+
/**
|
| 14 |
+
* Wrap a RegisteredTool into an AgentTool.
|
| 15 |
+
* Uses the runner's createContext() for consistent context across tools and event handlers.
|
| 16 |
+
*/
|
| 17 |
+
export function wrapRegisteredTool(registeredTool: RegisteredTool, runner: ExtensionRunner): AgentTool {
|
| 18 |
+
const tool = wrapToolDefinition(registeredTool.definition, () => runner.createContext());
|
| 19 |
+
const execute = tool.execute;
|
| 20 |
+
return {
|
| 21 |
+
...tool,
|
| 22 |
+
execute: async (toolCallId, params, signal, onUpdate) => {
|
| 23 |
+
const activeBefore = runner.getActiveTools();
|
| 24 |
+
const result = await execute(toolCallId, params, signal, onUpdate);
|
| 25 |
+
const activeAfter = runner.getActiveTools();
|
| 26 |
+
if (!activeBefore.every((name) => activeAfter.includes(name))) return result;
|
| 27 |
+
|
| 28 |
+
const beforeNames = new Set(activeBefore);
|
| 29 |
+
const addedToolNames = activeAfter.filter((name) => !beforeNames.has(name));
|
| 30 |
+
if (addedToolNames.length === 0) return result;
|
| 31 |
+
return {
|
| 32 |
+
...result,
|
| 33 |
+
addedToolNames: [...new Set([...(result.addedToolNames ?? []), ...addedToolNames])],
|
| 34 |
+
};
|
| 35 |
+
},
|
| 36 |
+
};
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
/**
|
| 40 |
+
* Wrap all registered tools into AgentTools.
|
| 41 |
+
* Uses the runner's createContext() for consistent context across tools and event handlers.
|
| 42 |
+
*/
|
| 43 |
+
export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: ExtensionRunner): AgentTool[] {
|
| 44 |
+
return registeredTools.map((tool) => wrapRegisteredTool(tool, runner));
|
| 45 |
+
}
|
packages/coding-agent/src/core/tools/bash.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { constants } from "node:fs";
|
| 2 |
+
import { access as fsAccess } from "node:fs/promises";
|
| 3 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 4 |
+
import { spawn } from "child_process";
|
| 5 |
+
import { type Static, Type } from "typebox";
|
| 6 |
+
import { waitForChildProcess } from "../../utils/child-process.ts";
|
| 7 |
+
import {
|
| 8 |
+
getShellConfig,
|
| 9 |
+
getShellEnv,
|
| 10 |
+
killProcessTree,
|
| 11 |
+
type ShellConfig,
|
| 12 |
+
trackDetachedChildPid,
|
| 13 |
+
untrackDetachedChildPid,
|
| 14 |
+
} from "../../utils/shell.ts";
|
| 15 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 16 |
+
import { OutputAccumulator } from "./output-accumulator.ts";
|
| 17 |
+
import { BASH_UPDATE_THROTTLE_MS, createShellRenderers } from "./renderers/bash.ts";
|
| 18 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 19 |
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult } from "./truncate.ts";
|
| 20 |
+
|
| 21 |
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
| 22 |
+
const MAX_TIMEOUT_SECONDS = MAX_TIMEOUT_MS / 1000;
|
| 23 |
+
|
| 24 |
+
function resolveTimeoutMs(timeout: number | undefined): number | undefined {
|
| 25 |
+
if (timeout === undefined) return undefined;
|
| 26 |
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
| 27 |
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const timeoutMs = timeout * 1000;
|
| 31 |
+
if (timeoutMs > MAX_TIMEOUT_MS) {
|
| 32 |
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
| 33 |
+
}
|
| 34 |
+
return timeoutMs;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
const bashSchema = Type.Object({
|
| 38 |
+
command: Type.String({ description: "Shell command to execute" }),
|
| 39 |
+
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
export const bashToolSystemPromptContribution = {
|
| 43 |
+
snippet: "Execute bash commands (ls, grep, find, etc.)",
|
| 44 |
+
guidelines: ["You can inspect PI_* environment variables for current model and session details."],
|
| 45 |
+
} as const;
|
| 46 |
+
|
| 47 |
+
export type BashToolInput = Static<typeof bashSchema>;
|
| 48 |
+
|
| 49 |
+
export interface BashToolDetails {
|
| 50 |
+
truncation?: TruncationResult;
|
| 51 |
+
fullOutputPath?: string;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Pluggable operations for the bash tool.
|
| 56 |
+
* Override these to delegate command execution to remote systems (for example SSH).
|
| 57 |
+
*/
|
| 58 |
+
export interface BashOperations {
|
| 59 |
+
/**
|
| 60 |
+
* Execute a command and stream output.
|
| 61 |
+
* @param command The command to execute
|
| 62 |
+
* @param cwd Working directory
|
| 63 |
+
* @param options Execution options
|
| 64 |
+
* @returns Promise resolving to exit code (null if killed)
|
| 65 |
+
*/
|
| 66 |
+
exec: (
|
| 67 |
+
command: string,
|
| 68 |
+
cwd: string,
|
| 69 |
+
options: {
|
| 70 |
+
onData: (data: Buffer) => void;
|
| 71 |
+
signal?: AbortSignal;
|
| 72 |
+
timeout?: number;
|
| 73 |
+
env?: NodeJS.ProcessEnv;
|
| 74 |
+
},
|
| 75 |
+
) => Promise<{ exitCode: number | null }>;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
/** Shared process execution used by the built-in shell tools. */
|
| 79 |
+
export function createLocalShellOperations(shellName: string, resolveShellConfig: () => ShellConfig): BashOperations {
|
| 80 |
+
return {
|
| 81 |
+
exec: async (command, cwd, { onData, signal, timeout, env }) => {
|
| 82 |
+
const timeoutMs = resolveTimeoutMs(timeout);
|
| 83 |
+
if (signal?.aborted) {
|
| 84 |
+
throw new Error("aborted");
|
| 85 |
+
}
|
| 86 |
+
const shellConfig = resolveShellConfig();
|
| 87 |
+
try {
|
| 88 |
+
await fsAccess(cwd, constants.F_OK);
|
| 89 |
+
} catch {
|
| 90 |
+
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute ${shellName} commands.`);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
const commandFromStdin = shellConfig.commandTransport === "stdin";
|
| 94 |
+
const child = spawn(shellConfig.shell, commandFromStdin ? shellConfig.args : [...shellConfig.args, command], {
|
| 95 |
+
cwd,
|
| 96 |
+
detached: process.platform !== "win32",
|
| 97 |
+
env: env ?? getShellEnv(),
|
| 98 |
+
stdio: [commandFromStdin ? "pipe" : "ignore", "pipe", "pipe"],
|
| 99 |
+
windowsHide: true,
|
| 100 |
+
});
|
| 101 |
+
if (commandFromStdin) {
|
| 102 |
+
child.stdin?.on("error", () => {});
|
| 103 |
+
child.stdin?.end(command);
|
| 104 |
+
}
|
| 105 |
+
if (child.pid) trackDetachedChildPid(child.pid);
|
| 106 |
+
let timedOut = false;
|
| 107 |
+
let timeoutHandle: NodeJS.Timeout | undefined;
|
| 108 |
+
const onAbort = () => {
|
| 109 |
+
if (child.pid) killProcessTree(child.pid);
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
try {
|
| 113 |
+
// Set timeout if provided.
|
| 114 |
+
if (timeoutMs !== undefined) {
|
| 115 |
+
timeoutHandle = setTimeout(() => {
|
| 116 |
+
timedOut = true;
|
| 117 |
+
if (child.pid) killProcessTree(child.pid);
|
| 118 |
+
}, timeoutMs);
|
| 119 |
+
}
|
| 120 |
+
// Stream stdout and stderr.
|
| 121 |
+
child.stdout?.on("data", onData);
|
| 122 |
+
child.stderr?.on("data", onData);
|
| 123 |
+
// Handle abort signal by killing the entire process tree.
|
| 124 |
+
if (signal) {
|
| 125 |
+
if (signal.aborted) onAbort();
|
| 126 |
+
else signal.addEventListener("abort", onAbort, { once: true });
|
| 127 |
+
}
|
| 128 |
+
// Handle shell spawn errors and wait for the process to terminate without hanging
|
| 129 |
+
// on inherited stdio handles held by detached descendants.
|
| 130 |
+
const exitCode = await waitForChildProcess(child);
|
| 131 |
+
if (signal?.aborted) {
|
| 132 |
+
throw new Error("aborted");
|
| 133 |
+
}
|
| 134 |
+
if (timedOut) {
|
| 135 |
+
throw new Error(`timeout:${timeout}`);
|
| 136 |
+
}
|
| 137 |
+
return { exitCode };
|
| 138 |
+
} finally {
|
| 139 |
+
if (child.pid) untrackDetachedChildPid(child.pid);
|
| 140 |
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
| 141 |
+
if (signal) signal.removeEventListener("abort", onAbort);
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
};
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
/**
|
| 148 |
+
* Create bash operations using pi's built-in local shell execution backend.
|
| 149 |
+
*
|
| 150 |
+
* This is useful for extensions that intercept user_bash and still want pi's
|
| 151 |
+
* standard local shell behavior while wrapping or rewriting commands.
|
| 152 |
+
*/
|
| 153 |
+
export function createLocalBashOperations(options?: { shellPath?: string }): BashOperations {
|
| 154 |
+
return createLocalShellOperations("bash", () => getShellConfig(options?.shellPath));
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
export interface BashSpawnContext {
|
| 158 |
+
command: string;
|
| 159 |
+
cwd: string;
|
| 160 |
+
env: NodeJS.ProcessEnv;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
export type BashSpawnHook = (context: BashSpawnContext) => BashSpawnContext;
|
| 164 |
+
|
| 165 |
+
function resolveSpawnContext(
|
| 166 |
+
command: string,
|
| 167 |
+
cwd: string,
|
| 168 |
+
spawnHook: BashSpawnHook | undefined,
|
| 169 |
+
exposeSessionEnvironment: boolean,
|
| 170 |
+
ctx: ExtensionContext | undefined,
|
| 171 |
+
): BashSpawnContext {
|
| 172 |
+
const env = { ...getShellEnv() };
|
| 173 |
+
delete env.PI_SESSION_ID;
|
| 174 |
+
delete env.PI_SESSION_FILE;
|
| 175 |
+
delete env.PI_PROVIDER;
|
| 176 |
+
delete env.PI_MODEL;
|
| 177 |
+
delete env.PI_REASONING_LEVEL;
|
| 178 |
+
if (exposeSessionEnvironment && ctx) {
|
| 179 |
+
const model = ctx.model;
|
| 180 |
+
env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
|
| 181 |
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
| 182 |
+
if (sessionFile) env.PI_SESSION_FILE = sessionFile;
|
| 183 |
+
if (model) {
|
| 184 |
+
env.PI_PROVIDER = model.provider;
|
| 185 |
+
env.PI_MODEL = model.id;
|
| 186 |
+
}
|
| 187 |
+
if (ctx.thinkingLevel) env.PI_REASONING_LEVEL = ctx.thinkingLevel;
|
| 188 |
+
}
|
| 189 |
+
const baseContext: BashSpawnContext = { command, cwd, env };
|
| 190 |
+
return spawnHook ? spawnHook(baseContext) : baseContext;
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
export interface BashToolOptions {
|
| 194 |
+
/** Custom operations for command execution. Default: local shell */
|
| 195 |
+
operations?: BashOperations;
|
| 196 |
+
/** Command prefix prepended to every command (for example shell setup commands) */
|
| 197 |
+
commandPrefix?: string;
|
| 198 |
+
/** Optional explicit shell path from settings */
|
| 199 |
+
shellPath?: string;
|
| 200 |
+
/** Expose current Pi session metadata as PI_* environment variables. Default: true */
|
| 201 |
+
exposeSessionEnvironment?: boolean;
|
| 202 |
+
/** Hook to adjust command, cwd, or env before execution */
|
| 203 |
+
spawnHook?: BashSpawnHook;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
export type BashRenderState = {
|
| 207 |
+
startedAt: number | undefined;
|
| 208 |
+
endedAt: number | undefined;
|
| 209 |
+
interval: NodeJS.Timeout | undefined;
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
export interface ShellToolConfig {
|
| 213 |
+
name: string;
|
| 214 |
+
label: string;
|
| 215 |
+
shellName: string;
|
| 216 |
+
prompt: string;
|
| 217 |
+
promptSnippet: string;
|
| 218 |
+
promptGuidelines?: readonly string[];
|
| 219 |
+
tempFilePrefix: string;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
export function createShellToolDefinition(
|
| 223 |
+
cwd: string,
|
| 224 |
+
config: ShellToolConfig,
|
| 225 |
+
options?: BashToolOptions,
|
| 226 |
+
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
| 227 |
+
const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
|
| 228 |
+
const commandPrefix = options?.commandPrefix;
|
| 229 |
+
const exposeSessionEnvironment = options?.exposeSessionEnvironment ?? true;
|
| 230 |
+
const spawnHook = options?.spawnHook;
|
| 231 |
+
return {
|
| 232 |
+
name: config.name,
|
| 233 |
+
label: config.label,
|
| 234 |
+
description: `Execute a ${config.shellName} command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
| 235 |
+
promptSnippet: config.promptSnippet,
|
| 236 |
+
promptGuidelines: exposeSessionEnvironment && config.promptGuidelines ? [...config.promptGuidelines] : undefined,
|
| 237 |
+
parameters: bashSchema,
|
| 238 |
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
| 239 |
+
async execute(
|
| 240 |
+
_toolCallId,
|
| 241 |
+
{ command, timeout }: { command: string; timeout?: number },
|
| 242 |
+
signal?: AbortSignal,
|
| 243 |
+
onUpdate?,
|
| 244 |
+
ctx?: ExtensionContext,
|
| 245 |
+
) {
|
| 246 |
+
const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
|
| 247 |
+
const spawnContext = resolveSpawnContext(
|
| 248 |
+
resolvedCommand,
|
| 249 |
+
ctx?.cwd || cwd,
|
| 250 |
+
spawnHook,
|
| 251 |
+
exposeSessionEnvironment,
|
| 252 |
+
ctx,
|
| 253 |
+
);
|
| 254 |
+
const output = new OutputAccumulator({ tempFilePrefix: config.tempFilePrefix });
|
| 255 |
+
let acceptingOutput = true;
|
| 256 |
+
let updateTimer: NodeJS.Timeout | undefined;
|
| 257 |
+
let updateDirty = false;
|
| 258 |
+
let lastUpdateAt = 0;
|
| 259 |
+
|
| 260 |
+
const emitOutputUpdate = () => {
|
| 261 |
+
if (!onUpdate || !updateDirty) return;
|
| 262 |
+
updateDirty = false;
|
| 263 |
+
lastUpdateAt = Date.now();
|
| 264 |
+
const snapshot = output.snapshot({ persistIfTruncated: true });
|
| 265 |
+
onUpdate({
|
| 266 |
+
content: [{ type: "text", text: snapshot.content || "" }],
|
| 267 |
+
details: {
|
| 268 |
+
truncation: snapshot.truncation.truncated ? snapshot.truncation : undefined,
|
| 269 |
+
fullOutputPath: snapshot.fullOutputPath,
|
| 270 |
+
},
|
| 271 |
+
});
|
| 272 |
+
};
|
| 273 |
+
|
| 274 |
+
const clearUpdateTimer = () => {
|
| 275 |
+
if (updateTimer) {
|
| 276 |
+
clearTimeout(updateTimer);
|
| 277 |
+
updateTimer = undefined;
|
| 278 |
+
}
|
| 279 |
+
};
|
| 280 |
+
|
| 281 |
+
const scheduleOutputUpdate = () => {
|
| 282 |
+
if (!onUpdate) return;
|
| 283 |
+
updateDirty = true;
|
| 284 |
+
const delay = BASH_UPDATE_THROTTLE_MS - (Date.now() - lastUpdateAt);
|
| 285 |
+
if (delay <= 0) {
|
| 286 |
+
clearUpdateTimer();
|
| 287 |
+
emitOutputUpdate();
|
| 288 |
+
return;
|
| 289 |
+
}
|
| 290 |
+
updateTimer ??= setTimeout(() => {
|
| 291 |
+
updateTimer = undefined;
|
| 292 |
+
emitOutputUpdate();
|
| 293 |
+
}, delay);
|
| 294 |
+
};
|
| 295 |
+
|
| 296 |
+
if (onUpdate) {
|
| 297 |
+
onUpdate({ content: [], details: undefined });
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
const handleData = (data: Buffer) => {
|
| 301 |
+
if (!acceptingOutput) return;
|
| 302 |
+
output.append(data);
|
| 303 |
+
scheduleOutputUpdate();
|
| 304 |
+
};
|
| 305 |
+
|
| 306 |
+
const finishOutput = async () => {
|
| 307 |
+
acceptingOutput = false;
|
| 308 |
+
output.finish();
|
| 309 |
+
clearUpdateTimer();
|
| 310 |
+
emitOutputUpdate();
|
| 311 |
+
const snapshot = output.snapshot({ persistIfTruncated: true });
|
| 312 |
+
await output.closeTempFile();
|
| 313 |
+
return snapshot;
|
| 314 |
+
};
|
| 315 |
+
|
| 316 |
+
const formatOutput = (snapshot: Awaited<ReturnType<typeof finishOutput>>, emptyText = "(no output)") => {
|
| 317 |
+
const truncation = snapshot.truncation;
|
| 318 |
+
let text = snapshot.content || emptyText;
|
| 319 |
+
let details: BashToolDetails | undefined;
|
| 320 |
+
if (truncation.truncated) {
|
| 321 |
+
details = { truncation, fullOutputPath: snapshot.fullOutputPath };
|
| 322 |
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
| 323 |
+
const endLine = truncation.totalLines;
|
| 324 |
+
if (truncation.lastLinePartial) {
|
| 325 |
+
const lastLineSize = formatSize(output.getLastLineBytes());
|
| 326 |
+
text += `\n\n[Showing last ${formatSize(truncation.outputBytes)} of line ${endLine} (line is ${lastLineSize}). Full output: ${snapshot.fullOutputPath}]`;
|
| 327 |
+
} else if (truncation.truncatedBy === "lines") {
|
| 328 |
+
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${snapshot.fullOutputPath}]`;
|
| 329 |
+
} else {
|
| 330 |
+
text += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Full output: ${snapshot.fullOutputPath}]`;
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
return { text, details };
|
| 334 |
+
};
|
| 335 |
+
|
| 336 |
+
const appendStatus = (text: string, status: string) => `${text ? `${text}\n\n` : ""}${status}`;
|
| 337 |
+
|
| 338 |
+
try {
|
| 339 |
+
let exitCode: number | null;
|
| 340 |
+
try {
|
| 341 |
+
const result = await ops.exec(spawnContext.command, spawnContext.cwd, {
|
| 342 |
+
onData: handleData,
|
| 343 |
+
signal,
|
| 344 |
+
timeout,
|
| 345 |
+
env: spawnContext.env,
|
| 346 |
+
});
|
| 347 |
+
exitCode = result.exitCode;
|
| 348 |
+
} catch (err) {
|
| 349 |
+
const snapshot = await finishOutput();
|
| 350 |
+
const { text } = formatOutput(snapshot, "");
|
| 351 |
+
if (err instanceof Error && err.message === "aborted") {
|
| 352 |
+
throw new Error(appendStatus(text, "Command aborted"));
|
| 353 |
+
}
|
| 354 |
+
if (err instanceof Error && err.message.startsWith("timeout:")) {
|
| 355 |
+
const timeoutSecs = err.message.split(":")[1];
|
| 356 |
+
throw new Error(appendStatus(text, `Command timed out after ${timeoutSecs} seconds`));
|
| 357 |
+
}
|
| 358 |
+
throw err;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const snapshot = await finishOutput();
|
| 362 |
+
const { text: outputText, details } = formatOutput(snapshot);
|
| 363 |
+
if (exitCode !== 0 && exitCode !== null) {
|
| 364 |
+
throw new Error(appendStatus(outputText, `Command exited with code ${exitCode}`));
|
| 365 |
+
}
|
| 366 |
+
return { content: [{ type: "text", text: outputText }], details };
|
| 367 |
+
} finally {
|
| 368 |
+
clearUpdateTimer();
|
| 369 |
+
}
|
| 370 |
+
},
|
| 371 |
+
...createShellRenderers(config.prompt),
|
| 372 |
+
};
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
const bashToolConfig: ShellToolConfig = {
|
| 376 |
+
name: "bash",
|
| 377 |
+
label: "bash",
|
| 378 |
+
shellName: "bash",
|
| 379 |
+
prompt: "$",
|
| 380 |
+
promptSnippet: bashToolSystemPromptContribution.snippet,
|
| 381 |
+
promptGuidelines: bashToolSystemPromptContribution.guidelines,
|
| 382 |
+
tempFilePrefix: "pi-bash",
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
export function createBashToolDefinition(
|
| 386 |
+
cwd: string,
|
| 387 |
+
options?: BashToolOptions,
|
| 388 |
+
): ToolDefinition<typeof bashSchema, BashToolDetails | undefined, BashRenderState> {
|
| 389 |
+
return createShellToolDefinition(cwd, bashToolConfig, options);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
export function createBashTool(cwd: string, options?: BashToolOptions): AgentTool<typeof bashSchema> {
|
| 393 |
+
const definition = createBashToolDefinition(cwd, options);
|
| 394 |
+
const tool = wrapToolDefinition(definition);
|
| 395 |
+
Object.assign(tool, {
|
| 396 |
+
promptSnippet: definition.promptSnippet,
|
| 397 |
+
promptGuidelines: definition.promptGuidelines,
|
| 398 |
+
});
|
| 399 |
+
return tool;
|
| 400 |
+
}
|
packages/coding-agent/src/core/tools/edit-diff.ts
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared diff computation utilities for the edit and similar tools.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import * as Diff from "diff";
|
| 6 |
+
import { constants } from "fs";
|
| 7 |
+
import { access, readFile } from "fs/promises";
|
| 8 |
+
import { splitBom } from "../../utils/text.ts";
|
| 9 |
+
import { resolveToCwd } from "./path-utils.ts";
|
| 10 |
+
|
| 11 |
+
export function detectLineEnding(content: string): "\r\n" | "\n" {
|
| 12 |
+
const crlfIdx = content.indexOf("\r\n");
|
| 13 |
+
const lfIdx = content.indexOf("\n");
|
| 14 |
+
if (lfIdx === -1) return "\n";
|
| 15 |
+
if (crlfIdx === -1) return "\n";
|
| 16 |
+
return crlfIdx < lfIdx ? "\r\n" : "\n";
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function normalizeToLF(text: string): string {
|
| 20 |
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string {
|
| 24 |
+
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
/**
|
| 28 |
+
* Normalize text for fuzzy matching. Applies progressive transformations:
|
| 29 |
+
* - Strip trailing whitespace from each line
|
| 30 |
+
* - Normalize smart quotes to ASCII equivalents
|
| 31 |
+
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
| 32 |
+
* - Normalize special Unicode spaces to regular space
|
| 33 |
+
*/
|
| 34 |
+
export function normalizeForFuzzyMatch(text: string): string {
|
| 35 |
+
return (
|
| 36 |
+
text
|
| 37 |
+
.normalize("NFKC")
|
| 38 |
+
// Strip trailing whitespace per line
|
| 39 |
+
.split("\n")
|
| 40 |
+
.map((line) => line.trimEnd())
|
| 41 |
+
.join("\n")
|
| 42 |
+
// Smart single quotes → '
|
| 43 |
+
.replace(/[\u2018\u2019\u201A\u201B]/g, "'")
|
| 44 |
+
// Smart double quotes → "
|
| 45 |
+
.replace(/[\u201C\u201D\u201E\u201F]/g, '"')
|
| 46 |
+
// Various dashes/hyphens → -
|
| 47 |
+
// U+2010 hyphen, U+2011 non-breaking hyphen, U+2012 figure dash,
|
| 48 |
+
// U+2013 en-dash, U+2014 em-dash, U+2015 horizontal bar, U+2212 minus
|
| 49 |
+
.replace(/[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g, "-")
|
| 50 |
+
// Special spaces → regular space
|
| 51 |
+
// U+00A0 NBSP, U+2002-U+200A various spaces, U+202F narrow NBSP,
|
| 52 |
+
// U+205F medium math space, U+3000 ideographic space
|
| 53 |
+
.replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ")
|
| 54 |
+
);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function splitLinesWithEndings(content: string): string[] {
|
| 58 |
+
return content.match(/[^\n]*\n|[^\n]+/g) ?? [];
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
interface LineSpan {
|
| 62 |
+
start: number;
|
| 63 |
+
end: number;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
interface MatchedEdit {
|
| 67 |
+
editIndex: number;
|
| 68 |
+
matchIndex: number;
|
| 69 |
+
matchLength: number;
|
| 70 |
+
newText: string;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
type TextReplacement = Pick<MatchedEdit, "matchIndex" | "matchLength" | "newText">;
|
| 74 |
+
|
| 75 |
+
function getLineSpans(content: string): LineSpan[] {
|
| 76 |
+
let offset = 0;
|
| 77 |
+
return splitLinesWithEndings(content).map((line) => {
|
| 78 |
+
const span = { start: offset, end: offset + line.length };
|
| 79 |
+
offset = span.end;
|
| 80 |
+
return span;
|
| 81 |
+
});
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function getReplacementLineRange(lines: LineSpan[], replacement: TextReplacement) {
|
| 85 |
+
const replacementStart = replacement.matchIndex;
|
| 86 |
+
const replacementEnd = replacement.matchIndex + replacement.matchLength;
|
| 87 |
+
|
| 88 |
+
let startLine = -1;
|
| 89 |
+
for (let i = 0; i < lines.length; i++) {
|
| 90 |
+
const line = lines[i];
|
| 91 |
+
if (replacementStart >= line.start && replacementStart < line.end) {
|
| 92 |
+
startLine = i;
|
| 93 |
+
break;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
if (startLine === -1) {
|
| 97 |
+
throw new Error("Replacement range is outside the base content.");
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
let endLine = startLine;
|
| 101 |
+
while (endLine < lines.length && lines[endLine].end < replacementEnd) {
|
| 102 |
+
endLine++;
|
| 103 |
+
}
|
| 104 |
+
if (endLine >= lines.length) {
|
| 105 |
+
throw new Error("Replacement range is outside the base content.");
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
return { startLine, endLine: endLine + 1 };
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function applyReplacements(content: string, replacements: TextReplacement[], offset = 0): string {
|
| 112 |
+
let result = content;
|
| 113 |
+
for (let i = replacements.length - 1; i >= 0; i--) {
|
| 114 |
+
const replacement = replacements[i];
|
| 115 |
+
const matchIndex = replacement.matchIndex - offset;
|
| 116 |
+
result =
|
| 117 |
+
result.substring(0, matchIndex) + replacement.newText + result.substring(matchIndex + replacement.matchLength);
|
| 118 |
+
}
|
| 119 |
+
return result;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
/**
|
| 123 |
+
* Apply replacements matched against `baseContent` to `originalContent` while
|
| 124 |
+
* preserving unchanged line blocks from the original.
|
| 125 |
+
*
|
| 126 |
+
* This is useful when `baseContent` is a normalized view of the original. Each
|
| 127 |
+
* replacement is widened to the lines it actually touches, those touched lines
|
| 128 |
+
* are rewritten from the normalized base, and all other lines are copied back
|
| 129 |
+
* from `originalContent`. The actual replacement ranges drive preservation so
|
| 130 |
+
* duplicate normalized lines cannot be aligned to the wrong occurrence.
|
| 131 |
+
*/
|
| 132 |
+
export function applyReplacementsPreservingUnchangedLines(
|
| 133 |
+
originalContent: string,
|
| 134 |
+
baseContent: string,
|
| 135 |
+
replacements: TextReplacement[],
|
| 136 |
+
): string {
|
| 137 |
+
const originalLines = splitLinesWithEndings(originalContent);
|
| 138 |
+
const baseLines = getLineSpans(baseContent);
|
| 139 |
+
if (originalLines.length !== baseLines.length) {
|
| 140 |
+
throw new Error("Cannot preserve unchanged lines because the base content has a different line count.");
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
const groups: Array<{ startLine: number; endLine: number; replacements: TextReplacement[] }> = [];
|
| 144 |
+
const sortedReplacements = [...replacements].sort((a, b) => a.matchIndex - b.matchIndex);
|
| 145 |
+
for (const replacement of sortedReplacements) {
|
| 146 |
+
const range = getReplacementLineRange(baseLines, replacement);
|
| 147 |
+
const current = groups[groups.length - 1];
|
| 148 |
+
if (current && range.startLine < current.endLine) {
|
| 149 |
+
current.endLine = Math.max(current.endLine, range.endLine);
|
| 150 |
+
current.replacements.push(replacement);
|
| 151 |
+
continue;
|
| 152 |
+
}
|
| 153 |
+
groups.push({ ...range, replacements: [replacement] });
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
let originalLineIndex = 0;
|
| 157 |
+
let result = "";
|
| 158 |
+
for (const group of groups) {
|
| 159 |
+
result += originalLines.slice(originalLineIndex, group.startLine).join("");
|
| 160 |
+
|
| 161 |
+
const groupStartOffset = baseLines[group.startLine].start;
|
| 162 |
+
const groupEndOffset = baseLines[group.endLine - 1].end;
|
| 163 |
+
result += applyReplacements(
|
| 164 |
+
baseContent.slice(groupStartOffset, groupEndOffset),
|
| 165 |
+
group.replacements,
|
| 166 |
+
groupStartOffset,
|
| 167 |
+
);
|
| 168 |
+
originalLineIndex = group.endLine;
|
| 169 |
+
}
|
| 170 |
+
result += originalLines.slice(originalLineIndex).join("");
|
| 171 |
+
|
| 172 |
+
return result;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
export interface FuzzyMatchResult {
|
| 176 |
+
/** Whether a match was found */
|
| 177 |
+
found: boolean;
|
| 178 |
+
/** The index where the match starts (in the content that should be used for replacement) */
|
| 179 |
+
index: number;
|
| 180 |
+
/** Length of the matched text */
|
| 181 |
+
matchLength: number;
|
| 182 |
+
/** Whether fuzzy matching was used (false = exact match) */
|
| 183 |
+
usedFuzzyMatch: boolean;
|
| 184 |
+
/**
|
| 185 |
+
* The content to use for replacement operations.
|
| 186 |
+
* When exact match: original content. When fuzzy match: normalized content.
|
| 187 |
+
*/
|
| 188 |
+
contentForReplacement: string;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
export interface Edit {
|
| 192 |
+
oldText: string;
|
| 193 |
+
newText: string;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
export interface AppliedEditsResult {
|
| 197 |
+
baseContent: string;
|
| 198 |
+
newContent: string;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
/**
|
| 202 |
+
* Find oldText in content, trying exact match first, then fuzzy match.
|
| 203 |
+
* When fuzzy matching is used, the returned contentForReplacement is the
|
| 204 |
+
* fuzzy-normalized version of the content (trailing whitespace stripped,
|
| 205 |
+
* Unicode quotes/dashes normalized to ASCII).
|
| 206 |
+
*/
|
| 207 |
+
export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {
|
| 208 |
+
// Try exact match first
|
| 209 |
+
const exactIndex = content.indexOf(oldText);
|
| 210 |
+
if (exactIndex !== -1) {
|
| 211 |
+
return {
|
| 212 |
+
found: true,
|
| 213 |
+
index: exactIndex,
|
| 214 |
+
matchLength: oldText.length,
|
| 215 |
+
usedFuzzyMatch: false,
|
| 216 |
+
contentForReplacement: content,
|
| 217 |
+
};
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// Try fuzzy match - work entirely in normalized space
|
| 221 |
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
| 222 |
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
| 223 |
+
const fuzzyIndex = fuzzyContent.indexOf(fuzzyOldText);
|
| 224 |
+
|
| 225 |
+
if (fuzzyIndex === -1) {
|
| 226 |
+
return {
|
| 227 |
+
found: false,
|
| 228 |
+
index: -1,
|
| 229 |
+
matchLength: 0,
|
| 230 |
+
usedFuzzyMatch: false,
|
| 231 |
+
contentForReplacement: content,
|
| 232 |
+
};
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
// When fuzzy matching, return offsets in normalized space. Callers can use
|
| 236 |
+
// the normalized content to compute replacements, then decide how much of
|
| 237 |
+
// that normalized output should be written back.
|
| 238 |
+
return {
|
| 239 |
+
found: true,
|
| 240 |
+
index: fuzzyIndex,
|
| 241 |
+
matchLength: fuzzyOldText.length,
|
| 242 |
+
usedFuzzyMatch: true,
|
| 243 |
+
contentForReplacement: fuzzyContent,
|
| 244 |
+
};
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
function countOccurrences(content: string, oldText: string): number {
|
| 248 |
+
const fuzzyContent = normalizeForFuzzyMatch(content);
|
| 249 |
+
const fuzzyOldText = normalizeForFuzzyMatch(oldText);
|
| 250 |
+
return fuzzyContent.split(fuzzyOldText).length - 1;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
function getNotFoundError(path: string, editIndex: number, totalEdits: number): Error {
|
| 254 |
+
if (totalEdits === 1) {
|
| 255 |
+
return new Error(
|
| 256 |
+
`Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,
|
| 257 |
+
);
|
| 258 |
+
}
|
| 259 |
+
return new Error(
|
| 260 |
+
`Could not find edits[${editIndex}] in ${path}. The oldText must match exactly including all whitespace and newlines.`,
|
| 261 |
+
);
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
function getDuplicateError(path: string, editIndex: number, totalEdits: number, occurrences: number): Error {
|
| 265 |
+
if (totalEdits === 1) {
|
| 266 |
+
return new Error(
|
| 267 |
+
`Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,
|
| 268 |
+
);
|
| 269 |
+
}
|
| 270 |
+
return new Error(
|
| 271 |
+
`Found ${occurrences} occurrences of edits[${editIndex}] in ${path}. Each oldText must be unique. Please provide more context to make it unique.`,
|
| 272 |
+
);
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
function getEmptyOldTextError(path: string, editIndex: number, totalEdits: number): Error {
|
| 276 |
+
if (totalEdits === 1) {
|
| 277 |
+
return new Error(`oldText must not be empty in ${path}.`);
|
| 278 |
+
}
|
| 279 |
+
return new Error(`edits[${editIndex}].oldText must not be empty in ${path}.`);
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function getNoChangeError(path: string, totalEdits: number): Error {
|
| 283 |
+
if (totalEdits === 1) {
|
| 284 |
+
return new Error(
|
| 285 |
+
`No changes made to ${path}. The replacement produced identical content. This might indicate an issue with special characters or the text not existing as expected.`,
|
| 286 |
+
);
|
| 287 |
+
}
|
| 288 |
+
return new Error(`No changes made to ${path}. The replacements produced identical content.`);
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/**
|
| 292 |
+
* Apply one or more exact-text replacements to LF-normalized content.
|
| 293 |
+
*
|
| 294 |
+
* All edits are matched against the same original content. Replacements are
|
| 295 |
+
* then applied in reverse order so offsets remain stable. If any edit needs
|
| 296 |
+
* fuzzy matching, the operation runs in fuzzy-normalized content space and then
|
| 297 |
+
* overlays those line-level changes onto the original content so unchanged line
|
| 298 |
+
* blocks keep their original bytes.
|
| 299 |
+
*/
|
| 300 |
+
export function applyEditsToNormalizedContent(
|
| 301 |
+
normalizedContent: string,
|
| 302 |
+
edits: Edit[],
|
| 303 |
+
path: string,
|
| 304 |
+
): AppliedEditsResult {
|
| 305 |
+
const normalizedEdits = edits.map((edit) => ({
|
| 306 |
+
oldText: normalizeToLF(edit.oldText),
|
| 307 |
+
newText: normalizeToLF(edit.newText),
|
| 308 |
+
}));
|
| 309 |
+
|
| 310 |
+
for (let i = 0; i < normalizedEdits.length; i++) {
|
| 311 |
+
if (normalizedEdits[i].oldText.length === 0) {
|
| 312 |
+
throw getEmptyOldTextError(path, i, normalizedEdits.length);
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
const initialMatches = normalizedEdits.map((edit) => fuzzyFindText(normalizedContent, edit.oldText));
|
| 317 |
+
const usedFuzzyMatch = initialMatches.some((match) => match.usedFuzzyMatch);
|
| 318 |
+
const replacementBaseContent = usedFuzzyMatch ? normalizeForFuzzyMatch(normalizedContent) : normalizedContent;
|
| 319 |
+
|
| 320 |
+
const matchedEdits: MatchedEdit[] = [];
|
| 321 |
+
for (let i = 0; i < normalizedEdits.length; i++) {
|
| 322 |
+
const edit = normalizedEdits[i];
|
| 323 |
+
const matchResult = fuzzyFindText(replacementBaseContent, edit.oldText);
|
| 324 |
+
if (!matchResult.found) {
|
| 325 |
+
throw getNotFoundError(path, i, normalizedEdits.length);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
const occurrences = countOccurrences(replacementBaseContent, edit.oldText);
|
| 329 |
+
if (occurrences > 1) {
|
| 330 |
+
throw getDuplicateError(path, i, normalizedEdits.length, occurrences);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
matchedEdits.push({
|
| 334 |
+
editIndex: i,
|
| 335 |
+
matchIndex: matchResult.index,
|
| 336 |
+
matchLength: matchResult.matchLength,
|
| 337 |
+
newText: edit.newText,
|
| 338 |
+
});
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
matchedEdits.sort((a, b) => a.matchIndex - b.matchIndex);
|
| 342 |
+
for (let i = 1; i < matchedEdits.length; i++) {
|
| 343 |
+
const previous = matchedEdits[i - 1];
|
| 344 |
+
const current = matchedEdits[i];
|
| 345 |
+
if (previous.matchIndex + previous.matchLength > current.matchIndex) {
|
| 346 |
+
throw new Error(
|
| 347 |
+
`edits[${previous.editIndex}] and edits[${current.editIndex}] overlap in ${path}. Merge them into one edit or target disjoint regions.`,
|
| 348 |
+
);
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
const baseContent = normalizedContent;
|
| 353 |
+
const newContent = usedFuzzyMatch
|
| 354 |
+
? applyReplacementsPreservingUnchangedLines(normalizedContent, replacementBaseContent, matchedEdits)
|
| 355 |
+
: applyReplacements(replacementBaseContent, matchedEdits);
|
| 356 |
+
|
| 357 |
+
if (baseContent === newContent) {
|
| 358 |
+
throw getNoChangeError(path, normalizedEdits.length);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
return { baseContent, newContent };
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
/** Generate a standard unified patch. */
|
| 365 |
+
export function generateUnifiedPatch(path: string, oldContent: string, newContent: string, contextLines = 4): string {
|
| 366 |
+
return Diff.createTwoFilesPatch(path, path, oldContent, newContent, undefined, undefined, {
|
| 367 |
+
context: contextLines,
|
| 368 |
+
headerOptions: Diff.FILE_HEADERS_ONLY,
|
| 369 |
+
});
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
/**
|
| 373 |
+
* Generate a display-oriented diff string with line numbers and context.
|
| 374 |
+
* Returns both the diff string and the first changed line number (in the new file).
|
| 375 |
+
*/
|
| 376 |
+
export function generateDiffString(
|
| 377 |
+
oldContent: string,
|
| 378 |
+
newContent: string,
|
| 379 |
+
contextLines = 4,
|
| 380 |
+
): { diff: string; firstChangedLine: number | undefined } {
|
| 381 |
+
const parts = Diff.diffLines(oldContent, newContent);
|
| 382 |
+
const output: string[] = [];
|
| 383 |
+
|
| 384 |
+
const oldLines = oldContent.split("\n");
|
| 385 |
+
const newLines = newContent.split("\n");
|
| 386 |
+
const maxLineNum = Math.max(oldLines.length, newLines.length);
|
| 387 |
+
const lineNumWidth = String(maxLineNum).length;
|
| 388 |
+
|
| 389 |
+
let oldLineNum = 1;
|
| 390 |
+
let newLineNum = 1;
|
| 391 |
+
let lastWasChange = false;
|
| 392 |
+
let firstChangedLine: number | undefined;
|
| 393 |
+
|
| 394 |
+
for (let i = 0; i < parts.length; i++) {
|
| 395 |
+
const part = parts[i];
|
| 396 |
+
const raw = part.value.split("\n");
|
| 397 |
+
if (raw[raw.length - 1] === "") {
|
| 398 |
+
raw.pop();
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
if (part.added || part.removed) {
|
| 402 |
+
// Capture the first changed line (in the new file)
|
| 403 |
+
if (firstChangedLine === undefined) {
|
| 404 |
+
firstChangedLine = newLineNum;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// Show the change
|
| 408 |
+
for (const line of raw) {
|
| 409 |
+
if (part.added) {
|
| 410 |
+
const lineNum = String(newLineNum).padStart(lineNumWidth, " ");
|
| 411 |
+
output.push(`+${lineNum} ${line}`);
|
| 412 |
+
newLineNum++;
|
| 413 |
+
} else {
|
| 414 |
+
// removed
|
| 415 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 416 |
+
output.push(`-${lineNum} ${line}`);
|
| 417 |
+
oldLineNum++;
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
lastWasChange = true;
|
| 421 |
+
} else {
|
| 422 |
+
// Context lines - only show a few before/after changes
|
| 423 |
+
const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed);
|
| 424 |
+
const hasLeadingChange = lastWasChange;
|
| 425 |
+
const hasTrailingChange = nextPartIsChange;
|
| 426 |
+
|
| 427 |
+
if (hasLeadingChange && hasTrailingChange) {
|
| 428 |
+
if (raw.length <= contextLines * 2) {
|
| 429 |
+
for (const line of raw) {
|
| 430 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 431 |
+
output.push(` ${lineNum} ${line}`);
|
| 432 |
+
oldLineNum++;
|
| 433 |
+
newLineNum++;
|
| 434 |
+
}
|
| 435 |
+
} else {
|
| 436 |
+
const leadingLines = raw.slice(0, contextLines);
|
| 437 |
+
const trailingLines = raw.slice(raw.length - contextLines);
|
| 438 |
+
const skippedLines = raw.length - leadingLines.length - trailingLines.length;
|
| 439 |
+
|
| 440 |
+
for (const line of leadingLines) {
|
| 441 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 442 |
+
output.push(` ${lineNum} ${line}`);
|
| 443 |
+
oldLineNum++;
|
| 444 |
+
newLineNum++;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
| 448 |
+
oldLineNum += skippedLines;
|
| 449 |
+
newLineNum += skippedLines;
|
| 450 |
+
|
| 451 |
+
for (const line of trailingLines) {
|
| 452 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 453 |
+
output.push(` ${lineNum} ${line}`);
|
| 454 |
+
oldLineNum++;
|
| 455 |
+
newLineNum++;
|
| 456 |
+
}
|
| 457 |
+
}
|
| 458 |
+
} else if (hasLeadingChange) {
|
| 459 |
+
const shownLines = raw.slice(0, contextLines);
|
| 460 |
+
const skippedLines = raw.length - shownLines.length;
|
| 461 |
+
|
| 462 |
+
for (const line of shownLines) {
|
| 463 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 464 |
+
output.push(` ${lineNum} ${line}`);
|
| 465 |
+
oldLineNum++;
|
| 466 |
+
newLineNum++;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
if (skippedLines > 0) {
|
| 470 |
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
| 471 |
+
oldLineNum += skippedLines;
|
| 472 |
+
newLineNum += skippedLines;
|
| 473 |
+
}
|
| 474 |
+
} else if (hasTrailingChange) {
|
| 475 |
+
const skippedLines = Math.max(0, raw.length - contextLines);
|
| 476 |
+
if (skippedLines > 0) {
|
| 477 |
+
output.push(` ${"".padStart(lineNumWidth, " ")} ...`);
|
| 478 |
+
oldLineNum += skippedLines;
|
| 479 |
+
newLineNum += skippedLines;
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
for (const line of raw.slice(skippedLines)) {
|
| 483 |
+
const lineNum = String(oldLineNum).padStart(lineNumWidth, " ");
|
| 484 |
+
output.push(` ${lineNum} ${line}`);
|
| 485 |
+
oldLineNum++;
|
| 486 |
+
newLineNum++;
|
| 487 |
+
}
|
| 488 |
+
} else {
|
| 489 |
+
// Skip these context lines entirely
|
| 490 |
+
oldLineNum += raw.length;
|
| 491 |
+
newLineNum += raw.length;
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
lastWasChange = false;
|
| 495 |
+
}
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
return { diff: output.join("\n"), firstChangedLine };
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
export interface EditDiffResult {
|
| 502 |
+
diff: string;
|
| 503 |
+
firstChangedLine: number | undefined;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
export interface EditDiffError {
|
| 507 |
+
error: string;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/**
|
| 511 |
+
* Compute the diff for one or more edit operations without applying them.
|
| 512 |
+
* Used for preview rendering in the TUI before the tool executes.
|
| 513 |
+
*/
|
| 514 |
+
export async function computeEditsDiff(
|
| 515 |
+
path: string,
|
| 516 |
+
edits: Edit[],
|
| 517 |
+
cwd: string,
|
| 518 |
+
): Promise<EditDiffResult | EditDiffError> {
|
| 519 |
+
const absolutePath = resolveToCwd(path, cwd);
|
| 520 |
+
|
| 521 |
+
try {
|
| 522 |
+
// Check if file exists and is readable
|
| 523 |
+
try {
|
| 524 |
+
await access(absolutePath, constants.R_OK);
|
| 525 |
+
} catch (error: unknown) {
|
| 526 |
+
const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
| 527 |
+
return { error: `Could not edit file: ${path}. ${errorMessage}.` };
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
// Read the file
|
| 531 |
+
const rawContent = await readFile(absolutePath, "utf-8");
|
| 532 |
+
|
| 533 |
+
// Strip BOM before matching (LLM won't include invisible BOM in oldText)
|
| 534 |
+
const { text: content } = splitBom(rawContent);
|
| 535 |
+
const normalizedContent = normalizeToLF(content);
|
| 536 |
+
const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
|
| 537 |
+
|
| 538 |
+
// Generate the diff
|
| 539 |
+
return generateDiffString(baseContent, newContent);
|
| 540 |
+
} catch (err) {
|
| 541 |
+
return { error: err instanceof Error ? err.message : String(err) };
|
| 542 |
+
}
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
/**
|
| 546 |
+
* Compute the diff for a single edit operation without applying it.
|
| 547 |
+
* Kept as a convenience wrapper for single-edit callers.
|
| 548 |
+
*/
|
| 549 |
+
export async function computeEditDiff(
|
| 550 |
+
path: string,
|
| 551 |
+
oldText: string,
|
| 552 |
+
newText: string,
|
| 553 |
+
cwd: string,
|
| 554 |
+
): Promise<EditDiffResult | EditDiffError> {
|
| 555 |
+
return computeEditsDiff(path, [{ oldText, newText }], cwd);
|
| 556 |
+
}
|
packages/coding-agent/src/core/tools/edit.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 2 |
+
import { constants } from "fs";
|
| 3 |
+
import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
|
| 4 |
+
import { type Static, Type } from "typebox";
|
| 5 |
+
import { splitBom } from "../../utils/text.ts";
|
| 6 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 7 |
+
import {
|
| 8 |
+
applyEditsToNormalizedContent,
|
| 9 |
+
detectLineEnding,
|
| 10 |
+
type Edit,
|
| 11 |
+
generateDiffString,
|
| 12 |
+
generateUnifiedPatch,
|
| 13 |
+
normalizeToLF,
|
| 14 |
+
restoreLineEndings,
|
| 15 |
+
} from "./edit-diff.ts";
|
| 16 |
+
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
| 17 |
+
import { resolveToCwd } from "./path-utils.ts";
|
| 18 |
+
import { type EditRenderState, editRenderers } from "./renderers/edit.ts";
|
| 19 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 20 |
+
|
| 21 |
+
const replaceEditSchema = Type.Object(
|
| 22 |
+
{
|
| 23 |
+
oldText: Type.String({
|
| 24 |
+
description:
|
| 25 |
+
"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.",
|
| 26 |
+
}),
|
| 27 |
+
newText: Type.String({ description: "Replacement text for this targeted edit." }),
|
| 28 |
+
},
|
| 29 |
+
{},
|
| 30 |
+
);
|
| 31 |
+
|
| 32 |
+
const editSchema = Type.Object(
|
| 33 |
+
{
|
| 34 |
+
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
| 35 |
+
edits: Type.Array(replaceEditSchema, {
|
| 36 |
+
description:
|
| 37 |
+
"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.",
|
| 38 |
+
}),
|
| 39 |
+
},
|
| 40 |
+
{},
|
| 41 |
+
);
|
| 42 |
+
|
| 43 |
+
export const editToolSystemPromptContribution = {
|
| 44 |
+
snippet: "Make precise file edits with exact text replacement, including multiple disjoint edits in one call",
|
| 45 |
+
guidelines: [
|
| 46 |
+
"Use edit for precise changes (edits[].oldText must match exactly)",
|
| 47 |
+
"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls",
|
| 48 |
+
"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.",
|
| 49 |
+
"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
|
| 50 |
+
],
|
| 51 |
+
} as const;
|
| 52 |
+
|
| 53 |
+
export type EditToolInput = Static<typeof editSchema>;
|
| 54 |
+
type LegacyEditToolInput = EditToolInput & {
|
| 55 |
+
oldText?: unknown;
|
| 56 |
+
newText?: unknown;
|
| 57 |
+
};
|
| 58 |
+
|
| 59 |
+
type SingleEditInput = { oldText: string; newText: string };
|
| 60 |
+
|
| 61 |
+
function isSingleEditInput(value: unknown): value is SingleEditInput {
|
| 62 |
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
| 63 |
+
return false;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
const edit = value as Record<string, unknown>;
|
| 67 |
+
return typeof edit.oldText === "string" && typeof edit.newText === "string";
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export interface EditToolDetails {
|
| 71 |
+
/** Display-oriented diff of the changes made */
|
| 72 |
+
diff: string;
|
| 73 |
+
/** Standard unified patch of the changes made */
|
| 74 |
+
patch: string;
|
| 75 |
+
/** Line number of the first change in the new file (for editor navigation) */
|
| 76 |
+
firstChangedLine?: number;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
/**
|
| 80 |
+
* Pluggable operations for the edit tool.
|
| 81 |
+
* Override these to delegate file editing to remote systems (for example SSH).
|
| 82 |
+
*/
|
| 83 |
+
export interface EditOperations {
|
| 84 |
+
/** Read file contents as a Buffer */
|
| 85 |
+
readFile: (absolutePath: string) => Promise<Buffer>;
|
| 86 |
+
/** Write content to a file */
|
| 87 |
+
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
| 88 |
+
/** Check if file is readable and writable (throw if not) */
|
| 89 |
+
access: (absolutePath: string) => Promise<void>;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const defaultEditOperations: EditOperations = {
|
| 93 |
+
readFile: (path) => fsReadFile(path),
|
| 94 |
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
| 95 |
+
access: (path) => fsAccess(path, constants.R_OK | constants.W_OK),
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
export interface EditToolOptions {
|
| 99 |
+
/** Custom operations for file editing. Default: local filesystem */
|
| 100 |
+
operations?: EditOperations;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
function prepareEditArguments(input: unknown): EditToolInput {
|
| 104 |
+
if (!input || typeof input !== "object") {
|
| 105 |
+
return input as EditToolInput;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const args = input as Record<string, unknown>;
|
| 109 |
+
|
| 110 |
+
// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array.
|
| 111 |
+
// Others send a single edit object instead of a one-element edits array.
|
| 112 |
+
if (typeof args.edits === "string") {
|
| 113 |
+
try {
|
| 114 |
+
const parsed = JSON.parse(args.edits);
|
| 115 |
+
if (Array.isArray(parsed)) {
|
| 116 |
+
args.edits = parsed;
|
| 117 |
+
} else if (isSingleEditInput(parsed)) {
|
| 118 |
+
args.edits = [parsed];
|
| 119 |
+
}
|
| 120 |
+
} catch {}
|
| 121 |
+
} else if (isSingleEditInput(args.edits)) {
|
| 122 |
+
args.edits = [args.edits];
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
const legacy = args as LegacyEditToolInput;
|
| 126 |
+
if (typeof legacy.oldText !== "string" || typeof legacy.newText !== "string") {
|
| 127 |
+
return args as EditToolInput;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
const edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];
|
| 131 |
+
edits.push({ oldText: legacy.oldText, newText: legacy.newText });
|
| 132 |
+
const { oldText: _oldText, newText: _newText, ...rest } = legacy;
|
| 133 |
+
return { ...rest, edits } as EditToolInput;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
function validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {
|
| 137 |
+
if (!Array.isArray(input.edits) || input.edits.length === 0) {
|
| 138 |
+
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
|
| 139 |
+
}
|
| 140 |
+
return { path: input.path, edits: input.edits };
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
export function createEditToolDefinition(
|
| 144 |
+
cwd: string,
|
| 145 |
+
options?: EditToolOptions,
|
| 146 |
+
): ToolDefinition<typeof editSchema, EditToolDetails | undefined, EditRenderState> {
|
| 147 |
+
const ops = options?.operations ?? defaultEditOperations;
|
| 148 |
+
return {
|
| 149 |
+
name: "edit",
|
| 150 |
+
label: "edit",
|
| 151 |
+
description:
|
| 152 |
+
"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.",
|
| 153 |
+
promptSnippet: editToolSystemPromptContribution.snippet,
|
| 154 |
+
promptGuidelines: [...editToolSystemPromptContribution.guidelines],
|
| 155 |
+
parameters: editSchema,
|
| 156 |
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
| 157 |
+
renderShell: "self",
|
| 158 |
+
prepareArguments: prepareEditArguments,
|
| 159 |
+
async execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, ctx?: ExtensionContext) {
|
| 160 |
+
const { path, edits } = validateEditInput(input);
|
| 161 |
+
const absolutePath = resolveToCwd(path, ctx?.cwd || cwd);
|
| 162 |
+
|
| 163 |
+
return withFileMutationQueue(absolutePath, async () => {
|
| 164 |
+
// Do not reject from an abort event listener here: that would release the
|
| 165 |
+
// mutation queue while an in-flight filesystem operation may still finish.
|
| 166 |
+
// Checking signal.aborted after each await observes the same aborts while
|
| 167 |
+
// keeping the queue locked until the current operation has settled.
|
| 168 |
+
const throwIfAborted = (): void => {
|
| 169 |
+
if (signal?.aborted) throw new Error("Operation aborted");
|
| 170 |
+
};
|
| 171 |
+
|
| 172 |
+
throwIfAborted();
|
| 173 |
+
|
| 174 |
+
// Check if file exists.
|
| 175 |
+
try {
|
| 176 |
+
await ops.access(absolutePath);
|
| 177 |
+
} catch (error: unknown) {
|
| 178 |
+
throwIfAborted();
|
| 179 |
+
const errorMessage =
|
| 180 |
+
error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
|
| 181 |
+
throw new Error(`Could not edit file: ${path}. ${errorMessage}.`);
|
| 182 |
+
}
|
| 183 |
+
throwIfAborted();
|
| 184 |
+
|
| 185 |
+
// Read the file.
|
| 186 |
+
const buffer = await ops.readFile(absolutePath);
|
| 187 |
+
const rawContent = buffer.toString("utf-8");
|
| 188 |
+
throwIfAborted();
|
| 189 |
+
|
| 190 |
+
// Strip BOM before matching. The model will not include an invisible BOM in oldText.
|
| 191 |
+
const { bom, text: content } = splitBom(rawContent);
|
| 192 |
+
const originalEnding = detectLineEnding(content);
|
| 193 |
+
const normalizedContent = normalizeToLF(content);
|
| 194 |
+
const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
|
| 195 |
+
throwIfAborted();
|
| 196 |
+
|
| 197 |
+
const finalContent = bom + restoreLineEndings(newContent, originalEnding);
|
| 198 |
+
await ops.writeFile(absolutePath, finalContent);
|
| 199 |
+
throwIfAborted();
|
| 200 |
+
|
| 201 |
+
const diffResult = generateDiffString(baseContent, newContent);
|
| 202 |
+
const patch = generateUnifiedPatch(path, baseContent, newContent);
|
| 203 |
+
return {
|
| 204 |
+
content: [
|
| 205 |
+
{
|
| 206 |
+
type: "text",
|
| 207 |
+
text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
|
| 208 |
+
},
|
| 209 |
+
],
|
| 210 |
+
details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
| 211 |
+
};
|
| 212 |
+
});
|
| 213 |
+
},
|
| 214 |
+
...editRenderers,
|
| 215 |
+
};
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
export function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {
|
| 219 |
+
return wrapToolDefinition(createEditToolDefinition(cwd, options));
|
| 220 |
+
}
|
packages/coding-agent/src/core/tools/file-mutation-queue.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { realpath } from "node:fs/promises";
|
| 2 |
+
import { resolve } from "node:path";
|
| 3 |
+
|
| 4 |
+
const fileMutationQueues = new Map<string, Promise<void>>();
|
| 5 |
+
let registrationQueue = Promise.resolve();
|
| 6 |
+
|
| 7 |
+
function isMissingPathError(error: unknown): boolean {
|
| 8 |
+
return (
|
| 9 |
+
typeof error === "object" &&
|
| 10 |
+
error !== null &&
|
| 11 |
+
"code" in error &&
|
| 12 |
+
(error.code === "ENOENT" || error.code === "ENOTDIR")
|
| 13 |
+
);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
async function getMutationQueueKey(filePath: string): Promise<string> {
|
| 17 |
+
const resolvedPath = resolve(filePath);
|
| 18 |
+
try {
|
| 19 |
+
return await realpath(resolvedPath);
|
| 20 |
+
} catch (error) {
|
| 21 |
+
if (isMissingPathError(error)) {
|
| 22 |
+
return resolvedPath;
|
| 23 |
+
}
|
| 24 |
+
throw error;
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Serialize file mutation operations targeting the same file.
|
| 30 |
+
* Operations for different files still run in parallel.
|
| 31 |
+
*/
|
| 32 |
+
export async function withFileMutationQueue<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
|
| 33 |
+
const registration = registrationQueue.then(async () => {
|
| 34 |
+
const key = await getMutationQueueKey(filePath);
|
| 35 |
+
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
|
| 36 |
+
|
| 37 |
+
let releaseNext!: () => void;
|
| 38 |
+
const nextQueue = new Promise<void>((resolveQueue) => {
|
| 39 |
+
releaseNext = resolveQueue;
|
| 40 |
+
});
|
| 41 |
+
const chainedQueue = currentQueue.then(() => nextQueue);
|
| 42 |
+
fileMutationQueues.set(key, chainedQueue);
|
| 43 |
+
|
| 44 |
+
return { key, currentQueue, chainedQueue, releaseNext };
|
| 45 |
+
});
|
| 46 |
+
registrationQueue = registration.then(
|
| 47 |
+
() => undefined,
|
| 48 |
+
() => undefined,
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
const { key, currentQueue, chainedQueue, releaseNext } = await registration;
|
| 52 |
+
await currentQueue;
|
| 53 |
+
try {
|
| 54 |
+
return await fn();
|
| 55 |
+
} finally {
|
| 56 |
+
releaseNext();
|
| 57 |
+
if (fileMutationQueues.get(key) === chainedQueue) {
|
| 58 |
+
fileMutationQueues.delete(key);
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
}
|
packages/coding-agent/src/core/tools/find.ts
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createInterface } from "node:readline";
|
| 2 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 3 |
+
import { spawn } from "child_process";
|
| 4 |
+
import path from "path";
|
| 5 |
+
import { type Static, Type } from "typebox";
|
| 6 |
+
import { ensureTool } from "../../utils/tools-manager.ts";
|
| 7 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 8 |
+
import { pathExists, resolveToCwd } from "./path-utils.ts";
|
| 9 |
+
import { findRenderers } from "./renderers/find.ts";
|
| 10 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 11 |
+
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
| 12 |
+
|
| 13 |
+
/** Relativize a find result against the search root and normalize it to posix separators. */
|
| 14 |
+
export function relativizeFindResultPath(
|
| 15 |
+
resultPath: string,
|
| 16 |
+
searchPath: string,
|
| 17 |
+
pathModule: path.PlatformPath = path,
|
| 18 |
+
): string {
|
| 19 |
+
const hadTrailingSeparator =
|
| 20 |
+
resultPath.endsWith(pathModule.sep) || (pathModule.sep === "\\" && resultPath.endsWith("/"));
|
| 21 |
+
const relativePath = pathModule.isAbsolute(resultPath) ? pathModule.relative(searchPath, resultPath) : resultPath;
|
| 22 |
+
const posixPath = relativePath.split(pathModule.sep).join("/");
|
| 23 |
+
return hadTrailingSeparator && !posixPath.endsWith("/") ? `${posixPath}/` : posixPath;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const findSchema = Type.Object({
|
| 27 |
+
pattern: Type.String({
|
| 28 |
+
description: "Glob pattern to match files, e.g. '*.ts', '**/*.json', or 'src/**/*.spec.ts'",
|
| 29 |
+
}),
|
| 30 |
+
path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
|
| 31 |
+
limit: Type.Optional(Type.Number({ description: "Maximum number of results (default: 1000)" })),
|
| 32 |
+
});
|
| 33 |
+
|
| 34 |
+
export const findToolSystemPromptContribution = {
|
| 35 |
+
snippet: "Find files by glob pattern (respects .gitignore)",
|
| 36 |
+
guidelines: [],
|
| 37 |
+
} as const;
|
| 38 |
+
|
| 39 |
+
export type FindToolInput = Static<typeof findSchema>;
|
| 40 |
+
|
| 41 |
+
const DEFAULT_LIMIT = 1000;
|
| 42 |
+
|
| 43 |
+
export interface FindToolDetails {
|
| 44 |
+
truncation?: TruncationResult;
|
| 45 |
+
resultLimitReached?: number;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/**
|
| 49 |
+
* Pluggable operations for the find tool.
|
| 50 |
+
* Override these to delegate file search to remote systems (for example SSH).
|
| 51 |
+
*/
|
| 52 |
+
export interface FindOperations {
|
| 53 |
+
/** Check if path exists */
|
| 54 |
+
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
| 55 |
+
/** Find files matching glob pattern. Returns relative or absolute paths. */
|
| 56 |
+
glob: (pattern: string, cwd: string, options: { ignore: string[]; limit: number }) => Promise<string[]> | string[];
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
const defaultFindOperations: FindOperations = {
|
| 60 |
+
exists: pathExists,
|
| 61 |
+
// This is a placeholder. Actual fd execution happens in execute() when no custom glob is provided.
|
| 62 |
+
glob: () => [],
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
export interface FindToolOptions {
|
| 66 |
+
/** Custom operations for find. Default: local filesystem plus fd */
|
| 67 |
+
operations?: FindOperations;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function createFindToolDefinition(
|
| 71 |
+
cwd: string,
|
| 72 |
+
options?: FindToolOptions,
|
| 73 |
+
): ToolDefinition<typeof findSchema, FindToolDetails | undefined> {
|
| 74 |
+
const customOps = options?.operations;
|
| 75 |
+
return {
|
| 76 |
+
name: "find",
|
| 77 |
+
label: "find",
|
| 78 |
+
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
| 79 |
+
promptSnippet: findToolSystemPromptContribution.snippet,
|
| 80 |
+
parameters: findSchema,
|
| 81 |
+
async execute(
|
| 82 |
+
_toolCallId,
|
| 83 |
+
{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },
|
| 84 |
+
signal?: AbortSignal,
|
| 85 |
+
_onUpdate?,
|
| 86 |
+
ctx?: ExtensionContext,
|
| 87 |
+
) {
|
| 88 |
+
return new Promise((resolve, reject) => {
|
| 89 |
+
if (signal?.aborted) {
|
| 90 |
+
reject(new Error("Operation aborted"));
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
let settled = false;
|
| 95 |
+
let stopChild: (() => void) | undefined;
|
| 96 |
+
const settle = (fn: () => void) => {
|
| 97 |
+
if (settled) return;
|
| 98 |
+
settled = true;
|
| 99 |
+
signal?.removeEventListener("abort", onAbort);
|
| 100 |
+
stopChild = undefined;
|
| 101 |
+
fn();
|
| 102 |
+
};
|
| 103 |
+
const onAbort = () => {
|
| 104 |
+
stopChild?.();
|
| 105 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 106 |
+
};
|
| 107 |
+
signal?.addEventListener("abort", onAbort, { once: true });
|
| 108 |
+
|
| 109 |
+
(async () => {
|
| 110 |
+
try {
|
| 111 |
+
const searchPath = resolveToCwd(searchDir || ".", ctx?.cwd || cwd);
|
| 112 |
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
| 113 |
+
const ops = customOps ?? defaultFindOperations;
|
| 114 |
+
|
| 115 |
+
// If custom operations provide glob(), use that instead of fd.
|
| 116 |
+
if (customOps?.glob) {
|
| 117 |
+
if (!(await ops.exists(searchPath))) {
|
| 118 |
+
settle(() => reject(new Error(`Path not found: ${searchPath}`)));
|
| 119 |
+
return;
|
| 120 |
+
}
|
| 121 |
+
if (signal?.aborted) {
|
| 122 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
const results = await ops.glob(pattern, searchPath, {
|
| 126 |
+
ignore: ["**/node_modules/**", "**/.git/**"],
|
| 127 |
+
limit: effectiveLimit,
|
| 128 |
+
});
|
| 129 |
+
if (signal?.aborted) {
|
| 130 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
if (results.length === 0) {
|
| 134 |
+
settle(() =>
|
| 135 |
+
resolve({
|
| 136 |
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
| 137 |
+
details: undefined,
|
| 138 |
+
}),
|
| 139 |
+
);
|
| 140 |
+
return;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
// Relativize paths against the search root for stable output.
|
| 144 |
+
const relativized = results.map((p) => relativizeFindResultPath(p, searchPath));
|
| 145 |
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
| 146 |
+
const rawOutput = relativized.join("\n");
|
| 147 |
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
| 148 |
+
let resultOutput = truncation.content;
|
| 149 |
+
const details: FindToolDetails = {};
|
| 150 |
+
const notices: string[] = [];
|
| 151 |
+
if (resultLimitReached) {
|
| 152 |
+
notices.push(`${effectiveLimit} results limit reached`);
|
| 153 |
+
details.resultLimitReached = effectiveLimit;
|
| 154 |
+
}
|
| 155 |
+
if (truncation.truncated) {
|
| 156 |
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
| 157 |
+
details.truncation = truncation;
|
| 158 |
+
}
|
| 159 |
+
if (notices.length > 0) {
|
| 160 |
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
| 161 |
+
}
|
| 162 |
+
settle(() =>
|
| 163 |
+
resolve({
|
| 164 |
+
content: [{ type: "text", text: resultOutput }],
|
| 165 |
+
details: Object.keys(details).length > 0 ? details : undefined,
|
| 166 |
+
}),
|
| 167 |
+
);
|
| 168 |
+
return;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
// Default implementation uses fd.
|
| 172 |
+
const fdPath = await ensureTool("fd");
|
| 173 |
+
if (signal?.aborted) {
|
| 174 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 175 |
+
return;
|
| 176 |
+
}
|
| 177 |
+
if (!fdPath) {
|
| 178 |
+
settle(() => reject(new Error("fd is not available and could not be downloaded")));
|
| 179 |
+
return;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
const args: string[] = ["--glob", "--color=never", "--hidden"];
|
| 183 |
+
|
| 184 |
+
// fd normally ignores .gitignore outside git repos, so keep --no-require-git
|
| 185 |
+
// there. Inside repos, use fd's default git-aware behavior so parent
|
| 186 |
+
// .gitignore rules stop at nested repo boundaries:
|
| 187 |
+
// https://github.com/earendil-works/pi/issues/5960
|
| 188 |
+
let insideGitRepo = false;
|
| 189 |
+
for (let current = searchPath; ; ) {
|
| 190 |
+
if (await pathExists(path.join(current, ".git"))) {
|
| 191 |
+
insideGitRepo = true;
|
| 192 |
+
break;
|
| 193 |
+
}
|
| 194 |
+
const parent = path.dirname(current);
|
| 195 |
+
if (parent === current) break;
|
| 196 |
+
current = parent;
|
| 197 |
+
}
|
| 198 |
+
if (!insideGitRepo) args.push("--no-require-git");
|
| 199 |
+
args.push("--max-results", String(effectiveLimit));
|
| 200 |
+
|
| 201 |
+
// fd --glob matches against the basename unless --full-path is set; in --full-path
|
| 202 |
+
// mode it matches against the absolute candidate path, so a path-containing
|
| 203 |
+
// pattern like 'src/**/*.spec.ts' needs a leading '**/' to match anything.
|
| 204 |
+
let effectivePattern = pattern;
|
| 205 |
+
if (pattern.includes("/")) {
|
| 206 |
+
args.push("--full-path");
|
| 207 |
+
if (!pattern.startsWith("/") && !pattern.startsWith("**/") && pattern !== "**") {
|
| 208 |
+
effectivePattern = `**/${pattern}`;
|
| 209 |
+
}
|
| 210 |
+
// fd matches full paths using native separators on Windows.
|
| 211 |
+
if (process.platform === "win32")
|
| 212 |
+
effectivePattern = effectivePattern.replaceAll("/", String.raw`[/\\]`);
|
| 213 |
+
}
|
| 214 |
+
args.push("--", effectivePattern, searchPath);
|
| 215 |
+
|
| 216 |
+
const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
| 217 |
+
const rl = createInterface({ input: child.stdout });
|
| 218 |
+
let stderr = "";
|
| 219 |
+
const lines: string[] = [];
|
| 220 |
+
|
| 221 |
+
stopChild = () => {
|
| 222 |
+
if (!child.killed) {
|
| 223 |
+
child.kill();
|
| 224 |
+
}
|
| 225 |
+
};
|
| 226 |
+
|
| 227 |
+
const cleanup = () => {
|
| 228 |
+
rl.close();
|
| 229 |
+
};
|
| 230 |
+
|
| 231 |
+
child.stderr?.on("data", (chunk) => {
|
| 232 |
+
stderr += chunk.toString();
|
| 233 |
+
});
|
| 234 |
+
|
| 235 |
+
rl.on("line", (line) => {
|
| 236 |
+
lines.push(line);
|
| 237 |
+
});
|
| 238 |
+
|
| 239 |
+
child.on("error", (error) => {
|
| 240 |
+
cleanup();
|
| 241 |
+
settle(() => reject(new Error(`Failed to run fd: ${error.message}`)));
|
| 242 |
+
});
|
| 243 |
+
|
| 244 |
+
child.on("close", (code) => {
|
| 245 |
+
cleanup();
|
| 246 |
+
if (signal?.aborted) {
|
| 247 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 248 |
+
return;
|
| 249 |
+
}
|
| 250 |
+
const output = lines.join("\n");
|
| 251 |
+
if (code !== 0) {
|
| 252 |
+
const errorMsg = stderr.trim() || `fd exited with code ${code}`;
|
| 253 |
+
if (!output) {
|
| 254 |
+
settle(() => reject(new Error(errorMsg)));
|
| 255 |
+
return;
|
| 256 |
+
}
|
| 257 |
+
}
|
| 258 |
+
if (!output) {
|
| 259 |
+
settle(() =>
|
| 260 |
+
resolve({
|
| 261 |
+
content: [{ type: "text", text: "No files found matching pattern" }],
|
| 262 |
+
details: undefined,
|
| 263 |
+
}),
|
| 264 |
+
);
|
| 265 |
+
return;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
const relativized: string[] = [];
|
| 269 |
+
for (const rawLine of lines) {
|
| 270 |
+
const line = rawLine.replace(/\r$/, "").trim();
|
| 271 |
+
if (!line) continue;
|
| 272 |
+
relativized.push(relativizeFindResultPath(line, searchPath));
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
const resultLimitReached = relativized.length >= effectiveLimit;
|
| 276 |
+
const rawOutput = relativized.join("\n");
|
| 277 |
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
| 278 |
+
let resultOutput = truncation.content;
|
| 279 |
+
const details: FindToolDetails = {};
|
| 280 |
+
const notices: string[] = [];
|
| 281 |
+
if (resultLimitReached) {
|
| 282 |
+
notices.push(
|
| 283 |
+
`${effectiveLimit} results limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
| 284 |
+
);
|
| 285 |
+
details.resultLimitReached = effectiveLimit;
|
| 286 |
+
}
|
| 287 |
+
if (truncation.truncated) {
|
| 288 |
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
| 289 |
+
details.truncation = truncation;
|
| 290 |
+
}
|
| 291 |
+
if (notices.length > 0) {
|
| 292 |
+
resultOutput += `\n\n[${notices.join(". ")}]`;
|
| 293 |
+
}
|
| 294 |
+
settle(() =>
|
| 295 |
+
resolve({
|
| 296 |
+
content: [{ type: "text", text: resultOutput }],
|
| 297 |
+
details: Object.keys(details).length > 0 ? details : undefined,
|
| 298 |
+
}),
|
| 299 |
+
);
|
| 300 |
+
});
|
| 301 |
+
} catch (e) {
|
| 302 |
+
if (signal?.aborted) {
|
| 303 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 304 |
+
return;
|
| 305 |
+
}
|
| 306 |
+
const error = e instanceof Error ? e : new Error(String(e));
|
| 307 |
+
settle(() => reject(error));
|
| 308 |
+
}
|
| 309 |
+
})();
|
| 310 |
+
});
|
| 311 |
+
},
|
| 312 |
+
...findRenderers,
|
| 313 |
+
};
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
export function createFindTool(cwd: string, options?: FindToolOptions): AgentTool<typeof findSchema> {
|
| 317 |
+
return wrapToolDefinition(createFindToolDefinition(cwd, options));
|
| 318 |
+
}
|
packages/coding-agent/src/core/tools/grep.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
|
| 2 |
+
import { createInterface } from "node:readline";
|
| 3 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 4 |
+
import { spawn } from "child_process";
|
| 5 |
+
import path from "path";
|
| 6 |
+
import { type Static, Type } from "typebox";
|
| 7 |
+
import { ensureTool } from "../../utils/tools-manager.ts";
|
| 8 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 9 |
+
import { resolveToCwd } from "./path-utils.ts";
|
| 10 |
+
import { grepRenderers } from "./renderers/grep.ts";
|
| 11 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 12 |
+
import {
|
| 13 |
+
DEFAULT_MAX_BYTES,
|
| 14 |
+
formatSize,
|
| 15 |
+
GREP_MAX_LINE_LENGTH,
|
| 16 |
+
type TruncationResult,
|
| 17 |
+
truncateHead,
|
| 18 |
+
truncateLine,
|
| 19 |
+
} from "./truncate.ts";
|
| 20 |
+
|
| 21 |
+
const grepSchema = Type.Object({
|
| 22 |
+
pattern: Type.String({ description: "Search pattern (regex or literal string)" }),
|
| 23 |
+
path: Type.Optional(Type.String({ description: "Directory or file to search (default: current directory)" })),
|
| 24 |
+
glob: Type.Optional(Type.String({ description: "Filter files by glob pattern, e.g. '*.ts' or '**/*.spec.ts'" })),
|
| 25 |
+
ignoreCase: Type.Optional(Type.Boolean({ description: "Case-insensitive search (default: false)" })),
|
| 26 |
+
literal: Type.Optional(
|
| 27 |
+
Type.Boolean({ description: "Treat pattern as literal string instead of regex (default: false)" }),
|
| 28 |
+
),
|
| 29 |
+
context: Type.Optional(
|
| 30 |
+
Type.Number({ description: "Number of lines to show before and after each match (default: 0)" }),
|
| 31 |
+
),
|
| 32 |
+
limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
export const grepToolSystemPromptContribution = {
|
| 36 |
+
snippet: "Search file contents for patterns (respects .gitignore)",
|
| 37 |
+
guidelines: [],
|
| 38 |
+
} as const;
|
| 39 |
+
|
| 40 |
+
export type GrepToolInput = Static<typeof grepSchema>;
|
| 41 |
+
const DEFAULT_LIMIT = 100;
|
| 42 |
+
|
| 43 |
+
export interface GrepToolDetails {
|
| 44 |
+
truncation?: TruncationResult;
|
| 45 |
+
matchLimitReached?: number;
|
| 46 |
+
linesTruncated?: boolean;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/**
|
| 50 |
+
* Pluggable operations for the grep tool.
|
| 51 |
+
* Override these to delegate search to remote systems (for example SSH).
|
| 52 |
+
*/
|
| 53 |
+
export interface GrepOperations {
|
| 54 |
+
/** Check if path is a directory. Throws if path does not exist. */
|
| 55 |
+
isDirectory: (absolutePath: string) => Promise<boolean> | boolean;
|
| 56 |
+
/** Read file contents for context lines */
|
| 57 |
+
readFile: (absolutePath: string) => Promise<string> | string;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const defaultGrepOperations: GrepOperations = {
|
| 61 |
+
isDirectory: async (p) => (await fsStat(p)).isDirectory(),
|
| 62 |
+
readFile: (p) => fsReadFile(p, "utf-8"),
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
export interface GrepToolOptions {
|
| 66 |
+
/** Custom operations for grep. Default: local filesystem plus ripgrep */
|
| 67 |
+
operations?: GrepOperations;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
export function createGrepToolDefinition(
|
| 71 |
+
cwd: string,
|
| 72 |
+
options?: GrepToolOptions,
|
| 73 |
+
): ToolDefinition<typeof grepSchema, GrepToolDetails | undefined> {
|
| 74 |
+
const customOps = options?.operations;
|
| 75 |
+
return {
|
| 76 |
+
name: "grep",
|
| 77 |
+
label: "grep",
|
| 78 |
+
description: `Search file contents for a pattern. Returns matching lines with file paths and line numbers. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} matches or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Long lines are truncated to ${GREP_MAX_LINE_LENGTH} chars.`,
|
| 79 |
+
promptSnippet: grepToolSystemPromptContribution.snippet,
|
| 80 |
+
parameters: grepSchema,
|
| 81 |
+
async execute(
|
| 82 |
+
_toolCallId,
|
| 83 |
+
{
|
| 84 |
+
pattern,
|
| 85 |
+
path: searchDir,
|
| 86 |
+
glob,
|
| 87 |
+
ignoreCase,
|
| 88 |
+
literal,
|
| 89 |
+
context,
|
| 90 |
+
limit,
|
| 91 |
+
}: {
|
| 92 |
+
pattern: string;
|
| 93 |
+
path?: string;
|
| 94 |
+
glob?: string;
|
| 95 |
+
ignoreCase?: boolean;
|
| 96 |
+
literal?: boolean;
|
| 97 |
+
context?: number;
|
| 98 |
+
limit?: number;
|
| 99 |
+
},
|
| 100 |
+
signal?: AbortSignal,
|
| 101 |
+
_onUpdate?,
|
| 102 |
+
ctx?: ExtensionContext,
|
| 103 |
+
) {
|
| 104 |
+
return new Promise((resolve, reject) => {
|
| 105 |
+
if (signal?.aborted) {
|
| 106 |
+
reject(new Error("Operation aborted"));
|
| 107 |
+
return;
|
| 108 |
+
}
|
| 109 |
+
let settled = false;
|
| 110 |
+
const settle = (fn: () => void) => {
|
| 111 |
+
if (!settled) {
|
| 112 |
+
settled = true;
|
| 113 |
+
fn();
|
| 114 |
+
}
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
(async () => {
|
| 118 |
+
try {
|
| 119 |
+
const rgPath = await ensureTool("rg");
|
| 120 |
+
if (!rgPath) {
|
| 121 |
+
settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded")));
|
| 122 |
+
return;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
const searchPath = resolveToCwd(searchDir || ".", ctx?.cwd || cwd);
|
| 126 |
+
const ops = customOps ?? defaultGrepOperations;
|
| 127 |
+
let isDirectory: boolean;
|
| 128 |
+
try {
|
| 129 |
+
isDirectory = await ops.isDirectory(searchPath);
|
| 130 |
+
} catch {
|
| 131 |
+
settle(() => reject(new Error(`Path not found: ${searchPath}`)));
|
| 132 |
+
return;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
const contextValue = context && context > 0 ? context : 0;
|
| 136 |
+
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
| 137 |
+
const formatPath = (filePath: string): string => {
|
| 138 |
+
if (isDirectory) {
|
| 139 |
+
const relative = path.relative(searchPath, filePath);
|
| 140 |
+
if (relative && !relative.startsWith("..")) {
|
| 141 |
+
return relative.replace(/\\/g, "/");
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
return path.basename(filePath);
|
| 145 |
+
};
|
| 146 |
+
|
| 147 |
+
const fileCache = new Map<string, string[]>();
|
| 148 |
+
const getFileLines = async (filePath: string): Promise<string[]> => {
|
| 149 |
+
let lines = fileCache.get(filePath);
|
| 150 |
+
if (!lines) {
|
| 151 |
+
try {
|
| 152 |
+
const content = await ops.readFile(filePath);
|
| 153 |
+
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
| 154 |
+
} catch {
|
| 155 |
+
lines = [];
|
| 156 |
+
}
|
| 157 |
+
fileCache.set(filePath, lines);
|
| 158 |
+
}
|
| 159 |
+
return lines;
|
| 160 |
+
};
|
| 161 |
+
|
| 162 |
+
const args: string[] = ["--json", "--line-number", "--color=never", "--hidden"];
|
| 163 |
+
if (ignoreCase) args.push("--ignore-case");
|
| 164 |
+
if (literal) args.push("--fixed-strings");
|
| 165 |
+
if (glob) args.push("--glob", glob);
|
| 166 |
+
args.push("--", pattern, searchPath);
|
| 167 |
+
|
| 168 |
+
const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
| 169 |
+
const rl = createInterface({ input: child.stdout });
|
| 170 |
+
let stderr = "";
|
| 171 |
+
let matchCount = 0;
|
| 172 |
+
let matchLimitReached = false;
|
| 173 |
+
let linesTruncated = false;
|
| 174 |
+
let aborted = false;
|
| 175 |
+
let killedDueToLimit = false;
|
| 176 |
+
const outputLines: string[] = [];
|
| 177 |
+
|
| 178 |
+
const cleanup = () => {
|
| 179 |
+
rl.close();
|
| 180 |
+
signal?.removeEventListener("abort", onAbort);
|
| 181 |
+
};
|
| 182 |
+
const stopChild = (dueToLimit = false) => {
|
| 183 |
+
if (!child.killed) {
|
| 184 |
+
killedDueToLimit = dueToLimit;
|
| 185 |
+
child.kill();
|
| 186 |
+
}
|
| 187 |
+
};
|
| 188 |
+
const onAbort = () => {
|
| 189 |
+
aborted = true;
|
| 190 |
+
stopChild();
|
| 191 |
+
};
|
| 192 |
+
signal?.addEventListener("abort", onAbort, { once: true });
|
| 193 |
+
child.stderr?.on("data", (chunk) => {
|
| 194 |
+
stderr += chunk.toString();
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {
|
| 198 |
+
const relativePath = formatPath(filePath);
|
| 199 |
+
const lines = await getFileLines(filePath);
|
| 200 |
+
if (!lines.length) return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
| 201 |
+
const block: string[] = [];
|
| 202 |
+
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
| 203 |
+
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
| 204 |
+
for (let current = start; current <= end; current++) {
|
| 205 |
+
const lineText = lines[current - 1] ?? "";
|
| 206 |
+
const sanitized = lineText.replace(/\r/g, "");
|
| 207 |
+
const isMatchLine = current === lineNumber;
|
| 208 |
+
// Truncate long lines so grep output stays compact.
|
| 209 |
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
| 210 |
+
if (wasTruncated) linesTruncated = true;
|
| 211 |
+
if (isMatchLine) block.push(`${relativePath}:${current}: ${truncatedText}`);
|
| 212 |
+
else block.push(`${relativePath}-${current}- ${truncatedText}`);
|
| 213 |
+
}
|
| 214 |
+
return block;
|
| 215 |
+
};
|
| 216 |
+
|
| 217 |
+
// Collect matches during streaming, then format them after rg exits.
|
| 218 |
+
const matches: Array<{ filePath: string; lineNumber: number; lineText?: string }> = [];
|
| 219 |
+
rl.on("line", (line) => {
|
| 220 |
+
if (!line.trim() || matchCount >= effectiveLimit) return;
|
| 221 |
+
let event: any;
|
| 222 |
+
try {
|
| 223 |
+
event = JSON.parse(line);
|
| 224 |
+
} catch {
|
| 225 |
+
return;
|
| 226 |
+
}
|
| 227 |
+
if (event.type === "match") {
|
| 228 |
+
matchCount++;
|
| 229 |
+
const filePath = event.data?.path?.text;
|
| 230 |
+
const lineNumber = event.data?.line_number;
|
| 231 |
+
const lineText = event.data?.lines?.text;
|
| 232 |
+
if (filePath && typeof lineNumber === "number")
|
| 233 |
+
matches.push({ filePath, lineNumber, lineText });
|
| 234 |
+
if (matchCount >= effectiveLimit) {
|
| 235 |
+
matchLimitReached = true;
|
| 236 |
+
stopChild(true);
|
| 237 |
+
}
|
| 238 |
+
}
|
| 239 |
+
});
|
| 240 |
+
|
| 241 |
+
child.on("error", (error) => {
|
| 242 |
+
cleanup();
|
| 243 |
+
settle(() => reject(new Error(`Failed to run ripgrep: ${error.message}`)));
|
| 244 |
+
});
|
| 245 |
+
child.on("close", async (code) => {
|
| 246 |
+
cleanup();
|
| 247 |
+
if (aborted) {
|
| 248 |
+
settle(() => reject(new Error("Operation aborted")));
|
| 249 |
+
return;
|
| 250 |
+
}
|
| 251 |
+
if (!killedDueToLimit && code !== 0 && code !== 1) {
|
| 252 |
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${code}`;
|
| 253 |
+
settle(() => reject(new Error(errorMsg)));
|
| 254 |
+
return;
|
| 255 |
+
}
|
| 256 |
+
if (matchCount === 0) {
|
| 257 |
+
settle(() =>
|
| 258 |
+
resolve({ content: [{ type: "text", text: "No matches found" }], details: undefined }),
|
| 259 |
+
);
|
| 260 |
+
return;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
// Format matches after streaming finishes so custom readFile() backends can be async.
|
| 264 |
+
for (const match of matches) {
|
| 265 |
+
if (contextValue === 0 && match.lineText !== undefined) {
|
| 266 |
+
const relativePath = formatPath(match.filePath);
|
| 267 |
+
const sanitized = match.lineText
|
| 268 |
+
.replace(/\r\n/g, "\n")
|
| 269 |
+
.replace(/\r/g, "")
|
| 270 |
+
.replace(/\n$/, "");
|
| 271 |
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
| 272 |
+
if (wasTruncated) linesTruncated = true;
|
| 273 |
+
outputLines.push(`${relativePath}:${match.lineNumber}: ${truncatedText}`);
|
| 274 |
+
} else {
|
| 275 |
+
const block = await formatBlock(match.filePath, match.lineNumber);
|
| 276 |
+
outputLines.push(...block);
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
const rawOutput = outputLines.join("\n");
|
| 281 |
+
// Apply byte truncation. There is no line limit here because the match limit already capped rows.
|
| 282 |
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
| 283 |
+
let output = truncation.content;
|
| 284 |
+
const details: GrepToolDetails = {};
|
| 285 |
+
// Build actionable notices for truncation and match limits.
|
| 286 |
+
const notices: string[] = [];
|
| 287 |
+
if (matchLimitReached) {
|
| 288 |
+
notices.push(
|
| 289 |
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
| 290 |
+
);
|
| 291 |
+
details.matchLimitReached = effectiveLimit;
|
| 292 |
+
}
|
| 293 |
+
if (truncation.truncated) {
|
| 294 |
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
| 295 |
+
details.truncation = truncation;
|
| 296 |
+
}
|
| 297 |
+
if (linesTruncated) {
|
| 298 |
+
notices.push(
|
| 299 |
+
`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`,
|
| 300 |
+
);
|
| 301 |
+
details.linesTruncated = true;
|
| 302 |
+
}
|
| 303 |
+
if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;
|
| 304 |
+
settle(() =>
|
| 305 |
+
resolve({
|
| 306 |
+
content: [{ type: "text", text: output }],
|
| 307 |
+
details: Object.keys(details).length > 0 ? details : undefined,
|
| 308 |
+
}),
|
| 309 |
+
);
|
| 310 |
+
});
|
| 311 |
+
} catch (err) {
|
| 312 |
+
settle(() => reject(err as Error));
|
| 313 |
+
}
|
| 314 |
+
})();
|
| 315 |
+
});
|
| 316 |
+
},
|
| 317 |
+
...grepRenderers,
|
| 318 |
+
};
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
export function createGrepTool(cwd: string, options?: GrepToolOptions): AgentTool<typeof grepSchema> {
|
| 322 |
+
return wrapToolDefinition(createGrepToolDefinition(cwd, options));
|
| 323 |
+
}
|
packages/coding-agent/src/core/tools/index.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export {
|
| 2 |
+
type BashOperations,
|
| 3 |
+
type BashSpawnContext,
|
| 4 |
+
type BashSpawnHook,
|
| 5 |
+
type BashToolDetails,
|
| 6 |
+
type BashToolInput,
|
| 7 |
+
type BashToolOptions,
|
| 8 |
+
createBashTool,
|
| 9 |
+
createBashToolDefinition,
|
| 10 |
+
createLocalBashOperations,
|
| 11 |
+
} from "./bash.ts";
|
| 12 |
+
export {
|
| 13 |
+
createEditTool,
|
| 14 |
+
createEditToolDefinition,
|
| 15 |
+
type EditOperations,
|
| 16 |
+
type EditToolDetails,
|
| 17 |
+
type EditToolInput,
|
| 18 |
+
type EditToolOptions,
|
| 19 |
+
} from "./edit.ts";
|
| 20 |
+
export { withFileMutationQueue } from "./file-mutation-queue.ts";
|
| 21 |
+
export {
|
| 22 |
+
createFindTool,
|
| 23 |
+
createFindToolDefinition,
|
| 24 |
+
type FindOperations,
|
| 25 |
+
type FindToolDetails,
|
| 26 |
+
type FindToolInput,
|
| 27 |
+
type FindToolOptions,
|
| 28 |
+
} from "./find.ts";
|
| 29 |
+
export {
|
| 30 |
+
createGrepTool,
|
| 31 |
+
createGrepToolDefinition,
|
| 32 |
+
type GrepOperations,
|
| 33 |
+
type GrepToolDetails,
|
| 34 |
+
type GrepToolInput,
|
| 35 |
+
type GrepToolOptions,
|
| 36 |
+
} from "./grep.ts";
|
| 37 |
+
export {
|
| 38 |
+
createLsTool,
|
| 39 |
+
createLsToolDefinition,
|
| 40 |
+
type LsOperations,
|
| 41 |
+
type LsToolDetails,
|
| 42 |
+
type LsToolInput,
|
| 43 |
+
type LsToolOptions,
|
| 44 |
+
} from "./ls.ts";
|
| 45 |
+
export {
|
| 46 |
+
createLocalPowerShellOperations,
|
| 47 |
+
createPowerShellTool,
|
| 48 |
+
createPowerShellToolDefinition,
|
| 49 |
+
type PowerShellOperations,
|
| 50 |
+
type PowerShellSpawnContext,
|
| 51 |
+
type PowerShellSpawnHook,
|
| 52 |
+
type PowerShellToolDetails,
|
| 53 |
+
type PowerShellToolInput,
|
| 54 |
+
type PowerShellToolOptions,
|
| 55 |
+
} from "./powershell.ts";
|
| 56 |
+
export {
|
| 57 |
+
createReadTool,
|
| 58 |
+
createReadToolDefinition,
|
| 59 |
+
type ReadOperations,
|
| 60 |
+
type ReadToolDetails,
|
| 61 |
+
type ReadToolInput,
|
| 62 |
+
type ReadToolOptions,
|
| 63 |
+
} from "./read.ts";
|
| 64 |
+
export {
|
| 65 |
+
DEFAULT_MAX_BYTES,
|
| 66 |
+
DEFAULT_MAX_LINES,
|
| 67 |
+
formatSize,
|
| 68 |
+
type TruncationOptions,
|
| 69 |
+
type TruncationResult,
|
| 70 |
+
truncateHead,
|
| 71 |
+
truncateLine,
|
| 72 |
+
truncateTail,
|
| 73 |
+
} from "./truncate.ts";
|
| 74 |
+
export {
|
| 75 |
+
createWriteTool,
|
| 76 |
+
createWriteToolDefinition,
|
| 77 |
+
type WriteOperations,
|
| 78 |
+
type WriteToolInput,
|
| 79 |
+
type WriteToolOptions,
|
| 80 |
+
} from "./write.ts";
|
| 81 |
+
|
| 82 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 83 |
+
import type { ToolDefinition } from "../extensions/types.ts";
|
| 84 |
+
import { type BashToolOptions, createBashTool, createBashToolDefinition } from "./bash.ts";
|
| 85 |
+
import { createEditTool, createEditToolDefinition, type EditToolOptions } from "./edit.ts";
|
| 86 |
+
import { createFindTool, createFindToolDefinition, type FindToolOptions } from "./find.ts";
|
| 87 |
+
import { createGrepTool, createGrepToolDefinition, type GrepToolOptions } from "./grep.ts";
|
| 88 |
+
import { createLsTool, createLsToolDefinition, type LsToolOptions } from "./ls.ts";
|
| 89 |
+
import { createPowerShellTool, createPowerShellToolDefinition, type PowerShellToolOptions } from "./powershell.ts";
|
| 90 |
+
import { createReadTool, createReadToolDefinition, type ReadToolOptions } from "./read.ts";
|
| 91 |
+
import { createWriteTool, createWriteToolDefinition, type WriteToolOptions } from "./write.ts";
|
| 92 |
+
|
| 93 |
+
export type Tool = AgentTool<any>;
|
| 94 |
+
export type ToolDef = ToolDefinition<any, any>;
|
| 95 |
+
export type ToolName = "read" | "bash" | "powershell" | "edit" | "write" | "grep" | "find" | "ls";
|
| 96 |
+
export const allToolNames: Set<ToolName> = new Set([
|
| 97 |
+
"read",
|
| 98 |
+
"bash",
|
| 99 |
+
"powershell",
|
| 100 |
+
"edit",
|
| 101 |
+
"write",
|
| 102 |
+
"grep",
|
| 103 |
+
"find",
|
| 104 |
+
"ls",
|
| 105 |
+
]);
|
| 106 |
+
|
| 107 |
+
export interface ToolsOptions {
|
| 108 |
+
read?: ReadToolOptions;
|
| 109 |
+
bash?: BashToolOptions;
|
| 110 |
+
powershell?: PowerShellToolOptions;
|
| 111 |
+
write?: WriteToolOptions;
|
| 112 |
+
edit?: EditToolOptions;
|
| 113 |
+
grep?: GrepToolOptions;
|
| 114 |
+
find?: FindToolOptions;
|
| 115 |
+
ls?: LsToolOptions;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export function createToolDefinition(toolName: ToolName, cwd: string, options?: ToolsOptions): ToolDef {
|
| 119 |
+
switch (toolName) {
|
| 120 |
+
case "read":
|
| 121 |
+
return createReadToolDefinition(cwd, options?.read);
|
| 122 |
+
case "bash":
|
| 123 |
+
return createBashToolDefinition(cwd, options?.bash);
|
| 124 |
+
case "powershell":
|
| 125 |
+
return createPowerShellToolDefinition(cwd, options?.powershell);
|
| 126 |
+
case "edit":
|
| 127 |
+
return createEditToolDefinition(cwd, options?.edit);
|
| 128 |
+
case "write":
|
| 129 |
+
return createWriteToolDefinition(cwd, options?.write);
|
| 130 |
+
case "grep":
|
| 131 |
+
return createGrepToolDefinition(cwd, options?.grep);
|
| 132 |
+
case "find":
|
| 133 |
+
return createFindToolDefinition(cwd, options?.find);
|
| 134 |
+
case "ls":
|
| 135 |
+
return createLsToolDefinition(cwd, options?.ls);
|
| 136 |
+
default:
|
| 137 |
+
throw new Error(`Unknown tool name: ${toolName}`);
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
export function createTool(toolName: ToolName, cwd: string, options?: ToolsOptions): Tool {
|
| 142 |
+
switch (toolName) {
|
| 143 |
+
case "read":
|
| 144 |
+
return createReadTool(cwd, options?.read);
|
| 145 |
+
case "bash":
|
| 146 |
+
return createBashTool(cwd, options?.bash);
|
| 147 |
+
case "powershell":
|
| 148 |
+
return createPowerShellTool(cwd, options?.powershell);
|
| 149 |
+
case "edit":
|
| 150 |
+
return createEditTool(cwd, options?.edit);
|
| 151 |
+
case "write":
|
| 152 |
+
return createWriteTool(cwd, options?.write);
|
| 153 |
+
case "grep":
|
| 154 |
+
return createGrepTool(cwd, options?.grep);
|
| 155 |
+
case "find":
|
| 156 |
+
return createFindTool(cwd, options?.find);
|
| 157 |
+
case "ls":
|
| 158 |
+
return createLsTool(cwd, options?.ls);
|
| 159 |
+
default:
|
| 160 |
+
throw new Error(`Unknown tool name: ${toolName}`);
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
export function createCodingToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] {
|
| 165 |
+
return [
|
| 166 |
+
createReadToolDefinition(cwd, options?.read),
|
| 167 |
+
createBashToolDefinition(cwd, options?.bash),
|
| 168 |
+
createEditToolDefinition(cwd, options?.edit),
|
| 169 |
+
createWriteToolDefinition(cwd, options?.write),
|
| 170 |
+
];
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
export function createReadOnlyToolDefinitions(cwd: string, options?: ToolsOptions): ToolDef[] {
|
| 174 |
+
return [
|
| 175 |
+
createReadToolDefinition(cwd, options?.read),
|
| 176 |
+
createGrepToolDefinition(cwd, options?.grep),
|
| 177 |
+
createFindToolDefinition(cwd, options?.find),
|
| 178 |
+
createLsToolDefinition(cwd, options?.ls),
|
| 179 |
+
];
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
export function createAllToolDefinitions(cwd: string, options?: ToolsOptions): Record<ToolName, ToolDef> {
|
| 183 |
+
return {
|
| 184 |
+
read: createReadToolDefinition(cwd, options?.read),
|
| 185 |
+
bash: createBashToolDefinition(cwd, options?.bash),
|
| 186 |
+
powershell: createPowerShellToolDefinition(cwd, options?.powershell),
|
| 187 |
+
edit: createEditToolDefinition(cwd, options?.edit),
|
| 188 |
+
write: createWriteToolDefinition(cwd, options?.write),
|
| 189 |
+
grep: createGrepToolDefinition(cwd, options?.grep),
|
| 190 |
+
find: createFindToolDefinition(cwd, options?.find),
|
| 191 |
+
ls: createLsToolDefinition(cwd, options?.ls),
|
| 192 |
+
};
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export function createCodingTools(cwd: string, options?: ToolsOptions): Tool[] {
|
| 196 |
+
return [
|
| 197 |
+
createReadTool(cwd, options?.read),
|
| 198 |
+
createBashTool(cwd, options?.bash),
|
| 199 |
+
createEditTool(cwd, options?.edit),
|
| 200 |
+
createWriteTool(cwd, options?.write),
|
| 201 |
+
];
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
export function createReadOnlyTools(cwd: string, options?: ToolsOptions): Tool[] {
|
| 205 |
+
return [
|
| 206 |
+
createReadTool(cwd, options?.read),
|
| 207 |
+
createGrepTool(cwd, options?.grep),
|
| 208 |
+
createFindTool(cwd, options?.find),
|
| 209 |
+
createLsTool(cwd, options?.ls),
|
| 210 |
+
];
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
export function createAllTools(cwd: string, options?: ToolsOptions): Record<ToolName, Tool> {
|
| 214 |
+
return {
|
| 215 |
+
read: createReadTool(cwd, options?.read),
|
| 216 |
+
bash: createBashTool(cwd, options?.bash),
|
| 217 |
+
powershell: createPowerShellTool(cwd, options?.powershell),
|
| 218 |
+
edit: createEditTool(cwd, options?.edit),
|
| 219 |
+
write: createWriteTool(cwd, options?.write),
|
| 220 |
+
grep: createGrepTool(cwd, options?.grep),
|
| 221 |
+
find: createFindTool(cwd, options?.find),
|
| 222 |
+
ls: createLsTool(cwd, options?.ls),
|
| 223 |
+
};
|
| 224 |
+
}
|
packages/coding-agent/src/core/tools/ls.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { readdir as fsReaddir, stat as fsStat } from "node:fs/promises";
|
| 2 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 3 |
+
import nodePath from "path";
|
| 4 |
+
import { type Static, Type } from "typebox";
|
| 5 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 6 |
+
import { pathExists, resolveToCwd } from "./path-utils.ts";
|
| 7 |
+
import { lsRenderers } from "./renderers/ls.ts";
|
| 8 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 9 |
+
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
| 10 |
+
|
| 11 |
+
const lsSchema = Type.Object({
|
| 12 |
+
path: Type.Optional(Type.String({ description: "Directory to list (default: current directory)" })),
|
| 13 |
+
limit: Type.Optional(Type.Number({ description: "Maximum number of entries to return (default: 500)" })),
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
export const lsToolSystemPromptContribution = {
|
| 17 |
+
snippet: "List directory contents",
|
| 18 |
+
guidelines: [],
|
| 19 |
+
} as const;
|
| 20 |
+
|
| 21 |
+
export type LsToolInput = Static<typeof lsSchema>;
|
| 22 |
+
|
| 23 |
+
const DEFAULT_LIMIT = 500;
|
| 24 |
+
|
| 25 |
+
export interface LsToolDetails {
|
| 26 |
+
truncation?: TruncationResult;
|
| 27 |
+
entryLimitReached?: number;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Pluggable operations for the ls tool.
|
| 32 |
+
* Override these to delegate directory listing to remote systems (for example SSH).
|
| 33 |
+
*/
|
| 34 |
+
export interface LsOperations {
|
| 35 |
+
/** Check if path exists */
|
| 36 |
+
exists: (absolutePath: string) => Promise<boolean> | boolean;
|
| 37 |
+
/** Get file or directory stats. Throws if not found. */
|
| 38 |
+
stat: (absolutePath: string) => Promise<{ isDirectory: () => boolean }> | { isDirectory: () => boolean };
|
| 39 |
+
/** Read directory entries */
|
| 40 |
+
readdir: (absolutePath: string) => Promise<string[]> | string[];
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const defaultLsOperations: LsOperations = {
|
| 44 |
+
exists: pathExists,
|
| 45 |
+
stat: fsStat,
|
| 46 |
+
readdir: fsReaddir,
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
export interface LsToolOptions {
|
| 50 |
+
/** Custom operations for directory listing. Default: local filesystem */
|
| 51 |
+
operations?: LsOperations;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function createLsToolDefinition(
|
| 55 |
+
cwd: string,
|
| 56 |
+
options?: LsToolOptions,
|
| 57 |
+
): ToolDefinition<typeof lsSchema, LsToolDetails | undefined> {
|
| 58 |
+
const ops = options?.operations ?? defaultLsOperations;
|
| 59 |
+
return {
|
| 60 |
+
name: "ls",
|
| 61 |
+
label: "ls",
|
| 62 |
+
description: `List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to ${DEFAULT_LIMIT} entries or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
| 63 |
+
promptSnippet: lsToolSystemPromptContribution.snippet,
|
| 64 |
+
parameters: lsSchema,
|
| 65 |
+
async execute(
|
| 66 |
+
_toolCallId,
|
| 67 |
+
{ path, limit }: { path?: string; limit?: number },
|
| 68 |
+
signal?: AbortSignal,
|
| 69 |
+
_onUpdate?,
|
| 70 |
+
ctx?: ExtensionContext,
|
| 71 |
+
) {
|
| 72 |
+
return new Promise((resolve, reject) => {
|
| 73 |
+
if (signal?.aborted) {
|
| 74 |
+
reject(new Error("Operation aborted"));
|
| 75 |
+
return;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const onAbort = () => reject(new Error("Operation aborted"));
|
| 79 |
+
signal?.addEventListener("abort", onAbort, { once: true });
|
| 80 |
+
|
| 81 |
+
(async () => {
|
| 82 |
+
try {
|
| 83 |
+
const dirPath = resolveToCwd(path || ".", ctx?.cwd || cwd);
|
| 84 |
+
const effectiveLimit = limit ?? DEFAULT_LIMIT;
|
| 85 |
+
|
| 86 |
+
// Check if path exists.
|
| 87 |
+
if (!(await ops.exists(dirPath))) {
|
| 88 |
+
reject(new Error(`Path not found: ${dirPath}`));
|
| 89 |
+
return;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Check if path is a directory.
|
| 93 |
+
const stat = await ops.stat(dirPath);
|
| 94 |
+
if (!stat.isDirectory()) {
|
| 95 |
+
reject(new Error(`Not a directory: ${dirPath}`));
|
| 96 |
+
return;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Read directory entries.
|
| 100 |
+
let entries: string[];
|
| 101 |
+
try {
|
| 102 |
+
entries = await ops.readdir(dirPath);
|
| 103 |
+
} catch (e: any) {
|
| 104 |
+
reject(new Error(`Cannot read directory: ${e.message}`));
|
| 105 |
+
return;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Sort alphabetically, case-insensitive.
|
| 109 |
+
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
| 110 |
+
|
| 111 |
+
// Format entries with directory indicators.
|
| 112 |
+
const results: string[] = [];
|
| 113 |
+
let entryLimitReached = false;
|
| 114 |
+
for (const entry of entries) {
|
| 115 |
+
if (results.length >= effectiveLimit) {
|
| 116 |
+
entryLimitReached = true;
|
| 117 |
+
break;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const fullPath = nodePath.join(dirPath, entry);
|
| 121 |
+
let suffix = "";
|
| 122 |
+
try {
|
| 123 |
+
const entryStat = await ops.stat(fullPath);
|
| 124 |
+
if (entryStat.isDirectory()) suffix = "/";
|
| 125 |
+
} catch {
|
| 126 |
+
// Skip entries we cannot stat.
|
| 127 |
+
continue;
|
| 128 |
+
}
|
| 129 |
+
results.push(entry + suffix);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
signal?.removeEventListener("abort", onAbort);
|
| 133 |
+
|
| 134 |
+
if (results.length === 0) {
|
| 135 |
+
resolve({ content: [{ type: "text", text: "(empty directory)" }], details: undefined });
|
| 136 |
+
return;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
const rawOutput = results.join("\n");
|
| 140 |
+
// Apply byte truncation. There is no separate line limit because entry count is already capped.
|
| 141 |
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
| 142 |
+
let output = truncation.content;
|
| 143 |
+
const details: LsToolDetails = {};
|
| 144 |
+
// Build actionable notices for truncation and entry limits.
|
| 145 |
+
const notices: string[] = [];
|
| 146 |
+
if (entryLimitReached) {
|
| 147 |
+
notices.push(`${effectiveLimit} entries limit reached. Use limit=${effectiveLimit * 2} for more`);
|
| 148 |
+
details.entryLimitReached = effectiveLimit;
|
| 149 |
+
}
|
| 150 |
+
if (truncation.truncated) {
|
| 151 |
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
| 152 |
+
details.truncation = truncation;
|
| 153 |
+
}
|
| 154 |
+
if (notices.length > 0) {
|
| 155 |
+
output += `\n\n[${notices.join(". ")}]`;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
resolve({
|
| 159 |
+
content: [{ type: "text", text: output }],
|
| 160 |
+
details: Object.keys(details).length > 0 ? details : undefined,
|
| 161 |
+
});
|
| 162 |
+
} catch (e: any) {
|
| 163 |
+
signal?.removeEventListener("abort", onAbort);
|
| 164 |
+
reject(e);
|
| 165 |
+
}
|
| 166 |
+
})();
|
| 167 |
+
});
|
| 168 |
+
},
|
| 169 |
+
...lsRenderers,
|
| 170 |
+
};
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
export function createLsTool(cwd: string, options?: LsToolOptions): AgentTool<typeof lsSchema> {
|
| 174 |
+
return wrapToolDefinition(createLsToolDefinition(cwd, options));
|
| 175 |
+
}
|
packages/coding-agent/src/core/tools/output-accumulator.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { randomBytes } from "node:crypto";
|
| 2 |
+
import { createWriteStream, type WriteStream } from "node:fs";
|
| 3 |
+
import { tmpdir } from "node:os";
|
| 4 |
+
import { join } from "node:path";
|
| 5 |
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult, truncateTail } from "./truncate.ts";
|
| 6 |
+
|
| 7 |
+
export interface OutputAccumulatorOptions {
|
| 8 |
+
maxLines?: number;
|
| 9 |
+
maxBytes?: number;
|
| 10 |
+
tempFilePrefix?: string;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface OutputSnapshot {
|
| 14 |
+
content: string;
|
| 15 |
+
truncation: TruncationResult;
|
| 16 |
+
fullOutputPath?: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function defaultTempFilePath(prefix: string): string {
|
| 20 |
+
const id = randomBytes(8).toString("hex");
|
| 21 |
+
return join(tmpdir(), `${prefix}-${id}.log`);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
function byteLength(text: string): number {
|
| 25 |
+
return Buffer.byteLength(text, "utf-8");
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/**
|
| 29 |
+
* Incrementally tracks streaming output with bounded memory.
|
| 30 |
+
*
|
| 31 |
+
* Appends decode chunks with a streaming UTF-8 decoder, keeps only a decoded
|
| 32 |
+
* tail for display snapshots, and opens a temp file when the full output needs
|
| 33 |
+
* to be preserved.
|
| 34 |
+
*/
|
| 35 |
+
export class OutputAccumulator {
|
| 36 |
+
private readonly maxLines: number;
|
| 37 |
+
private readonly maxBytes: number;
|
| 38 |
+
private readonly maxRollingBytes: number;
|
| 39 |
+
private readonly tempFilePrefix: string;
|
| 40 |
+
private readonly decoder = new TextDecoder();
|
| 41 |
+
|
| 42 |
+
private rawChunks: Buffer[] = [];
|
| 43 |
+
private tailText = "";
|
| 44 |
+
private tailBytes = 0;
|
| 45 |
+
private tailStartsAtLineBoundary = true;
|
| 46 |
+
private totalRawBytes = 0;
|
| 47 |
+
private totalDecodedBytes = 0;
|
| 48 |
+
private completedLines = 0;
|
| 49 |
+
private totalLines = 0;
|
| 50 |
+
private currentLineBytes = 0;
|
| 51 |
+
private hasOpenLine = false;
|
| 52 |
+
private finished = false;
|
| 53 |
+
|
| 54 |
+
private tempFilePath: string | undefined;
|
| 55 |
+
private tempFileStream: WriteStream | undefined;
|
| 56 |
+
|
| 57 |
+
constructor(options: OutputAccumulatorOptions = {}) {
|
| 58 |
+
this.maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
| 59 |
+
this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
| 60 |
+
this.maxRollingBytes = Math.max(this.maxBytes * 2, 1);
|
| 61 |
+
this.tempFilePrefix = options.tempFilePrefix ?? "pi-output";
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
append(data: Buffer): void {
|
| 65 |
+
if (this.finished) {
|
| 66 |
+
throw new Error("Cannot append to a finished output accumulator");
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
this.totalRawBytes += data.length;
|
| 70 |
+
this.appendDecodedText(this.decoder.decode(data, { stream: true }));
|
| 71 |
+
|
| 72 |
+
if (this.tempFileStream || this.shouldUseTempFile()) {
|
| 73 |
+
this.ensureTempFile();
|
| 74 |
+
this.tempFileStream?.write(data);
|
| 75 |
+
} else if (data.length > 0) {
|
| 76 |
+
this.rawChunks.push(data);
|
| 77 |
+
}
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
finish(): void {
|
| 81 |
+
if (this.finished) {
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
this.finished = true;
|
| 85 |
+
this.appendDecodedText(this.decoder.decode());
|
| 86 |
+
if (this.shouldUseTempFile()) {
|
| 87 |
+
this.ensureTempFile();
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {
|
| 92 |
+
const tailTruncation = truncateTail(this.getSnapshotText(), {
|
| 93 |
+
maxLines: this.maxLines,
|
| 94 |
+
maxBytes: this.maxBytes,
|
| 95 |
+
});
|
| 96 |
+
const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes;
|
| 97 |
+
const truncatedBy = truncated
|
| 98 |
+
? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes ? "bytes" : "lines"))
|
| 99 |
+
: null;
|
| 100 |
+
const truncation: TruncationResult = {
|
| 101 |
+
...tailTruncation,
|
| 102 |
+
truncated,
|
| 103 |
+
truncatedBy,
|
| 104 |
+
totalLines: this.totalLines,
|
| 105 |
+
totalBytes: this.totalDecodedBytes,
|
| 106 |
+
maxLines: this.maxLines,
|
| 107 |
+
maxBytes: this.maxBytes,
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
if (options.persistIfTruncated && truncation.truncated) {
|
| 111 |
+
this.ensureTempFile();
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
return {
|
| 115 |
+
content: truncation.content,
|
| 116 |
+
truncation,
|
| 117 |
+
fullOutputPath: this.tempFilePath,
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
async closeTempFile(): Promise<void> {
|
| 122 |
+
if (!this.tempFileStream) {
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
const stream = this.tempFileStream;
|
| 127 |
+
this.tempFileStream = undefined;
|
| 128 |
+
|
| 129 |
+
await new Promise<void>((resolve, reject) => {
|
| 130 |
+
const onError = (error: Error) => {
|
| 131 |
+
stream.off("finish", onFinish);
|
| 132 |
+
reject(error);
|
| 133 |
+
};
|
| 134 |
+
const onFinish = () => {
|
| 135 |
+
stream.off("error", onError);
|
| 136 |
+
resolve();
|
| 137 |
+
};
|
| 138 |
+
stream.once("error", onError);
|
| 139 |
+
stream.once("finish", onFinish);
|
| 140 |
+
stream.end();
|
| 141 |
+
});
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
getLastLineBytes(): number {
|
| 145 |
+
return this.currentLineBytes;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
private appendDecodedText(text: string): void {
|
| 149 |
+
if (text.length === 0) {
|
| 150 |
+
return;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
const bytes = byteLength(text);
|
| 154 |
+
this.totalDecodedBytes += bytes;
|
| 155 |
+
this.tailText += text;
|
| 156 |
+
this.tailBytes += bytes;
|
| 157 |
+
if (this.tailBytes > this.maxRollingBytes * 2) {
|
| 158 |
+
this.trimTail();
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
let newlines = 0;
|
| 162 |
+
let lastNewline = -1;
|
| 163 |
+
for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
|
| 164 |
+
newlines++;
|
| 165 |
+
lastNewline = i;
|
| 166 |
+
}
|
| 167 |
+
if (newlines === 0) {
|
| 168 |
+
this.currentLineBytes += bytes;
|
| 169 |
+
this.hasOpenLine = true;
|
| 170 |
+
} else {
|
| 171 |
+
this.completedLines += newlines;
|
| 172 |
+
const tail = text.slice(lastNewline + 1);
|
| 173 |
+
this.currentLineBytes = byteLength(tail);
|
| 174 |
+
this.hasOpenLine = tail.length > 0;
|
| 175 |
+
}
|
| 176 |
+
this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
private trimTail(): void {
|
| 180 |
+
const buffer = Buffer.from(this.tailText, "utf-8");
|
| 181 |
+
if (buffer.length <= this.maxRollingBytes) {
|
| 182 |
+
this.tailBytes = buffer.length;
|
| 183 |
+
return;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
let start = buffer.length - this.maxRollingBytes;
|
| 187 |
+
while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) {
|
| 188 |
+
start++;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
|
| 192 |
+
this.tailText = buffer.subarray(start).toString("utf-8");
|
| 193 |
+
this.tailBytes = byteLength(this.tailText);
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
private getSnapshotText(): string {
|
| 197 |
+
if (this.tailStartsAtLineBoundary) {
|
| 198 |
+
return this.tailText;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
const firstNewline = this.tailText.indexOf("\n");
|
| 202 |
+
return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
private shouldUseTempFile(): boolean {
|
| 206 |
+
return (
|
| 207 |
+
this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines
|
| 208 |
+
);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
private ensureTempFile(): void {
|
| 212 |
+
if (this.tempFilePath) {
|
| 213 |
+
return;
|
| 214 |
+
}
|
| 215 |
+
this.tempFilePath = defaultTempFilePath(this.tempFilePrefix);
|
| 216 |
+
this.tempFileStream = createWriteStream(this.tempFilePath);
|
| 217 |
+
for (const chunk of this.rawChunks) {
|
| 218 |
+
this.tempFileStream.write(chunk);
|
| 219 |
+
}
|
| 220 |
+
this.rawChunks = [];
|
| 221 |
+
}
|
| 222 |
+
}
|
packages/coding-agent/src/core/tools/path-utils.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { accessSync, constants } from "node:fs";
|
| 2 |
+
import { access } from "node:fs/promises";
|
| 3 |
+
import { normalizePath, resolvePath } from "../../utils/paths.ts";
|
| 4 |
+
|
| 5 |
+
const NARROW_NO_BREAK_SPACE = "\u202F";
|
| 6 |
+
|
| 7 |
+
function tryMacOSScreenshotPath(filePath: string): string {
|
| 8 |
+
return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
function tryNFDVariant(filePath: string): string {
|
| 12 |
+
// macOS stores filenames in NFD (decomposed) form, try converting user input to NFD
|
| 13 |
+
return filePath.normalize("NFD");
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
function tryCurlyQuoteVariant(filePath: string): string {
|
| 17 |
+
// macOS uses U+2019 (right single quotation mark) in screenshot names like "Capture d'écran"
|
| 18 |
+
// Users typically type U+0027 (straight apostrophe)
|
| 19 |
+
return filePath.replace(/'/g, "\u2019");
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
function fileExists(filePath: string): boolean {
|
| 23 |
+
try {
|
| 24 |
+
accessSync(filePath, constants.F_OK);
|
| 25 |
+
return true;
|
| 26 |
+
} catch {
|
| 27 |
+
return false;
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export async function pathExists(filePath: string): Promise<boolean> {
|
| 32 |
+
try {
|
| 33 |
+
await access(filePath, constants.F_OK);
|
| 34 |
+
return true;
|
| 35 |
+
} catch {
|
| 36 |
+
return false;
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export function expandPath(filePath: string): string {
|
| 41 |
+
return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Resolve a path relative to the given cwd.
|
| 46 |
+
* Handles ~ expansion and absolute paths.
|
| 47 |
+
*/
|
| 48 |
+
export function resolveToCwd(filePath: string, cwd: string): string {
|
| 49 |
+
return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export function resolveReadPath(filePath: string, cwd: string): string {
|
| 53 |
+
const resolved = resolveToCwd(filePath, cwd);
|
| 54 |
+
|
| 55 |
+
if (fileExists(resolved)) {
|
| 56 |
+
return resolved;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Try macOS AM/PM variant (narrow no-break space before AM/PM)
|
| 60 |
+
const amPmVariant = tryMacOSScreenshotPath(resolved);
|
| 61 |
+
if (amPmVariant !== resolved && fileExists(amPmVariant)) {
|
| 62 |
+
return amPmVariant;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// Try NFD variant (macOS stores filenames in NFD form)
|
| 66 |
+
const nfdVariant = tryNFDVariant(resolved);
|
| 67 |
+
if (nfdVariant !== resolved && fileExists(nfdVariant)) {
|
| 68 |
+
return nfdVariant;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
// Try curly quote variant (macOS uses U+2019 in screenshot names)
|
| 72 |
+
const curlyVariant = tryCurlyQuoteVariant(resolved);
|
| 73 |
+
if (curlyVariant !== resolved && fileExists(curlyVariant)) {
|
| 74 |
+
return curlyVariant;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
// Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
|
| 78 |
+
const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
|
| 79 |
+
if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) {
|
| 80 |
+
return nfdCurlyVariant;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return resolved;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {
|
| 87 |
+
const resolved = resolveToCwd(filePath, cwd);
|
| 88 |
+
|
| 89 |
+
if (await pathExists(resolved)) {
|
| 90 |
+
return resolved;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// Try macOS AM/PM variant (narrow no-break space before AM/PM)
|
| 94 |
+
const amPmVariant = tryMacOSScreenshotPath(resolved);
|
| 95 |
+
if (amPmVariant !== resolved && (await pathExists(amPmVariant))) {
|
| 96 |
+
return amPmVariant;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Try NFD variant (macOS stores filenames in NFD form)
|
| 100 |
+
const nfdVariant = tryNFDVariant(resolved);
|
| 101 |
+
if (nfdVariant !== resolved && (await pathExists(nfdVariant))) {
|
| 102 |
+
return nfdVariant;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// Try curly quote variant (macOS uses U+2019 in screenshot names)
|
| 106 |
+
const curlyVariant = tryCurlyQuoteVariant(resolved);
|
| 107 |
+
if (curlyVariant !== resolved && (await pathExists(curlyVariant))) {
|
| 108 |
+
return curlyVariant;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
// Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
|
| 112 |
+
const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
|
| 113 |
+
if (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {
|
| 114 |
+
return nfdCurlyVariant;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return resolved;
|
| 118 |
+
}
|
packages/coding-agent/src/core/tools/powershell.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getPowerShellConfig } from "../../utils/shell.ts";
|
| 2 |
+
import {
|
| 3 |
+
type BashOperations,
|
| 4 |
+
type BashSpawnContext,
|
| 5 |
+
type BashSpawnHook,
|
| 6 |
+
type BashToolDetails,
|
| 7 |
+
type BashToolInput,
|
| 8 |
+
type BashToolOptions,
|
| 9 |
+
type createBashTool,
|
| 10 |
+
createLocalShellOperations,
|
| 11 |
+
createShellToolDefinition,
|
| 12 |
+
type ShellToolConfig,
|
| 13 |
+
} from "./bash.ts";
|
| 14 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 15 |
+
|
| 16 |
+
const UTF8_OUTPUT_PREFIX = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
|
| 17 |
+
|
| 18 |
+
export const powershellToolSystemPromptContribution = {
|
| 19 |
+
snippet: "Execute PowerShell commands",
|
| 20 |
+
guidelines: ["You can inspect PI_* environment variables for current model and session details."],
|
| 21 |
+
} as const;
|
| 22 |
+
|
| 23 |
+
export type PowerShellOperations = BashOperations;
|
| 24 |
+
export type PowerShellSpawnContext = BashSpawnContext;
|
| 25 |
+
export type PowerShellSpawnHook = BashSpawnHook;
|
| 26 |
+
export type PowerShellToolDetails = BashToolDetails;
|
| 27 |
+
export type PowerShellToolInput = BashToolInput;
|
| 28 |
+
|
| 29 |
+
export interface PowerShellToolOptions
|
| 30 |
+
extends Pick<BashToolOptions, "operations" | "exposeSessionEnvironment" | "spawnHook"> {}
|
| 31 |
+
|
| 32 |
+
export function createLocalPowerShellOperations(): PowerShellOperations {
|
| 33 |
+
const operations = createLocalShellOperations("PowerShell", getPowerShellConfig);
|
| 34 |
+
return {
|
| 35 |
+
exec: (command, cwd, options) => operations.exec(`${UTF8_OUTPUT_PREFIX}${command}`, cwd, options),
|
| 36 |
+
};
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const powershellToolConfig: ShellToolConfig = {
|
| 40 |
+
name: "powershell",
|
| 41 |
+
label: "powershell",
|
| 42 |
+
shellName: "PowerShell",
|
| 43 |
+
prompt: "PS>",
|
| 44 |
+
promptSnippet: powershellToolSystemPromptContribution.snippet,
|
| 45 |
+
promptGuidelines: powershellToolSystemPromptContribution.guidelines,
|
| 46 |
+
tempFilePrefix: "pi-powershell",
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
export function createPowerShellToolDefinition(
|
| 50 |
+
cwd: string,
|
| 51 |
+
options?: PowerShellToolOptions,
|
| 52 |
+
): ReturnType<typeof createShellToolDefinition> {
|
| 53 |
+
return createShellToolDefinition(cwd, powershellToolConfig, {
|
| 54 |
+
...options,
|
| 55 |
+
operations: options?.operations ?? createLocalPowerShellOperations(),
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export function createPowerShellTool(cwd: string, options?: PowerShellToolOptions): ReturnType<typeof createBashTool> {
|
| 60 |
+
const definition = createPowerShellToolDefinition(cwd, options);
|
| 61 |
+
const tool = wrapToolDefinition(definition);
|
| 62 |
+
Object.assign(tool, {
|
| 63 |
+
promptSnippet: definition.promptSnippet,
|
| 64 |
+
promptGuidelines: definition.promptGuidelines,
|
| 65 |
+
});
|
| 66 |
+
return tool;
|
| 67 |
+
}
|
packages/coding-agent/src/core/tools/read.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 2 |
+
import type { Api, ImageContent, Model, TextContent } from "@earendil-works/pi-ai";
|
| 3 |
+
import { constants } from "fs";
|
| 4 |
+
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
| 5 |
+
import { type Static, Type } from "typebox";
|
| 6 |
+
import { processImage } from "../../utils/image-process.ts";
|
| 7 |
+
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.ts";
|
| 8 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 9 |
+
import { resolveReadPathAsync } from "./path-utils.ts";
|
| 10 |
+
import { readRenderers } from "./renderers/read.ts";
|
| 11 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 12 |
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "./truncate.ts";
|
| 13 |
+
|
| 14 |
+
const readSchema = Type.Object({
|
| 15 |
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
| 16 |
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
| 17 |
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
export const readToolSystemPromptContribution = {
|
| 21 |
+
snippet: "Read file contents",
|
| 22 |
+
guidelines: ["Use read to examine files instead of cat or sed."],
|
| 23 |
+
} as const;
|
| 24 |
+
|
| 25 |
+
export type ReadToolInput = Static<typeof readSchema>;
|
| 26 |
+
|
| 27 |
+
export interface ReadToolDetails {
|
| 28 |
+
truncation?: TruncationResult;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* Pluggable operations for the read tool.
|
| 33 |
+
* Override these to delegate file reading to remote systems (for example SSH).
|
| 34 |
+
*/
|
| 35 |
+
export interface ReadOperations {
|
| 36 |
+
/** Read file contents as a Buffer */
|
| 37 |
+
readFile: (absolutePath: string) => Promise<Buffer>;
|
| 38 |
+
/** Check if file is readable (throw if not) */
|
| 39 |
+
access: (absolutePath: string) => Promise<void>;
|
| 40 |
+
/** Detect image MIME type, return null or undefined for non-images */
|
| 41 |
+
detectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
const defaultReadOperations: ReadOperations = {
|
| 45 |
+
readFile: (path) => fsReadFile(path),
|
| 46 |
+
access: (path) => fsAccess(path, constants.R_OK),
|
| 47 |
+
detectImageMimeType: detectSupportedImageMimeTypeFromFile,
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
export interface ReadToolOptions {
|
| 51 |
+
/** Whether to auto-resize images to 2000x2000 max. Default: true */
|
| 52 |
+
autoResizeImages?: boolean;
|
| 53 |
+
/** Custom operations for file reading. Default: local filesystem */
|
| 54 |
+
operations?: ReadOperations;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
function getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {
|
| 58 |
+
if (!model || model.input.includes("image")) {
|
| 59 |
+
return undefined;
|
| 60 |
+
}
|
| 61 |
+
return "[Current model does not support images. The image will be omitted from this request.]";
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
export function createReadToolDefinition(
|
| 65 |
+
cwd: string,
|
| 66 |
+
options?: ReadToolOptions,
|
| 67 |
+
): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {
|
| 68 |
+
const autoResizeImages = options?.autoResizeImages ?? true;
|
| 69 |
+
const ops = options?.operations ?? defaultReadOperations;
|
| 70 |
+
return {
|
| 71 |
+
name: "read",
|
| 72 |
+
label: "read",
|
| 73 |
+
description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
|
| 74 |
+
promptSnippet: readToolSystemPromptContribution.snippet,
|
| 75 |
+
promptGuidelines: [...readToolSystemPromptContribution.guidelines],
|
| 76 |
+
parameters: readSchema,
|
| 77 |
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
| 78 |
+
async execute(
|
| 79 |
+
_toolCallId,
|
| 80 |
+
{ path, offset, limit }: { path: string; offset?: number; limit?: number },
|
| 81 |
+
signal?: AbortSignal,
|
| 82 |
+
_onUpdate?,
|
| 83 |
+
ctx?: ExtensionContext,
|
| 84 |
+
) {
|
| 85 |
+
return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
|
| 86 |
+
(resolve, reject) => {
|
| 87 |
+
if (signal?.aborted) {
|
| 88 |
+
reject(new Error("Operation aborted"));
|
| 89 |
+
return;
|
| 90 |
+
}
|
| 91 |
+
let aborted = false;
|
| 92 |
+
const onAbort = () => {
|
| 93 |
+
aborted = true;
|
| 94 |
+
reject(new Error("Operation aborted"));
|
| 95 |
+
};
|
| 96 |
+
signal?.addEventListener("abort", onAbort, { once: true });
|
| 97 |
+
|
| 98 |
+
(async () => {
|
| 99 |
+
try {
|
| 100 |
+
const absolutePath = await resolveReadPathAsync(path, ctx?.cwd || cwd);
|
| 101 |
+
if (aborted) return;
|
| 102 |
+
// Check if file exists and is readable.
|
| 103 |
+
await ops.access(absolutePath);
|
| 104 |
+
if (aborted) return;
|
| 105 |
+
const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;
|
| 106 |
+
let content: (TextContent | ImageContent)[];
|
| 107 |
+
let details: ReadToolDetails | undefined;
|
| 108 |
+
const nonVisionImageNote = getNonVisionImageNote(ctx?.model);
|
| 109 |
+
if (mimeType) {
|
| 110 |
+
// Read image as binary.
|
| 111 |
+
const buffer = await ops.readFile(absolutePath);
|
| 112 |
+
const processed = await processImage(buffer, mimeType, { autoResizeImages });
|
| 113 |
+
if (!processed.ok) {
|
| 114 |
+
let textNote = `Read image file [${mimeType}]\n${processed.message}`;
|
| 115 |
+
if (nonVisionImageNote) textNote += `\n${nonVisionImageNote}`;
|
| 116 |
+
content = [{ type: "text", text: textNote }];
|
| 117 |
+
} else {
|
| 118 |
+
let textNote = `Read image file [${processed.mimeType}]`;
|
| 119 |
+
if (processed.hints.length > 0) textNote += `\n${processed.hints.join("\n")}`;
|
| 120 |
+
if (nonVisionImageNote) textNote += `\n${nonVisionImageNote}`;
|
| 121 |
+
content = [
|
| 122 |
+
{ type: "text", text: textNote },
|
| 123 |
+
{ type: "image", data: processed.data, mimeType: processed.mimeType },
|
| 124 |
+
];
|
| 125 |
+
}
|
| 126 |
+
} else {
|
| 127 |
+
// Read text content.
|
| 128 |
+
const buffer = await ops.readFile(absolutePath);
|
| 129 |
+
const textContent = buffer.toString("utf-8");
|
| 130 |
+
const allLines = textContent.split("\n");
|
| 131 |
+
const totalFileLines = allLines.length;
|
| 132 |
+
// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.
|
| 133 |
+
const startLine = offset ? Math.max(0, offset - 1) : 0;
|
| 134 |
+
const startLineDisplay = startLine + 1;
|
| 135 |
+
// Check if offset is out of bounds.
|
| 136 |
+
if (startLine >= allLines.length) {
|
| 137 |
+
throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
|
| 138 |
+
}
|
| 139 |
+
let selectedContent: string;
|
| 140 |
+
let userLimitedLines: number | undefined;
|
| 141 |
+
// If limit is specified by the user, honor it first. Otherwise truncateHead decides.
|
| 142 |
+
if (limit !== undefined) {
|
| 143 |
+
const endLine = Math.min(startLine + limit, allLines.length);
|
| 144 |
+
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
| 145 |
+
userLimitedLines = endLine - startLine;
|
| 146 |
+
} else {
|
| 147 |
+
selectedContent = allLines.slice(startLine).join("\n");
|
| 148 |
+
}
|
| 149 |
+
// Apply truncation, respecting both line and byte limits.
|
| 150 |
+
const truncation = truncateHead(selectedContent);
|
| 151 |
+
let outputText: string;
|
| 152 |
+
if (truncation.firstLineExceedsLimit) {
|
| 153 |
+
// First line alone exceeds the byte limit. Point the model at a bash fallback.
|
| 154 |
+
const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8"));
|
| 155 |
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
| 156 |
+
details = { truncation };
|
| 157 |
+
} else if (truncation.truncated) {
|
| 158 |
+
// Truncation occurred. Build an actionable continuation notice.
|
| 159 |
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
| 160 |
+
const nextOffset = endLineDisplay + 1;
|
| 161 |
+
outputText = truncation.content;
|
| 162 |
+
if (truncation.truncatedBy === "lines") {
|
| 163 |
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;
|
| 164 |
+
} else {
|
| 165 |
+
outputText += `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
| 166 |
+
}
|
| 167 |
+
details = { truncation };
|
| 168 |
+
} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
|
| 169 |
+
// User-specified limit stopped early, but the file still has more content.
|
| 170 |
+
const remaining = allLines.length - (startLine + userLimitedLines);
|
| 171 |
+
const nextOffset = startLine + userLimitedLines + 1;
|
| 172 |
+
outputText = `${truncation.content}\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;
|
| 173 |
+
} else {
|
| 174 |
+
// No truncation and no remaining user-limited content.
|
| 175 |
+
outputText = truncation.content;
|
| 176 |
+
}
|
| 177 |
+
content = [{ type: "text", text: outputText }];
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
if (aborted) return;
|
| 181 |
+
signal?.removeEventListener("abort", onAbort);
|
| 182 |
+
resolve({ content, details });
|
| 183 |
+
} catch (error: any) {
|
| 184 |
+
signal?.removeEventListener("abort", onAbort);
|
| 185 |
+
if (!aborted) reject(error);
|
| 186 |
+
}
|
| 187 |
+
})();
|
| 188 |
+
},
|
| 189 |
+
);
|
| 190 |
+
},
|
| 191 |
+
...readRenderers,
|
| 192 |
+
};
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {
|
| 196 |
+
return wrapToolDefinition(createReadToolDefinition(cwd, options));
|
| 197 |
+
}
|
packages/coding-agent/src/core/tools/render-utils.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import * as os from "node:os";
|
| 2 |
+
import { pathToFileURL } from "node:url";
|
| 3 |
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
| 4 |
+
import { getCapabilities, getImageDimensions, hyperlink, imageFallback } from "@earendil-works/pi-tui";
|
| 5 |
+
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
| 6 |
+
import { stripAnsi } from "../../utils/ansi.ts";
|
| 7 |
+
import { resolvePath } from "../../utils/paths.ts";
|
| 8 |
+
import { sanitizeBinaryOutput } from "../../utils/shell.ts";
|
| 9 |
+
|
| 10 |
+
export function shortenPath(path: unknown): string {
|
| 11 |
+
if (typeof path !== "string") return "";
|
| 12 |
+
const home = os.homedir();
|
| 13 |
+
if (path.startsWith(home)) {
|
| 14 |
+
return `~${path.slice(home.length)}`;
|
| 15 |
+
}
|
| 16 |
+
return path;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export function linkPath(styledText: string, rawPath: string, cwd: string): string {
|
| 20 |
+
if (!getCapabilities().hyperlinks) return styledText;
|
| 21 |
+
const absolutePath = resolvePath(rawPath, cwd);
|
| 22 |
+
return hyperlink(styledText, pathToFileURL(absolutePath).href);
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export function str(value: unknown): string | null {
|
| 26 |
+
if (typeof value === "string") return value;
|
| 27 |
+
if (value == null) return "";
|
| 28 |
+
return null;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export function replaceTabs(text: string): string {
|
| 32 |
+
return text.replace(/\t/g, " ");
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
export function normalizeDisplayText(text: string): string {
|
| 36 |
+
return text.replace(/\r/g, "");
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export function getTextOutput(
|
| 40 |
+
result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> } | undefined,
|
| 41 |
+
showImages: boolean,
|
| 42 |
+
): string {
|
| 43 |
+
if (!result) return "";
|
| 44 |
+
|
| 45 |
+
const textBlocks = result.content.filter((c) => c.type === "text");
|
| 46 |
+
const imageBlocks = result.content.filter((c) => c.type === "image");
|
| 47 |
+
|
| 48 |
+
let output = textBlocks.map((c) => sanitizeBinaryOutput(stripAnsi(c.text || "")).replace(/\r/g, "")).join("\n");
|
| 49 |
+
|
| 50 |
+
const caps = getCapabilities();
|
| 51 |
+
if (imageBlocks.length > 0 && (!caps.images || !showImages)) {
|
| 52 |
+
const imageIndicators = imageBlocks
|
| 53 |
+
.map((img) => {
|
| 54 |
+
const mimeType = img.mimeType ?? "image/unknown";
|
| 55 |
+
const dims =
|
| 56 |
+
img.data && img.mimeType ? (getImageDimensions(img.data, img.mimeType) ?? undefined) : undefined;
|
| 57 |
+
return imageFallback(mimeType, dims);
|
| 58 |
+
})
|
| 59 |
+
.join("\n");
|
| 60 |
+
output = output ? `${output}\n${imageIndicators}` : imageIndicators;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
return output;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export type ToolRenderResultLike<TDetails> = {
|
| 67 |
+
content: (TextContent | ImageContent)[];
|
| 68 |
+
details: TDetails;
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
export function invalidArgText(theme: Theme): string {
|
| 72 |
+
return theme.fg("error", "[invalid arg]");
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
export function renderToolPath(
|
| 76 |
+
rawPath: string | null,
|
| 77 |
+
theme: Theme,
|
| 78 |
+
cwd: string,
|
| 79 |
+
options?: { emptyFallback?: string },
|
| 80 |
+
): string {
|
| 81 |
+
if (rawPath === null) return invalidArgText(theme);
|
| 82 |
+
const value = rawPath || options?.emptyFallback;
|
| 83 |
+
if (!value) return theme.fg("toolOutput", "...");
|
| 84 |
+
return linkPath(theme.fg("accent", shortenPath(value)), value, cwd);
|
| 85 |
+
}
|
packages/coding-agent/src/core/tools/renderers/bash.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the shell tools.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `bash.ts` spreads these into the shell
|
| 6 |
+
* tool definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Container, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
| 10 |
+
import { keyHint } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 11 |
+
import { truncateToVisualLines } from "../../../modes/interactive/components/visual-truncate.ts";
|
| 12 |
+
import { theme } from "../../../modes/interactive/theme/theme.ts";
|
| 13 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 14 |
+
import type { BashToolDetails } from "../bash.ts";
|
| 15 |
+
import { getTextOutput, invalidArgText, str } from "../render-utils.ts";
|
| 16 |
+
import { DEFAULT_MAX_BYTES, formatSize } from "../truncate.ts";
|
| 17 |
+
|
| 18 |
+
const BASH_PREVIEW_LINES = 5;
|
| 19 |
+
export const BASH_UPDATE_THROTTLE_MS = 100;
|
| 20 |
+
type BashResultRenderState = {
|
| 21 |
+
cachedWidth: number | undefined;
|
| 22 |
+
cachedLines: string[] | undefined;
|
| 23 |
+
cachedSkipped: number | undefined;
|
| 24 |
+
};
|
| 25 |
+
class BashResultRenderComponent extends Container {
|
| 26 |
+
state: BashResultRenderState = {
|
| 27 |
+
cachedWidth: undefined,
|
| 28 |
+
cachedLines: undefined,
|
| 29 |
+
cachedSkipped: undefined,
|
| 30 |
+
};
|
| 31 |
+
}
|
| 32 |
+
function formatDuration(ms: number): string {
|
| 33 |
+
return `${(ms / 1000).toFixed(1)}s`;
|
| 34 |
+
}
|
| 35 |
+
function formatShellCall(args: { command?: string; timeout?: number } | undefined, prompt: string): string {
|
| 36 |
+
const command = str(args?.command);
|
| 37 |
+
const timeout = args?.timeout as number | undefined;
|
| 38 |
+
const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
|
| 39 |
+
const commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "...");
|
| 40 |
+
return theme.fg("toolTitle", theme.bold(`${prompt} ${commandDisplay}`)) + timeoutSuffix;
|
| 41 |
+
}
|
| 42 |
+
function rebuildBashResultRenderComponent(
|
| 43 |
+
component: BashResultRenderComponent,
|
| 44 |
+
result: {
|
| 45 |
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
| 46 |
+
details?: BashToolDetails;
|
| 47 |
+
},
|
| 48 |
+
options: ToolRenderResultOptions,
|
| 49 |
+
showImages: boolean,
|
| 50 |
+
startedAt: number | undefined,
|
| 51 |
+
endedAt: number | undefined,
|
| 52 |
+
): void {
|
| 53 |
+
const state = component.state;
|
| 54 |
+
component.clear();
|
| 55 |
+
|
| 56 |
+
let output = getTextOutput(result as any, showImages).trim();
|
| 57 |
+
const truncation = result.details?.truncation;
|
| 58 |
+
const fullOutputPath = result.details?.fullOutputPath;
|
| 59 |
+
if (!options.isPartial && truncation?.truncated && fullOutputPath && output.endsWith("]")) {
|
| 60 |
+
const footerStart = output.lastIndexOf("\n\n[");
|
| 61 |
+
if (footerStart !== -1 && output.slice(footerStart).includes(fullOutputPath)) {
|
| 62 |
+
output = output.slice(0, footerStart).trimEnd();
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
if (output) {
|
| 67 |
+
const styledOutput = output
|
| 68 |
+
.split("\n")
|
| 69 |
+
.map((line) => theme.fg("toolOutput", line))
|
| 70 |
+
.join("\n");
|
| 71 |
+
|
| 72 |
+
if (options.expanded) {
|
| 73 |
+
component.addChild(new Text(`\n${styledOutput}`, 0, 0));
|
| 74 |
+
} else {
|
| 75 |
+
component.addChild({
|
| 76 |
+
render: (width: number) => {
|
| 77 |
+
if (state.cachedLines === undefined || state.cachedWidth !== width) {
|
| 78 |
+
const preview = truncateToVisualLines(styledOutput, BASH_PREVIEW_LINES, width);
|
| 79 |
+
state.cachedLines = preview.visualLines;
|
| 80 |
+
state.cachedSkipped = preview.skippedCount;
|
| 81 |
+
state.cachedWidth = width;
|
| 82 |
+
}
|
| 83 |
+
if (state.cachedSkipped && state.cachedSkipped > 0) {
|
| 84 |
+
const hint =
|
| 85 |
+
theme.fg("muted", `... (${state.cachedSkipped} earlier lines,`) +
|
| 86 |
+
` ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 87 |
+
return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])];
|
| 88 |
+
}
|
| 89 |
+
return ["", ...(state.cachedLines ?? [])];
|
| 90 |
+
},
|
| 91 |
+
invalidate: () => {
|
| 92 |
+
state.cachedWidth = undefined;
|
| 93 |
+
state.cachedLines = undefined;
|
| 94 |
+
state.cachedSkipped = undefined;
|
| 95 |
+
},
|
| 96 |
+
});
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if (truncation?.truncated || fullOutputPath) {
|
| 101 |
+
const warnings: string[] = [];
|
| 102 |
+
if (fullOutputPath) {
|
| 103 |
+
warnings.push(`Full output: ${fullOutputPath}`);
|
| 104 |
+
}
|
| 105 |
+
if (truncation?.truncated) {
|
| 106 |
+
if (truncation.truncatedBy === "lines") {
|
| 107 |
+
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
| 108 |
+
} else {
|
| 109 |
+
warnings.push(
|
| 110 |
+
`Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`,
|
| 111 |
+
);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
component.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0));
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
if (startedAt !== undefined) {
|
| 118 |
+
const label = options.isPartial ? "Elapsed" : "Took";
|
| 119 |
+
const endTime = endedAt ?? Date.now();
|
| 120 |
+
component.addChild(new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/** Shell renderers are shared by bash and powershell, which differ only in the prompt they display. */
|
| 125 |
+
export function createShellRenderers(prompt: string): Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> {
|
| 126 |
+
return {
|
| 127 |
+
renderCall(args, _theme, context) {
|
| 128 |
+
const state = context.state;
|
| 129 |
+
if (context.executionStarted && state.startedAt === undefined) {
|
| 130 |
+
state.startedAt = Date.now();
|
| 131 |
+
state.endedAt = undefined;
|
| 132 |
+
}
|
| 133 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 134 |
+
text.setText(formatShellCall(args as { command?: string; timeout?: number } | undefined, prompt));
|
| 135 |
+
return text;
|
| 136 |
+
},
|
| 137 |
+
renderResult(result, options, _theme, context) {
|
| 138 |
+
const state = context.state;
|
| 139 |
+
if (state.startedAt !== undefined && options.isPartial && !state.interval) {
|
| 140 |
+
state.interval = setInterval(() => context.invalidate(), 1000);
|
| 141 |
+
}
|
| 142 |
+
if (!options.isPartial || context.isError) {
|
| 143 |
+
state.endedAt ??= Date.now();
|
| 144 |
+
if (state.interval) {
|
| 145 |
+
clearInterval(state.interval);
|
| 146 |
+
state.interval = undefined;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
const component =
|
| 150 |
+
(context.lastComponent as BashResultRenderComponent | undefined) ?? new BashResultRenderComponent();
|
| 151 |
+
rebuildBashResultRenderComponent(
|
| 152 |
+
component,
|
| 153 |
+
result as any,
|
| 154 |
+
options,
|
| 155 |
+
context.showImages,
|
| 156 |
+
state.startedAt,
|
| 157 |
+
state.endedAt,
|
| 158 |
+
);
|
| 159 |
+
component.invalidate();
|
| 160 |
+
return component;
|
| 161 |
+
},
|
| 162 |
+
};
|
| 163 |
+
}
|
packages/coding-agent/src/core/tools/renderers/edit.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the edit tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `edit.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
| 10 |
+
import { renderDiff } from "../../../modes/interactive/components/diff.ts";
|
| 11 |
+
import type { Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 12 |
+
import type { ToolDefinition } from "../../extensions/types.ts";
|
| 13 |
+
import type { EditToolDetails } from "../edit.ts";
|
| 14 |
+
import { computeEditsDiff, type Edit, type EditDiffError, type EditDiffResult } from "../edit-diff.ts";
|
| 15 |
+
import { renderToolPath, str } from "../render-utils.ts";
|
| 16 |
+
|
| 17 |
+
type EditPreview = EditDiffResult | EditDiffError;
|
| 18 |
+
export type EditRenderState = {
|
| 19 |
+
callComponent?: EditCallRenderComponent;
|
| 20 |
+
};
|
| 21 |
+
type RenderableEditArgs = {
|
| 22 |
+
path?: string;
|
| 23 |
+
file_path?: string;
|
| 24 |
+
edits?: Edit[];
|
| 25 |
+
oldText?: string;
|
| 26 |
+
newText?: string;
|
| 27 |
+
};
|
| 28 |
+
type EditToolResultLike = {
|
| 29 |
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
| 30 |
+
details?: EditToolDetails;
|
| 31 |
+
};
|
| 32 |
+
type EditCallRenderComponent = Box & {
|
| 33 |
+
preview?: EditPreview;
|
| 34 |
+
previewArgsKey?: string;
|
| 35 |
+
previewPending?: boolean;
|
| 36 |
+
settledError?: boolean;
|
| 37 |
+
};
|
| 38 |
+
function createEditCallRenderComponent(): EditCallRenderComponent {
|
| 39 |
+
return Object.assign(new Box(1, 1, (text: string) => text), {
|
| 40 |
+
preview: undefined as EditPreview | undefined,
|
| 41 |
+
previewArgsKey: undefined as string | undefined,
|
| 42 |
+
previewPending: false,
|
| 43 |
+
settledError: false,
|
| 44 |
+
});
|
| 45 |
+
}
|
| 46 |
+
function getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {
|
| 47 |
+
if (lastComponent instanceof Box) {
|
| 48 |
+
const component = lastComponent as EditCallRenderComponent;
|
| 49 |
+
state.callComponent = component;
|
| 50 |
+
return component;
|
| 51 |
+
}
|
| 52 |
+
if (state.callComponent) {
|
| 53 |
+
return state.callComponent;
|
| 54 |
+
}
|
| 55 |
+
const component = createEditCallRenderComponent();
|
| 56 |
+
state.callComponent = component;
|
| 57 |
+
return component;
|
| 58 |
+
}
|
| 59 |
+
function getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path: string; edits: Edit[] } | null {
|
| 60 |
+
if (!args) {
|
| 61 |
+
return null;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const path = typeof args.path === "string" ? args.path : typeof args.file_path === "string" ? args.file_path : null;
|
| 65 |
+
if (!path) {
|
| 66 |
+
return null;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
if (
|
| 70 |
+
Array.isArray(args.edits) &&
|
| 71 |
+
args.edits.length > 0 &&
|
| 72 |
+
args.edits.every((edit) => typeof edit?.oldText === "string" && typeof edit?.newText === "string")
|
| 73 |
+
) {
|
| 74 |
+
return { path, edits: args.edits };
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
if (typeof args.oldText === "string" && typeof args.newText === "string") {
|
| 78 |
+
return { path, edits: [{ oldText: args.oldText, newText: args.newText }] };
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
return null;
|
| 82 |
+
}
|
| 83 |
+
function formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {
|
| 84 |
+
const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
|
| 85 |
+
return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`;
|
| 86 |
+
}
|
| 87 |
+
function formatEditResult(
|
| 88 |
+
args: RenderableEditArgs | undefined,
|
| 89 |
+
preview: EditPreview | undefined,
|
| 90 |
+
result: EditToolResultLike,
|
| 91 |
+
theme: Theme,
|
| 92 |
+
isError: boolean,
|
| 93 |
+
): string | undefined {
|
| 94 |
+
const rawPath = str(args?.file_path ?? args?.path);
|
| 95 |
+
const previewDiff = preview && !("error" in preview) ? preview.diff : undefined;
|
| 96 |
+
const previewError = preview && "error" in preview ? preview.error : undefined;
|
| 97 |
+
if (isError) {
|
| 98 |
+
const errorText = result.content
|
| 99 |
+
.filter((c) => c.type === "text")
|
| 100 |
+
.map((c) => c.text || "")
|
| 101 |
+
.join("\n");
|
| 102 |
+
if (!errorText || errorText === previewError) {
|
| 103 |
+
return undefined;
|
| 104 |
+
}
|
| 105 |
+
return theme.fg("error", errorText);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const resultDiff = result.details?.diff;
|
| 109 |
+
if (resultDiff && resultDiff !== previewDiff) {
|
| 110 |
+
return renderDiff(resultDiff, { filePath: rawPath ?? undefined });
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return undefined;
|
| 114 |
+
}
|
| 115 |
+
function getEditHeaderBg(
|
| 116 |
+
preview: EditPreview | undefined,
|
| 117 |
+
settledError: boolean | undefined,
|
| 118 |
+
theme: Theme,
|
| 119 |
+
): (text: string) => string {
|
| 120 |
+
if (preview) {
|
| 121 |
+
if ("error" in preview) {
|
| 122 |
+
return (text: string) => theme.bg("toolErrorBg", text);
|
| 123 |
+
}
|
| 124 |
+
return (text: string) => theme.bg("toolSuccessBg", text);
|
| 125 |
+
}
|
| 126 |
+
if (settledError) {
|
| 127 |
+
return (text: string) => theme.bg("toolErrorBg", text);
|
| 128 |
+
}
|
| 129 |
+
return (text: string) => theme.bg("toolPendingBg", text);
|
| 130 |
+
}
|
| 131 |
+
function buildEditCallComponent(
|
| 132 |
+
component: EditCallRenderComponent,
|
| 133 |
+
args: RenderableEditArgs | undefined,
|
| 134 |
+
theme: Theme,
|
| 135 |
+
cwd: string,
|
| 136 |
+
): EditCallRenderComponent {
|
| 137 |
+
component.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));
|
| 138 |
+
component.clear();
|
| 139 |
+
component.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));
|
| 140 |
+
|
| 141 |
+
if (!component.preview) {
|
| 142 |
+
return component;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
const body =
|
| 146 |
+
"error" in component.preview ? theme.fg("error", component.preview.error) : renderDiff(component.preview.diff);
|
| 147 |
+
component.addChild(new Spacer(1));
|
| 148 |
+
component.addChild(new Text(body, 0, 0));
|
| 149 |
+
return component;
|
| 150 |
+
}
|
| 151 |
+
function setEditPreview(
|
| 152 |
+
component: EditCallRenderComponent,
|
| 153 |
+
preview: EditPreview,
|
| 154 |
+
argsKey: string | undefined,
|
| 155 |
+
): boolean {
|
| 156 |
+
const current = component.preview;
|
| 157 |
+
const changed =
|
| 158 |
+
current === undefined ||
|
| 159 |
+
("error" in current && "error" in preview
|
| 160 |
+
? current.error !== preview.error
|
| 161 |
+
: "error" in current !== "error" in preview) ||
|
| 162 |
+
(!("error" in current) &&
|
| 163 |
+
!("error" in preview) &&
|
| 164 |
+
(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));
|
| 165 |
+
component.preview = preview;
|
| 166 |
+
component.previewArgsKey = argsKey;
|
| 167 |
+
component.previewPending = false;
|
| 168 |
+
return changed;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
export const editRenderers: Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> = {
|
| 172 |
+
renderCall(args, theme, context) {
|
| 173 |
+
const component = getEditCallRenderComponent(context.state, context.lastComponent);
|
| 174 |
+
const previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);
|
| 175 |
+
const argsKey = previewInput ? JSON.stringify({ path: previewInput.path, edits: previewInput.edits }) : undefined;
|
| 176 |
+
|
| 177 |
+
if (component.previewArgsKey !== argsKey) {
|
| 178 |
+
component.preview = undefined;
|
| 179 |
+
component.previewArgsKey = argsKey;
|
| 180 |
+
component.previewPending = false;
|
| 181 |
+
component.settledError = false;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if (context.argsComplete && previewInput && !component.preview && !component.previewPending) {
|
| 185 |
+
component.previewPending = true;
|
| 186 |
+
const requestKey = argsKey;
|
| 187 |
+
void computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {
|
| 188 |
+
if (component.previewArgsKey === requestKey) {
|
| 189 |
+
setEditPreview(component, preview, requestKey);
|
| 190 |
+
context.invalidate();
|
| 191 |
+
}
|
| 192 |
+
});
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
return buildEditCallComponent(component, args as RenderableEditArgs | undefined, theme, context.cwd);
|
| 196 |
+
},
|
| 197 |
+
renderResult(result, _options, theme, context) {
|
| 198 |
+
const callComponent = context.state.callComponent;
|
| 199 |
+
const previewInput = getRenderablePreviewInput(context.args as RenderableEditArgs | undefined);
|
| 200 |
+
const argsKey = previewInput ? JSON.stringify({ path: previewInput.path, edits: previewInput.edits }) : undefined;
|
| 201 |
+
const typedResult = result as EditToolResultLike;
|
| 202 |
+
const resultDiff = !context.isError ? typedResult.details?.diff : undefined;
|
| 203 |
+
let changed = false;
|
| 204 |
+
if (callComponent) {
|
| 205 |
+
if (typeof resultDiff === "string") {
|
| 206 |
+
changed =
|
| 207 |
+
setEditPreview(
|
| 208 |
+
callComponent,
|
| 209 |
+
{ diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine },
|
| 210 |
+
argsKey,
|
| 211 |
+
) || changed;
|
| 212 |
+
}
|
| 213 |
+
if (callComponent.settledError !== context.isError) {
|
| 214 |
+
callComponent.settledError = context.isError;
|
| 215 |
+
changed = true;
|
| 216 |
+
}
|
| 217 |
+
if (changed) {
|
| 218 |
+
buildEditCallComponent(callComponent, context.args as RenderableEditArgs | undefined, theme, context.cwd);
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
const output = formatEditResult(
|
| 223 |
+
context.args as RenderableEditArgs | undefined,
|
| 224 |
+
callComponent?.preview,
|
| 225 |
+
typedResult,
|
| 226 |
+
theme,
|
| 227 |
+
context.isError,
|
| 228 |
+
);
|
| 229 |
+
const component = (context.lastComponent as Container | undefined) ?? new Container();
|
| 230 |
+
component.clear();
|
| 231 |
+
if (!output) {
|
| 232 |
+
return component;
|
| 233 |
+
}
|
| 234 |
+
component.addChild(new Spacer(1));
|
| 235 |
+
component.addChild(new Text(output, 1, 0));
|
| 236 |
+
return component;
|
| 237 |
+
},
|
| 238 |
+
};
|
packages/coding-agent/src/core/tools/renderers/find.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the find tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `find.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Text } from "@earendil-works/pi-tui";
|
| 10 |
+
import { keyHint } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 11 |
+
import type { Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 12 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 13 |
+
import type { FindToolDetails } from "../find.ts";
|
| 14 |
+
import { getTextOutput, invalidArgText, shortenPath, str } from "../render-utils.ts";
|
| 15 |
+
import { DEFAULT_MAX_BYTES, formatSize } from "../truncate.ts";
|
| 16 |
+
|
| 17 |
+
function formatFindCall(args: { pattern: string; path?: string; limit?: number } | undefined, theme: Theme): string {
|
| 18 |
+
const pattern = str(args?.pattern);
|
| 19 |
+
const rawPath = str(args?.path);
|
| 20 |
+
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
| 21 |
+
const limit = args?.limit;
|
| 22 |
+
const invalidArg = invalidArgText(theme);
|
| 23 |
+
let text =
|
| 24 |
+
theme.fg("toolTitle", theme.bold("find")) +
|
| 25 |
+
" " +
|
| 26 |
+
(pattern === null ? invalidArg : theme.fg("accent", pattern || "")) +
|
| 27 |
+
theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
|
| 28 |
+
if (limit !== undefined) {
|
| 29 |
+
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
| 30 |
+
}
|
| 31 |
+
return text;
|
| 32 |
+
}
|
| 33 |
+
function formatFindResult(
|
| 34 |
+
result: {
|
| 35 |
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
| 36 |
+
details?: FindToolDetails;
|
| 37 |
+
},
|
| 38 |
+
options: ToolRenderResultOptions,
|
| 39 |
+
theme: Theme,
|
| 40 |
+
showImages: boolean,
|
| 41 |
+
): string {
|
| 42 |
+
const output = getTextOutput(result, showImages).trim();
|
| 43 |
+
let text = "";
|
| 44 |
+
if (output) {
|
| 45 |
+
const lines = output.split("\n");
|
| 46 |
+
const maxLines = options.expanded ? lines.length : 20;
|
| 47 |
+
const displayLines = lines.slice(0, maxLines);
|
| 48 |
+
const remaining = lines.length - maxLines;
|
| 49 |
+
text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
| 50 |
+
if (remaining > 0) {
|
| 51 |
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 52 |
+
}
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
const resultLimit = result.details?.resultLimitReached;
|
| 56 |
+
const truncation = result.details?.truncation;
|
| 57 |
+
if (resultLimit || truncation?.truncated) {
|
| 58 |
+
const warnings: string[] = [];
|
| 59 |
+
if (resultLimit) warnings.push(`${resultLimit} results limit`);
|
| 60 |
+
if (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
| 61 |
+
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
| 62 |
+
}
|
| 63 |
+
return text;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
export const findRenderers: Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> = {
|
| 67 |
+
renderCall(args, theme, context) {
|
| 68 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 69 |
+
text.setText(formatFindCall(args as any, theme));
|
| 70 |
+
return text;
|
| 71 |
+
},
|
| 72 |
+
renderResult(result, options, theme, context) {
|
| 73 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 74 |
+
text.setText(formatFindResult(result as any, options, theme, context.showImages));
|
| 75 |
+
return text;
|
| 76 |
+
},
|
| 77 |
+
};
|
packages/coding-agent/src/core/tools/renderers/grep.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the grep tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `grep.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Text } from "@earendil-works/pi-tui";
|
| 10 |
+
import { keyHint } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 11 |
+
import type { Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 12 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 13 |
+
import type { GrepToolDetails } from "../grep.ts";
|
| 14 |
+
import { getTextOutput, invalidArgText, shortenPath, str } from "../render-utils.ts";
|
| 15 |
+
import { DEFAULT_MAX_BYTES, formatSize } from "../truncate.ts";
|
| 16 |
+
|
| 17 |
+
function formatGrepCall(
|
| 18 |
+
args: { pattern: string; path?: string; glob?: string; limit?: number } | undefined,
|
| 19 |
+
theme: Theme,
|
| 20 |
+
): string {
|
| 21 |
+
const pattern = str(args?.pattern);
|
| 22 |
+
const rawPath = str(args?.path);
|
| 23 |
+
const path = rawPath !== null ? shortenPath(rawPath || ".") : null;
|
| 24 |
+
const glob = str(args?.glob);
|
| 25 |
+
const limit = args?.limit;
|
| 26 |
+
const invalidArg = invalidArgText(theme);
|
| 27 |
+
let text =
|
| 28 |
+
theme.fg("toolTitle", theme.bold("grep")) +
|
| 29 |
+
" " +
|
| 30 |
+
(pattern === null ? invalidArg : theme.fg("accent", `/${pattern || ""}/`)) +
|
| 31 |
+
theme.fg("toolOutput", ` in ${path === null ? invalidArg : path}`);
|
| 32 |
+
if (glob) text += theme.fg("toolOutput", ` (${glob})`);
|
| 33 |
+
if (limit !== undefined) text += theme.fg("toolOutput", ` limit ${limit}`);
|
| 34 |
+
return text;
|
| 35 |
+
}
|
| 36 |
+
function formatGrepResult(
|
| 37 |
+
result: {
|
| 38 |
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
| 39 |
+
details?: GrepToolDetails;
|
| 40 |
+
},
|
| 41 |
+
options: ToolRenderResultOptions,
|
| 42 |
+
theme: Theme,
|
| 43 |
+
showImages: boolean,
|
| 44 |
+
): string {
|
| 45 |
+
const output = getTextOutput(result, showImages).trim();
|
| 46 |
+
let text = "";
|
| 47 |
+
if (output) {
|
| 48 |
+
const lines = output.split("\n");
|
| 49 |
+
const maxLines = options.expanded ? lines.length : 15;
|
| 50 |
+
const displayLines = lines.slice(0, maxLines);
|
| 51 |
+
const remaining = lines.length - maxLines;
|
| 52 |
+
text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
| 53 |
+
if (remaining > 0) {
|
| 54 |
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
const matchLimit = result.details?.matchLimitReached;
|
| 59 |
+
const truncation = result.details?.truncation;
|
| 60 |
+
const linesTruncated = result.details?.linesTruncated;
|
| 61 |
+
if (matchLimit || truncation?.truncated || linesTruncated) {
|
| 62 |
+
const warnings: string[] = [];
|
| 63 |
+
if (matchLimit) warnings.push(`${matchLimit} matches limit`);
|
| 64 |
+
if (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
| 65 |
+
if (linesTruncated) warnings.push("some lines truncated");
|
| 66 |
+
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
| 67 |
+
}
|
| 68 |
+
return text;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export const grepRenderers: Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> = {
|
| 72 |
+
renderCall(args, theme, context) {
|
| 73 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 74 |
+
text.setText(formatGrepCall(args as any, theme));
|
| 75 |
+
return text;
|
| 76 |
+
},
|
| 77 |
+
renderResult(result, options, theme, context) {
|
| 78 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 79 |
+
text.setText(formatGrepResult(result as any, options, theme, context.showImages));
|
| 80 |
+
return text;
|
| 81 |
+
},
|
| 82 |
+
};
|
packages/coding-agent/src/core/tools/renderers/index.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Built-in tool renderers, without the tools themselves.
|
| 3 |
+
*
|
| 4 |
+
* A presentation displays tool calls and results; it does not execute them and does not need their
|
| 5 |
+
* typebox parameter schemas. Importing this instead of `core/tools/index.ts` keeps ~17 MB of module
|
| 6 |
+
* graph out of a process that only renders.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import type { ToolDefinition } from "../../extensions/types.ts";
|
| 10 |
+
import type { ToolName } from "../index.ts";
|
| 11 |
+
import { createShellRenderers } from "./bash.ts";
|
| 12 |
+
import { editRenderers } from "./edit.ts";
|
| 13 |
+
import { findRenderers } from "./find.ts";
|
| 14 |
+
import { grepRenderers } from "./grep.ts";
|
| 15 |
+
import { lsRenderers } from "./ls.ts";
|
| 16 |
+
import { readRenderers } from "./read.ts";
|
| 17 |
+
import { writeRenderers } from "./write.ts";
|
| 18 |
+
|
| 19 |
+
export type ToolRenderers = Pick<ToolDefinition<any, any>, "renderCall" | "renderResult">;
|
| 20 |
+
|
| 21 |
+
export {
|
| 22 |
+
createShellRenderers,
|
| 23 |
+
editRenderers,
|
| 24 |
+
findRenderers,
|
| 25 |
+
grepRenderers,
|
| 26 |
+
lsRenderers,
|
| 27 |
+
readRenderers,
|
| 28 |
+
writeRenderers,
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
/** Renderers for every built-in tool, keyed by tool name. */
|
| 32 |
+
export function createAllToolRenderers(): Record<ToolName, ToolRenderers> {
|
| 33 |
+
return {
|
| 34 |
+
read: readRenderers,
|
| 35 |
+
bash: createShellRenderers("$"),
|
| 36 |
+
powershell: createShellRenderers("PS>"),
|
| 37 |
+
edit: editRenderers,
|
| 38 |
+
write: writeRenderers,
|
| 39 |
+
grep: grepRenderers,
|
| 40 |
+
find: findRenderers,
|
| 41 |
+
ls: lsRenderers,
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
/**
|
| 46 |
+
* Merge built-in renderers into a tool definition that does not supply its own.
|
| 47 |
+
*
|
| 48 |
+
* `ToolExecutionComponent` used to do this lookup itself, which forced every presentation to import
|
| 49 |
+
* the tool implementations. Callers do it now, so a process that renders can import renderers alone.
|
| 50 |
+
*/
|
| 51 |
+
export function withBuiltInRenderers<TDefinition extends ToolRenderers>(
|
| 52 |
+
toolName: string,
|
| 53 |
+
definition: TDefinition | undefined,
|
| 54 |
+
): TDefinition | ToolRenderers | undefined {
|
| 55 |
+
const builtIn = createAllToolRenderers()[toolName as ToolName];
|
| 56 |
+
if (!definition) return builtIn;
|
| 57 |
+
if (!builtIn) return definition;
|
| 58 |
+
return {
|
| 59 |
+
...definition,
|
| 60 |
+
renderCall: definition.renderCall ?? builtIn.renderCall,
|
| 61 |
+
renderResult: definition.renderResult ?? builtIn.renderResult,
|
| 62 |
+
};
|
| 63 |
+
}
|
packages/coding-agent/src/core/tools/renderers/ls.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the ls tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `ls.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Text } from "@earendil-works/pi-tui";
|
| 10 |
+
import { keyHint } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 11 |
+
import type { Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 12 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 13 |
+
import type { LsToolDetails } from "../ls.ts";
|
| 14 |
+
import { getTextOutput, renderToolPath, str } from "../render-utils.ts";
|
| 15 |
+
import { DEFAULT_MAX_BYTES, formatSize } from "../truncate.ts";
|
| 16 |
+
|
| 17 |
+
function formatLsCall(args: { path?: string; limit?: number } | undefined, theme: Theme, cwd: string): string {
|
| 18 |
+
const limit = args?.limit;
|
| 19 |
+
const pathDisplay = renderToolPath(str(args?.path), theme, cwd, { emptyFallback: "." });
|
| 20 |
+
let text = `${theme.fg("toolTitle", theme.bold("ls"))} ${pathDisplay}`;
|
| 21 |
+
if (limit !== undefined) {
|
| 22 |
+
text += theme.fg("toolOutput", ` (limit ${limit})`);
|
| 23 |
+
}
|
| 24 |
+
return text;
|
| 25 |
+
}
|
| 26 |
+
function formatLsResult(
|
| 27 |
+
result: {
|
| 28 |
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
| 29 |
+
details?: LsToolDetails;
|
| 30 |
+
},
|
| 31 |
+
options: ToolRenderResultOptions,
|
| 32 |
+
theme: Theme,
|
| 33 |
+
showImages: boolean,
|
| 34 |
+
): string {
|
| 35 |
+
const output = getTextOutput(result, showImages).trim();
|
| 36 |
+
let text = "";
|
| 37 |
+
if (output) {
|
| 38 |
+
const lines = output.split("\n");
|
| 39 |
+
const maxLines = options.expanded ? lines.length : 20;
|
| 40 |
+
const displayLines = lines.slice(0, maxLines);
|
| 41 |
+
const remaining = lines.length - maxLines;
|
| 42 |
+
text += `\n${displayLines.map((line) => theme.fg("toolOutput", line)).join("\n")}`;
|
| 43 |
+
if (remaining > 0) {
|
| 44 |
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 45 |
+
}
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
const entryLimit = result.details?.entryLimitReached;
|
| 49 |
+
const truncation = result.details?.truncation;
|
| 50 |
+
if (entryLimit || truncation?.truncated) {
|
| 51 |
+
const warnings: string[] = [];
|
| 52 |
+
if (entryLimit) warnings.push(`${entryLimit} entries limit`);
|
| 53 |
+
if (truncation?.truncated) warnings.push(`${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`);
|
| 54 |
+
text += `\n${theme.fg("warning", `[Truncated: ${warnings.join(", ")}]`)}`;
|
| 55 |
+
}
|
| 56 |
+
return text;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export const lsRenderers: Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> = {
|
| 60 |
+
renderCall(args, theme, context) {
|
| 61 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 62 |
+
text.setText(formatLsCall(args as any, theme, context.cwd));
|
| 63 |
+
return text;
|
| 64 |
+
},
|
| 65 |
+
renderResult(result, options, theme, context) {
|
| 66 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 67 |
+
text.setText(formatLsResult(result as any, options, theme, context.showImages));
|
| 68 |
+
return text;
|
| 69 |
+
},
|
| 70 |
+
};
|
packages/coding-agent/src/core/tools/renderers/read.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the read tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `read.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
|
| 10 |
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
| 11 |
+
import { Text } from "@earendil-works/pi-tui";
|
| 12 |
+
import { getReadmePath } from "../../../config.ts";
|
| 13 |
+
import { keyHint, keyText } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 14 |
+
import { getLanguageFromPath, highlightCode, type Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 15 |
+
import { formatPathRelativeToCwdOrAbsolute } from "../../../utils/paths.ts";
|
| 16 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 17 |
+
import { resolveToCwd } from "../path-utils.ts";
|
| 18 |
+
import type { ReadToolDetails } from "../read.ts";
|
| 19 |
+
import { getTextOutput, renderToolPath, replaceTabs, str } from "../render-utils.ts";
|
| 20 |
+
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "../truncate.ts";
|
| 21 |
+
|
| 22 |
+
interface CompactReadClassification {
|
| 23 |
+
kind: "docs" | "resource" | "skill";
|
| 24 |
+
label: string;
|
| 25 |
+
}
|
| 26 |
+
const COMPACT_RESOURCE_FILE_NAMES = new Set(["AGENTS.override.md", "AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
| 27 |
+
type ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };
|
| 28 |
+
function formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {
|
| 29 |
+
if (args?.offset === undefined && args?.limit === undefined) return "";
|
| 30 |
+
const startLine = args.offset ?? 1;
|
| 31 |
+
const endLine = args.limit !== undefined ? startLine + args.limit - 1 : "";
|
| 32 |
+
return theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
| 33 |
+
}
|
| 34 |
+
function formatReadCall(args: ReadRenderArgs | undefined, theme: Theme, cwd: string): string {
|
| 35 |
+
const pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);
|
| 36 |
+
return `${theme.fg("toolTitle", theme.bold("read"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;
|
| 37 |
+
}
|
| 38 |
+
function trimTrailingEmptyLines(lines: string[]): string[] {
|
| 39 |
+
let end = lines.length;
|
| 40 |
+
while (end > 0 && lines[end - 1] === "") {
|
| 41 |
+
end--;
|
| 42 |
+
}
|
| 43 |
+
return lines.slice(0, end);
|
| 44 |
+
}
|
| 45 |
+
function toPosixPath(filePath: string): string {
|
| 46 |
+
return filePath.split(sep).join("/");
|
| 47 |
+
}
|
| 48 |
+
function getPiDocsClassification(absolutePath: string): CompactReadClassification | undefined {
|
| 49 |
+
const packageRoot = dirname(getReadmePath());
|
| 50 |
+
const relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));
|
| 51 |
+
if (
|
| 52 |
+
relativePath === "" ||
|
| 53 |
+
relativePath === ".." ||
|
| 54 |
+
relativePath.startsWith(`..${sep}`) ||
|
| 55 |
+
isAbsolute(relativePath)
|
| 56 |
+
) {
|
| 57 |
+
return undefined;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
const label = toPosixPath(relativePath);
|
| 61 |
+
if (label === "README.md" || label.startsWith("docs/") || label.startsWith("examples/")) {
|
| 62 |
+
return { kind: "docs", label };
|
| 63 |
+
}
|
| 64 |
+
return undefined;
|
| 65 |
+
}
|
| 66 |
+
function getCompactReadClassification(
|
| 67 |
+
args: ReadRenderArgs | undefined,
|
| 68 |
+
cwd: string,
|
| 69 |
+
): CompactReadClassification | undefined {
|
| 70 |
+
const rawPath = str(args?.file_path ?? args?.path);
|
| 71 |
+
if (!rawPath) return undefined;
|
| 72 |
+
|
| 73 |
+
const absolutePath = resolveToCwd(rawPath, cwd);
|
| 74 |
+
const fileName = basename(absolutePath);
|
| 75 |
+
if (fileName === "SKILL.md") {
|
| 76 |
+
return { kind: "skill", label: basename(dirname(absolutePath)) || fileName };
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const docsClassification = getPiDocsClassification(absolutePath);
|
| 80 |
+
if (docsClassification) return docsClassification;
|
| 81 |
+
|
| 82 |
+
if (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {
|
| 83 |
+
return { kind: "resource", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return undefined;
|
| 87 |
+
}
|
| 88 |
+
function formatCompactReadCall(
|
| 89 |
+
classification: CompactReadClassification,
|
| 90 |
+
args: ReadRenderArgs | undefined,
|
| 91 |
+
theme: Theme,
|
| 92 |
+
): string {
|
| 93 |
+
const expandHint = theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
|
| 94 |
+
if (classification.kind === "skill") {
|
| 95 |
+
return (
|
| 96 |
+
theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
|
| 97 |
+
theme.fg("customMessageText", classification.label) +
|
| 98 |
+
formatReadLineRange(args, theme) +
|
| 99 |
+
expandHint
|
| 100 |
+
);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return (
|
| 104 |
+
theme.fg("toolTitle", theme.bold(`read ${classification.kind}`)) +
|
| 105 |
+
" " +
|
| 106 |
+
theme.fg("accent", classification.label) +
|
| 107 |
+
formatReadLineRange(args, theme) +
|
| 108 |
+
expandHint
|
| 109 |
+
);
|
| 110 |
+
}
|
| 111 |
+
function formatReadResult(
|
| 112 |
+
args: ReadRenderArgs | undefined,
|
| 113 |
+
result: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },
|
| 114 |
+
options: ToolRenderResultOptions,
|
| 115 |
+
theme: Theme,
|
| 116 |
+
showImages: boolean,
|
| 117 |
+
_cwd: string,
|
| 118 |
+
isError: boolean,
|
| 119 |
+
): string {
|
| 120 |
+
if (!options.expanded && !isError) {
|
| 121 |
+
return "";
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
const rawPath = str(args?.file_path ?? args?.path);
|
| 125 |
+
const output = getTextOutput(result, showImages);
|
| 126 |
+
const lang = !isError && rawPath ? getLanguageFromPath(rawPath) : undefined;
|
| 127 |
+
const renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
| 128 |
+
const lines = trimTrailingEmptyLines(renderedLines);
|
| 129 |
+
const maxLines = options.expanded ? lines.length : 10;
|
| 130 |
+
const displayLines = lines.slice(0, maxLines);
|
| 131 |
+
const remaining = lines.length - maxLines;
|
| 132 |
+
let text = `\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`;
|
| 133 |
+
if (remaining > 0) {
|
| 134 |
+
text += `${theme.fg("muted", `\n... (${remaining} more lines,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
const truncation = result.details?.truncation;
|
| 138 |
+
if (truncation?.truncated) {
|
| 139 |
+
if (truncation.firstLineExceedsLimit) {
|
| 140 |
+
text += `\n${theme.fg("warning", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;
|
| 141 |
+
} else if (truncation.truncatedBy === "lines") {
|
| 142 |
+
text += `\n${theme.fg("warning", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;
|
| 143 |
+
} else {
|
| 144 |
+
text += `\n${theme.fg("warning", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
return text;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
export const readRenderers: Pick<ToolDefinition<any, ReadToolDetails | undefined>, "renderCall" | "renderResult"> = {
|
| 151 |
+
renderCall(rawArgs, theme, context) {
|
| 152 |
+
const args = rawArgs as ReadRenderArgs | undefined;
|
| 153 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 154 |
+
const classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;
|
| 155 |
+
text.setText(
|
| 156 |
+
classification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme, context.cwd),
|
| 157 |
+
);
|
| 158 |
+
return text;
|
| 159 |
+
},
|
| 160 |
+
renderResult(result, options, theme, context) {
|
| 161 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 162 |
+
text.setText(
|
| 163 |
+
formatReadResult(
|
| 164 |
+
context.args as ReadRenderArgs | undefined,
|
| 165 |
+
result,
|
| 166 |
+
options,
|
| 167 |
+
theme,
|
| 168 |
+
context.showImages,
|
| 169 |
+
context.cwd,
|
| 170 |
+
context.isError,
|
| 171 |
+
),
|
| 172 |
+
);
|
| 173 |
+
return text;
|
| 174 |
+
},
|
| 175 |
+
};
|
packages/coding-agent/src/core/tools/renderers/write.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Presentation for the write tool.
|
| 3 |
+
*
|
| 4 |
+
* Renderers live apart from the implementation so a process that only displays tool output does not
|
| 5 |
+
* load the execution path or its typebox parameter schema. `write.ts` spreads these into its
|
| 6 |
+
* definition, so the tool's public shape is unchanged.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { Container, Text } from "@earendil-works/pi-tui";
|
| 10 |
+
import { keyHint } from "../../../modes/interactive/components/keybinding-hints.ts";
|
| 11 |
+
import { getLanguageFromPath, highlightCode, type Theme } from "../../../modes/interactive/theme/theme.ts";
|
| 12 |
+
import type { ToolDefinition, ToolRenderResultOptions } from "../../extensions/types.ts";
|
| 13 |
+
import { normalizeDisplayText, renderToolPath, replaceTabs, str } from "../render-utils.ts";
|
| 14 |
+
|
| 15 |
+
type WriteHighlightCache = {
|
| 16 |
+
rawPath: string | null;
|
| 17 |
+
lang: string;
|
| 18 |
+
rawContent: string;
|
| 19 |
+
normalizedLines: string[];
|
| 20 |
+
highlightedLines: string[];
|
| 21 |
+
};
|
| 22 |
+
class WriteCallRenderComponent extends Text {
|
| 23 |
+
cache?: WriteHighlightCache;
|
| 24 |
+
|
| 25 |
+
constructor() {
|
| 26 |
+
super("", 0, 0);
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
const WRITE_PARTIAL_FULL_HIGHLIGHT_LINES = 50;
|
| 30 |
+
function highlightSingleLine(line: string, lang: string): string {
|
| 31 |
+
const highlighted = highlightCode(line, lang);
|
| 32 |
+
return highlighted[0] ?? "";
|
| 33 |
+
}
|
| 34 |
+
function refreshWriteHighlightPrefix(cache: WriteHighlightCache): void {
|
| 35 |
+
const prefixCount = Math.min(WRITE_PARTIAL_FULL_HIGHLIGHT_LINES, cache.normalizedLines.length);
|
| 36 |
+
if (prefixCount === 0) return;
|
| 37 |
+
const prefixSource = cache.normalizedLines.slice(0, prefixCount).join("\n");
|
| 38 |
+
const prefixHighlighted = highlightCode(prefixSource, cache.lang);
|
| 39 |
+
for (let i = 0; i < prefixCount; i++) {
|
| 40 |
+
cache.highlightedLines[i] =
|
| 41 |
+
prefixHighlighted[i] ?? highlightSingleLine(cache.normalizedLines[i] ?? "", cache.lang);
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
function rebuildWriteHighlightCacheFull(rawPath: string | null, fileContent: string): WriteHighlightCache | undefined {
|
| 45 |
+
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
| 46 |
+
if (!lang) return undefined;
|
| 47 |
+
const displayContent = normalizeDisplayText(fileContent);
|
| 48 |
+
const normalized = replaceTabs(displayContent);
|
| 49 |
+
return {
|
| 50 |
+
rawPath,
|
| 51 |
+
lang,
|
| 52 |
+
rawContent: fileContent,
|
| 53 |
+
normalizedLines: normalized.split("\n"),
|
| 54 |
+
highlightedLines: highlightCode(normalized, lang),
|
| 55 |
+
};
|
| 56 |
+
}
|
| 57 |
+
function updateWriteHighlightCacheIncremental(
|
| 58 |
+
cache: WriteHighlightCache | undefined,
|
| 59 |
+
rawPath: string | null,
|
| 60 |
+
fileContent: string,
|
| 61 |
+
): WriteHighlightCache | undefined {
|
| 62 |
+
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
| 63 |
+
if (!lang) return undefined;
|
| 64 |
+
if (!cache) return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
| 65 |
+
if (cache.lang !== lang || cache.rawPath !== rawPath) return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
| 66 |
+
if (!fileContent.startsWith(cache.rawContent)) return rebuildWriteHighlightCacheFull(rawPath, fileContent);
|
| 67 |
+
if (fileContent.length === cache.rawContent.length) return cache;
|
| 68 |
+
|
| 69 |
+
const deltaRaw = fileContent.slice(cache.rawContent.length);
|
| 70 |
+
const deltaDisplay = normalizeDisplayText(deltaRaw);
|
| 71 |
+
const deltaNormalized = replaceTabs(deltaDisplay);
|
| 72 |
+
cache.rawContent = fileContent;
|
| 73 |
+
if (cache.normalizedLines.length === 0) {
|
| 74 |
+
cache.normalizedLines.push("");
|
| 75 |
+
cache.highlightedLines.push("");
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const segments = deltaNormalized.split("\n");
|
| 79 |
+
const lastIndex = cache.normalizedLines.length - 1;
|
| 80 |
+
cache.normalizedLines[lastIndex] += segments[0];
|
| 81 |
+
cache.highlightedLines[lastIndex] = highlightSingleLine(cache.normalizedLines[lastIndex], cache.lang);
|
| 82 |
+
for (let i = 1; i < segments.length; i++) {
|
| 83 |
+
cache.normalizedLines.push(segments[i]);
|
| 84 |
+
cache.highlightedLines.push(highlightSingleLine(segments[i], cache.lang));
|
| 85 |
+
}
|
| 86 |
+
refreshWriteHighlightPrefix(cache);
|
| 87 |
+
return cache;
|
| 88 |
+
}
|
| 89 |
+
function trimTrailingEmptyLines(lines: string[]): string[] {
|
| 90 |
+
let end = lines.length;
|
| 91 |
+
while (end > 0 && lines[end - 1] === "") {
|
| 92 |
+
end--;
|
| 93 |
+
}
|
| 94 |
+
return lines.slice(0, end);
|
| 95 |
+
}
|
| 96 |
+
function formatWriteCall(
|
| 97 |
+
args: { path?: string; file_path?: string; content?: string } | undefined,
|
| 98 |
+
options: ToolRenderResultOptions,
|
| 99 |
+
theme: Theme,
|
| 100 |
+
cache: WriteHighlightCache | undefined,
|
| 101 |
+
cwd: string,
|
| 102 |
+
): string {
|
| 103 |
+
const rawPath = str(args?.file_path ?? args?.path);
|
| 104 |
+
const fileContent = str(args?.content);
|
| 105 |
+
const pathDisplay = renderToolPath(rawPath, theme, cwd);
|
| 106 |
+
let text = `${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}`;
|
| 107 |
+
|
| 108 |
+
if (fileContent === null) {
|
| 109 |
+
text += `\n\n${theme.fg("error", "[invalid content arg - expected string]")}`;
|
| 110 |
+
} else if (fileContent) {
|
| 111 |
+
const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
|
| 112 |
+
const renderedLines = lang
|
| 113 |
+
? (cache?.highlightedLines ?? highlightCode(replaceTabs(normalizeDisplayText(fileContent)), lang))
|
| 114 |
+
: normalizeDisplayText(fileContent).split("\n");
|
| 115 |
+
const lines = trimTrailingEmptyLines(renderedLines);
|
| 116 |
+
const totalLines = lines.length;
|
| 117 |
+
const maxLines = options.expanded ? lines.length : 10;
|
| 118 |
+
const displayLines = lines.slice(0, maxLines);
|
| 119 |
+
const remaining = lines.length - maxLines;
|
| 120 |
+
text += `\n\n${displayLines.map((line) => (lang ? line : theme.fg("toolOutput", replaceTabs(line)))).join("\n")}`;
|
| 121 |
+
if (remaining > 0) {
|
| 122 |
+
text += `${theme.fg("muted", `\n... (${remaining} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")}${theme.fg("muted", ")")}`;
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
return text;
|
| 127 |
+
}
|
| 128 |
+
function formatWriteResult(
|
| 129 |
+
result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError?: boolean },
|
| 130 |
+
theme: Theme,
|
| 131 |
+
): string | undefined {
|
| 132 |
+
if (!result.isError) {
|
| 133 |
+
return undefined;
|
| 134 |
+
}
|
| 135 |
+
const output = result.content
|
| 136 |
+
.filter((c) => c.type === "text")
|
| 137 |
+
.map((c) => c.text || "")
|
| 138 |
+
.join("\n");
|
| 139 |
+
if (!output) {
|
| 140 |
+
return undefined;
|
| 141 |
+
}
|
| 142 |
+
return `\n${theme.fg("error", output)}`;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
export const writeRenderers: Pick<ToolDefinition<any, any>, "renderCall" | "renderResult"> = {
|
| 146 |
+
renderCall(args, theme, context) {
|
| 147 |
+
const renderArgs = args as { path?: string; file_path?: string; content?: string } | undefined;
|
| 148 |
+
const rawPath = str(renderArgs?.file_path ?? renderArgs?.path);
|
| 149 |
+
const fileContent = str(renderArgs?.content);
|
| 150 |
+
const component =
|
| 151 |
+
(context.lastComponent as WriteCallRenderComponent | undefined) ?? new WriteCallRenderComponent();
|
| 152 |
+
if (fileContent !== null) {
|
| 153 |
+
component.cache = context.argsComplete
|
| 154 |
+
? rebuildWriteHighlightCacheFull(rawPath, fileContent)
|
| 155 |
+
: updateWriteHighlightCacheIncremental(component.cache, rawPath, fileContent);
|
| 156 |
+
} else {
|
| 157 |
+
component.cache = undefined;
|
| 158 |
+
}
|
| 159 |
+
component.setText(
|
| 160 |
+
formatWriteCall(
|
| 161 |
+
renderArgs,
|
| 162 |
+
{ expanded: context.expanded, isPartial: context.isPartial },
|
| 163 |
+
theme,
|
| 164 |
+
component.cache,
|
| 165 |
+
context.cwd,
|
| 166 |
+
),
|
| 167 |
+
);
|
| 168 |
+
return component;
|
| 169 |
+
},
|
| 170 |
+
renderResult(result, _options, theme, context) {
|
| 171 |
+
const output = formatWriteResult({ ...result, isError: context.isError }, theme);
|
| 172 |
+
if (!output) {
|
| 173 |
+
const component = (context.lastComponent as Container | undefined) ?? new Container();
|
| 174 |
+
component.clear();
|
| 175 |
+
return component;
|
| 176 |
+
}
|
| 177 |
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
| 178 |
+
text.setText(output);
|
| 179 |
+
return text;
|
| 180 |
+
},
|
| 181 |
+
};
|
packages/coding-agent/src/core/tools/tool-definition-wrapper.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 2 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 3 |
+
|
| 4 |
+
/** Wrap a ToolDefinition into an AgentTool for the core runtime. */
|
| 5 |
+
export function wrapToolDefinition<TDetails = unknown>(
|
| 6 |
+
definition: ToolDefinition<any, TDetails>,
|
| 7 |
+
ctxFactory?: () => ExtensionContext,
|
| 8 |
+
): AgentTool<any, TDetails> {
|
| 9 |
+
return {
|
| 10 |
+
name: definition.name,
|
| 11 |
+
label: definition.label,
|
| 12 |
+
description: definition.description,
|
| 13 |
+
parameters: definition.parameters,
|
| 14 |
+
constrainedSampling: definition.constrainedSampling,
|
| 15 |
+
prepareArguments: definition.prepareArguments,
|
| 16 |
+
executionMode: definition.executionMode,
|
| 17 |
+
execute: (toolCallId, params, signal, onUpdate, ctx?: ExtensionContext) =>
|
| 18 |
+
definition.execute(toolCallId, params, signal, onUpdate, ctx ?? (ctxFactory?.() as ExtensionContext)),
|
| 19 |
+
};
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
/** Wrap multiple ToolDefinitions into AgentTools for the core runtime. */
|
| 23 |
+
export function wrapToolDefinitions(
|
| 24 |
+
definitions: ToolDefinition<any, any>[],
|
| 25 |
+
ctxFactory?: () => ExtensionContext,
|
| 26 |
+
): AgentTool<any>[] {
|
| 27 |
+
return definitions.map((definition) => wrapToolDefinition(definition, ctxFactory));
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
/**
|
| 31 |
+
* Synthesize a minimal ToolDefinition from an AgentTool.
|
| 32 |
+
*
|
| 33 |
+
* This keeps AgentSession's internal registry definition-first even when a caller
|
| 34 |
+
* provides plain AgentTool overrides that do not include prompt metadata or renderers.
|
| 35 |
+
*/
|
| 36 |
+
export function createToolDefinitionFromAgentTool(tool: AgentTool<any>): ToolDefinition<any, unknown> {
|
| 37 |
+
return {
|
| 38 |
+
name: tool.name,
|
| 39 |
+
label: tool.label,
|
| 40 |
+
description: tool.description,
|
| 41 |
+
parameters: tool.parameters as any,
|
| 42 |
+
constrainedSampling: tool.constrainedSampling,
|
| 43 |
+
prepareArguments: tool.prepareArguments,
|
| 44 |
+
executionMode: tool.executionMode,
|
| 45 |
+
execute: async (toolCallId, params, signal, onUpdate) => tool.execute(toolCallId, params, signal, onUpdate),
|
| 46 |
+
};
|
| 47 |
+
}
|
packages/coding-agent/src/core/tools/truncate.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared truncation utilities for tool outputs.
|
| 3 |
+
*
|
| 4 |
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
| 5 |
+
* - Line limit (default: 2000 lines)
|
| 6 |
+
* - Byte limit (default: 50KB)
|
| 7 |
+
*
|
| 8 |
+
* Never returns partial lines (except bash tail truncation edge case).
|
| 9 |
+
*/
|
| 10 |
+
|
| 11 |
+
export const DEFAULT_MAX_LINES = 2000;
|
| 12 |
+
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
| 13 |
+
export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
|
| 14 |
+
|
| 15 |
+
export interface TruncationResult {
|
| 16 |
+
/** The truncated content */
|
| 17 |
+
content: string;
|
| 18 |
+
/** Whether truncation occurred */
|
| 19 |
+
truncated: boolean;
|
| 20 |
+
/** Which limit was hit: "lines", "bytes", or null if not truncated */
|
| 21 |
+
truncatedBy: "lines" | "bytes" | null;
|
| 22 |
+
/** Total number of lines in the original content */
|
| 23 |
+
totalLines: number;
|
| 24 |
+
/** Total number of bytes in the original content */
|
| 25 |
+
totalBytes: number;
|
| 26 |
+
/** Number of complete lines in the truncated output */
|
| 27 |
+
outputLines: number;
|
| 28 |
+
/** Number of bytes in the truncated output */
|
| 29 |
+
outputBytes: number;
|
| 30 |
+
/** Whether the last line was partially truncated (only for tail truncation edge case) */
|
| 31 |
+
lastLinePartial: boolean;
|
| 32 |
+
/** Whether the first line exceeded the byte limit (for head truncation) */
|
| 33 |
+
firstLineExceedsLimit: boolean;
|
| 34 |
+
/** The max lines limit that was applied */
|
| 35 |
+
maxLines: number;
|
| 36 |
+
/** The max bytes limit that was applied */
|
| 37 |
+
maxBytes: number;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export interface TruncationOptions {
|
| 41 |
+
/** Maximum number of lines (default: 2000) */
|
| 42 |
+
maxLines?: number;
|
| 43 |
+
/** Maximum number of bytes (default: 50KB) */
|
| 44 |
+
maxBytes?: number;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function splitLinesForCounting(content: string): string[] {
|
| 48 |
+
if (content.length === 0) {
|
| 49 |
+
return [];
|
| 50 |
+
}
|
| 51 |
+
const lines = content.split("\n");
|
| 52 |
+
if (content.endsWith("\n")) {
|
| 53 |
+
lines.pop();
|
| 54 |
+
}
|
| 55 |
+
return lines;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
/**
|
| 59 |
+
* Format bytes as human-readable size.
|
| 60 |
+
*/
|
| 61 |
+
export function formatSize(bytes: number): string {
|
| 62 |
+
if (bytes < 1024) {
|
| 63 |
+
return `${bytes}B`;
|
| 64 |
+
} else if (bytes < 1024 * 1024) {
|
| 65 |
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
| 66 |
+
} else {
|
| 67 |
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/**
|
| 72 |
+
* Truncate content from the head (keep first N lines/bytes).
|
| 73 |
+
* Suitable for file reads where you want to see the beginning.
|
| 74 |
+
*
|
| 75 |
+
* Never returns partial lines. If first line exceeds byte limit,
|
| 76 |
+
* returns empty content with firstLineExceedsLimit=true.
|
| 77 |
+
*/
|
| 78 |
+
export function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {
|
| 79 |
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
| 80 |
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
| 81 |
+
|
| 82 |
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
| 83 |
+
const lines = splitLinesForCounting(content);
|
| 84 |
+
const totalLines = lines.length;
|
| 85 |
+
|
| 86 |
+
// Check if no truncation needed
|
| 87 |
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
| 88 |
+
return {
|
| 89 |
+
content,
|
| 90 |
+
truncated: false,
|
| 91 |
+
truncatedBy: null,
|
| 92 |
+
totalLines,
|
| 93 |
+
totalBytes,
|
| 94 |
+
outputLines: totalLines,
|
| 95 |
+
outputBytes: totalBytes,
|
| 96 |
+
lastLinePartial: false,
|
| 97 |
+
firstLineExceedsLimit: false,
|
| 98 |
+
maxLines,
|
| 99 |
+
maxBytes,
|
| 100 |
+
};
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
// Check if first line alone exceeds byte limit
|
| 104 |
+
const firstLineBytes = Buffer.byteLength(lines[0], "utf-8");
|
| 105 |
+
if (firstLineBytes > maxBytes) {
|
| 106 |
+
return {
|
| 107 |
+
content: "",
|
| 108 |
+
truncated: true,
|
| 109 |
+
truncatedBy: "bytes",
|
| 110 |
+
totalLines,
|
| 111 |
+
totalBytes,
|
| 112 |
+
outputLines: 0,
|
| 113 |
+
outputBytes: 0,
|
| 114 |
+
lastLinePartial: false,
|
| 115 |
+
firstLineExceedsLimit: true,
|
| 116 |
+
maxLines,
|
| 117 |
+
maxBytes,
|
| 118 |
+
};
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// Collect complete lines that fit
|
| 122 |
+
const outputLinesArr: string[] = [];
|
| 123 |
+
let outputBytesCount = 0;
|
| 124 |
+
let truncatedBy: "lines" | "bytes" = "lines";
|
| 125 |
+
|
| 126 |
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
| 127 |
+
const line = lines[i];
|
| 128 |
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
|
| 129 |
+
|
| 130 |
+
if (outputBytesCount + lineBytes > maxBytes) {
|
| 131 |
+
truncatedBy = "bytes";
|
| 132 |
+
break;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
outputLinesArr.push(line);
|
| 136 |
+
outputBytesCount += lineBytes;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// If we exited due to line limit
|
| 140 |
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
| 141 |
+
truncatedBy = "lines";
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
const outputContent = outputLinesArr.join("\n");
|
| 145 |
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
| 146 |
+
|
| 147 |
+
return {
|
| 148 |
+
content: outputContent,
|
| 149 |
+
truncated: true,
|
| 150 |
+
truncatedBy,
|
| 151 |
+
totalLines,
|
| 152 |
+
totalBytes,
|
| 153 |
+
outputLines: outputLinesArr.length,
|
| 154 |
+
outputBytes: finalOutputBytes,
|
| 155 |
+
lastLinePartial: false,
|
| 156 |
+
firstLineExceedsLimit: false,
|
| 157 |
+
maxLines,
|
| 158 |
+
maxBytes,
|
| 159 |
+
};
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
/**
|
| 163 |
+
* Truncate content from the tail (keep last N lines/bytes).
|
| 164 |
+
* Suitable for bash output where you want to see the end (errors, final results).
|
| 165 |
+
*
|
| 166 |
+
* May return partial first line if the last line of original content exceeds byte limit.
|
| 167 |
+
*/
|
| 168 |
+
export function truncateTail(content: string, options: TruncationOptions = {}): TruncationResult {
|
| 169 |
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
| 170 |
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
| 171 |
+
|
| 172 |
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
| 173 |
+
const lines = splitLinesForCounting(content);
|
| 174 |
+
const totalLines = lines.length;
|
| 175 |
+
|
| 176 |
+
// Check if no truncation needed
|
| 177 |
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
| 178 |
+
return {
|
| 179 |
+
content,
|
| 180 |
+
truncated: false,
|
| 181 |
+
truncatedBy: null,
|
| 182 |
+
totalLines,
|
| 183 |
+
totalBytes,
|
| 184 |
+
outputLines: totalLines,
|
| 185 |
+
outputBytes: totalBytes,
|
| 186 |
+
lastLinePartial: false,
|
| 187 |
+
firstLineExceedsLimit: false,
|
| 188 |
+
maxLines,
|
| 189 |
+
maxBytes,
|
| 190 |
+
};
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
// Work backwards from the end
|
| 194 |
+
const outputLinesArr: string[] = [];
|
| 195 |
+
let outputBytesCount = 0;
|
| 196 |
+
let truncatedBy: "lines" | "bytes" = "lines";
|
| 197 |
+
let lastLinePartial = false;
|
| 198 |
+
|
| 199 |
+
for (let i = lines.length - 1; i >= 0 && outputLinesArr.length < maxLines; i--) {
|
| 200 |
+
const line = lines[i];
|
| 201 |
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (outputLinesArr.length > 0 ? 1 : 0); // +1 for newline
|
| 202 |
+
|
| 203 |
+
if (outputBytesCount + lineBytes > maxBytes) {
|
| 204 |
+
truncatedBy = "bytes";
|
| 205 |
+
// Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
|
| 206 |
+
// take the end of the line (partial)
|
| 207 |
+
if (outputLinesArr.length === 0) {
|
| 208 |
+
const truncatedLine = truncateStringToBytesFromEnd(line, maxBytes);
|
| 209 |
+
outputLinesArr.unshift(truncatedLine);
|
| 210 |
+
outputBytesCount = Buffer.byteLength(truncatedLine, "utf-8");
|
| 211 |
+
lastLinePartial = true;
|
| 212 |
+
}
|
| 213 |
+
break;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
outputLinesArr.unshift(line);
|
| 217 |
+
outputBytesCount += lineBytes;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
// If we exited due to line limit
|
| 221 |
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
| 222 |
+
truncatedBy = "lines";
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
const outputContent = outputLinesArr.join("\n");
|
| 226 |
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
content: outputContent,
|
| 230 |
+
truncated: true,
|
| 231 |
+
truncatedBy,
|
| 232 |
+
totalLines,
|
| 233 |
+
totalBytes,
|
| 234 |
+
outputLines: outputLinesArr.length,
|
| 235 |
+
outputBytes: finalOutputBytes,
|
| 236 |
+
lastLinePartial,
|
| 237 |
+
firstLineExceedsLimit: false,
|
| 238 |
+
maxLines,
|
| 239 |
+
maxBytes,
|
| 240 |
+
};
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
/**
|
| 244 |
+
* Truncate a string to fit within a byte limit (from the end).
|
| 245 |
+
* Handles multi-byte UTF-8 characters correctly.
|
| 246 |
+
*/
|
| 247 |
+
function truncateStringToBytesFromEnd(str: string, maxBytes: number): string {
|
| 248 |
+
const buf = Buffer.from(str, "utf-8");
|
| 249 |
+
if (buf.length <= maxBytes) {
|
| 250 |
+
return str;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
// Start from the end, skip maxBytes back
|
| 254 |
+
let start = buf.length - maxBytes;
|
| 255 |
+
|
| 256 |
+
// Find a valid UTF-8 boundary (start of a character)
|
| 257 |
+
while (start < buf.length && (buf[start] & 0xc0) === 0x80) {
|
| 258 |
+
start++;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
return buf.slice(start).toString("utf-8");
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/**
|
| 265 |
+
* Truncate a single line to max characters, adding [truncated] suffix.
|
| 266 |
+
* Used for grep match lines.
|
| 267 |
+
*/
|
| 268 |
+
export function truncateLine(
|
| 269 |
+
line: string,
|
| 270 |
+
maxChars: number = GREP_MAX_LINE_LENGTH,
|
| 271 |
+
): { text: string; wasTruncated: boolean } {
|
| 272 |
+
if (line.length <= maxChars) {
|
| 273 |
+
return { text: line, wasTruncated: false };
|
| 274 |
+
}
|
| 275 |
+
return { text: `${line.slice(0, maxChars)}... [truncated]`, wasTruncated: true };
|
| 276 |
+
}
|
packages/coding-agent/src/core/tools/write.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
| 2 |
+
import { mkdir as fsMkdir, writeFile as fsWriteFile } from "fs/promises";
|
| 3 |
+
import { dirname } from "path";
|
| 4 |
+
import { type Static, Type } from "typebox";
|
| 5 |
+
import type { ExtensionContext, ToolDefinition } from "../extensions/types.ts";
|
| 6 |
+
import { withFileMutationQueue } from "./file-mutation-queue.ts";
|
| 7 |
+
import { resolveToCwd } from "./path-utils.ts";
|
| 8 |
+
import { writeRenderers } from "./renderers/write.ts";
|
| 9 |
+
import { wrapToolDefinition } from "./tool-definition-wrapper.ts";
|
| 10 |
+
|
| 11 |
+
const writeSchema = Type.Object({
|
| 12 |
+
path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
|
| 13 |
+
content: Type.String({ description: "Content to write to the file" }),
|
| 14 |
+
});
|
| 15 |
+
|
| 16 |
+
export const writeToolSystemPromptContribution = {
|
| 17 |
+
snippet: "Create or overwrite files",
|
| 18 |
+
guidelines: ["Use write only for new files or complete rewrites."],
|
| 19 |
+
} as const;
|
| 20 |
+
|
| 21 |
+
export type WriteToolInput = Static<typeof writeSchema>;
|
| 22 |
+
|
| 23 |
+
/**
|
| 24 |
+
* Pluggable operations for the write tool.
|
| 25 |
+
* Override these to delegate file writing to remote systems (for example SSH).
|
| 26 |
+
*/
|
| 27 |
+
export interface WriteOperations {
|
| 28 |
+
/** Write content to a file */
|
| 29 |
+
writeFile: (absolutePath: string, content: string) => Promise<void>;
|
| 30 |
+
/** Create directory recursively */
|
| 31 |
+
mkdir: (dir: string) => Promise<void>;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const defaultWriteOperations: WriteOperations = {
|
| 35 |
+
writeFile: (path, content) => fsWriteFile(path, content, "utf-8"),
|
| 36 |
+
mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
export interface WriteToolOptions {
|
| 40 |
+
/** Custom operations for file writing. Default: local filesystem */
|
| 41 |
+
operations?: WriteOperations;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export function createWriteToolDefinition(
|
| 45 |
+
cwd: string,
|
| 46 |
+
options?: WriteToolOptions,
|
| 47 |
+
): ToolDefinition<typeof writeSchema, undefined> {
|
| 48 |
+
const ops = options?.operations ?? defaultWriteOperations;
|
| 49 |
+
return {
|
| 50 |
+
name: "write",
|
| 51 |
+
label: "write",
|
| 52 |
+
description:
|
| 53 |
+
"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
|
| 54 |
+
promptSnippet: writeToolSystemPromptContribution.snippet,
|
| 55 |
+
promptGuidelines: [...writeToolSystemPromptContribution.guidelines],
|
| 56 |
+
parameters: writeSchema,
|
| 57 |
+
constrainedSampling: { type: "json_schema", strict: "prefer" },
|
| 58 |
+
async execute(
|
| 59 |
+
_toolCallId,
|
| 60 |
+
{ path, content }: { path: string; content: string },
|
| 61 |
+
signal?: AbortSignal,
|
| 62 |
+
_onUpdate?,
|
| 63 |
+
ctx?: ExtensionContext,
|
| 64 |
+
) {
|
| 65 |
+
const absolutePath = resolveToCwd(path, ctx?.cwd || cwd);
|
| 66 |
+
const dir = dirname(absolutePath);
|
| 67 |
+
return withFileMutationQueue(absolutePath, async () => {
|
| 68 |
+
// Do not reject from an abort event listener here: that would release the
|
| 69 |
+
// mutation queue while an in-flight filesystem operation may still finish.
|
| 70 |
+
// Checking signal.aborted after each await observes the same aborts while
|
| 71 |
+
// keeping the queue locked until the current operation has settled.
|
| 72 |
+
const throwIfAborted = (): void => {
|
| 73 |
+
if (signal?.aborted) throw new Error("Operation aborted");
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
throwIfAborted();
|
| 77 |
+
// Create parent directories if needed.
|
| 78 |
+
await ops.mkdir(dir);
|
| 79 |
+
throwIfAborted();
|
| 80 |
+
|
| 81 |
+
// Write the file contents.
|
| 82 |
+
await ops.writeFile(absolutePath, content);
|
| 83 |
+
throwIfAborted();
|
| 84 |
+
|
| 85 |
+
return {
|
| 86 |
+
content: [{ type: "text", text: `Successfully wrote to ${path}` }],
|
| 87 |
+
details: undefined,
|
| 88 |
+
};
|
| 89 |
+
});
|
| 90 |
+
},
|
| 91 |
+
...writeRenderers,
|
| 92 |
+
};
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export function createWriteTool(cwd: string, options?: WriteToolOptions): AgentTool<typeof writeSchema> {
|
| 96 |
+
return wrapToolDefinition(createWriteToolDefinition(cwd, options));
|
| 97 |
+
}
|
packages/coding-agent/src/experimental/mini/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mini
|
| 2 |
+
|
| 3 |
+
A minimal coding agent on the durable `AgentHarness`, split across three processes that talk JSON
|
| 4 |
+
over a socket and a pipe. It exists to exercise the harness from a real client and to find out what
|
| 5 |
+
an RPC-shaped presentation actually needs from it.
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
node packages/coding-agent/src/experimental/mini/main.ts [--continue]
|
| 9 |
+
|
| 10 |
+
# while the harness is changing under us, run from source instead of built dist:
|
| 11 |
+
./node_modules/.bin/tsx --tsconfig tsconfig.json packages/coding-agent/src/experimental/mini/main.ts
|
| 12 |
+
```
|
| 13 |
+
|
| 14 |
+
`--continue` attaches to the newest session for the current directory. Starting `mini` twice attaches
|
| 15 |
+
two presentations to the same session; both see the same transcript, live.
|
| 16 |
+
|
| 17 |
+
## Topology
|
| 18 |
+
|
| 19 |
+
```text
|
| 20 |
+
tui tui tui presentations: render, no agent state
|
| 21 |
+
\ | /
|
| 22 |
+
\ | / unix socket, ~/.pi/agent/experimental/mini.sock
|
| 23 |
+
\ | /
|
| 24 |
+
server routes calls, fans out events, spawns workers
|
| 25 |
+
/ \
|
| 26 |
+
/ \ child process stdio pipes
|
| 27 |
+
worker worker one per session: harness, storage, model runtime
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
A presentation holds one connection and reaches every service through it. The server answers
|
| 31 |
+
`sessions` itself and forwards anything else to the worker that connection is attached to, so the TUI
|
| 32 |
+
never learns which host answers what. The same rule runs backwards: a worker calling `sessions.list`
|
| 33 |
+
is answered by the server.
|
| 34 |
+
|
| 35 |
+
The first `mini` to start spawns the server detached. The server kills a worker when its last
|
| 36 |
+
presentation disconnects, and retires itself ten seconds after the last presentation leaves, so the
|
| 37 |
+
next start always runs current code. If that worker held an open durable operation, its replacement
|
| 38 |
+
automatically resumes the operation from the last recorded recovery state.
|
| 39 |
+
|
| 40 |
+
## Layout
|
| 41 |
+
|
| 42 |
+
| path | role |
|
| 43 |
+
| --- | --- |
|
| 44 |
+
| `main.ts` | the `mini` command |
|
| 45 |
+
| `shared/transport.ts` | `Connection`: newline-delimited JSON over any duplex pair |
|
| 46 |
+
| `shared/rpc.ts` | frames, named services, routing, liveness, cancellation |
|
| 47 |
+
| `shared/protocol.ts` | service tokens, contracts, wire types |
|
| 48 |
+
| `server/run.ts` | routes, worker supervision, event fan-out |
|
| 49 |
+
| `worker/run.ts` | opens the session, builds the harness, provides services |
|
| 50 |
+
| `worker/lane-service.ts` | `Lane`: watch subscriptions and lane commands |
|
| 51 |
+
| `worker/models-service.ts` | `Models`: catalog, accounts, interactive login |
|
| 52 |
+
| `tui/run.ts` | presentation host: ensure server, attach, run the view |
|
| 53 |
+
| `tui/session.ts` | attach, subscribe, hold the replicated snapshot |
|
| 54 |
+
| `tui/view.ts` | alt-screen rendering, reusing interactive-mode components |
|
| 55 |
+
|
| 56 |
+
## RPC
|
| 57 |
+
|
| 58 |
+
Two verbs. **Call** asks a question and gets one answer; **emit** publishes to whoever listens. Both
|
| 59 |
+
directions on every connection.
|
| 60 |
+
|
| 61 |
+
```ts
|
| 62 |
+
const peer = createPeer(connection, { forward }); // forward handles names this peer lacks
|
| 63 |
+
peer.provide(Lane, laneService); // register and announce
|
| 64 |
+
const lane = peer.use(Lane); // Remote<LaneServiceApi>
|
| 65 |
+
await lane.prompt("hi"); // -> { kind: "call", id, method: "lane.prompt" }
|
| 66 |
+
peer.on(Lane, (event) => fold(event)); // typed by the token
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
The wire is six frame kinds: `call`, `result`, `error`, `cancel`, `event`, `announce`, `ping`.
|
| 70 |
+
Replies correlate by `id` and resolve one promise; events carry a service name and no id, so the two
|
| 71 |
+
never interfere — a five-minute `lane.prompt` sits pending while its stream of events flows past.
|
| 72 |
+
|
| 73 |
+
Services are plain objects registered under a token, and a token carries the name plus the call and
|
| 74 |
+
event types, so `provide` checks the implementation and `use` checks the caller. Names are announced
|
| 75 |
+
on registration, so routing is a lookup and an unroutable call fails with both inventories named.
|
| 76 |
+
|
| 77 |
+
Failure handling: a closed connection rejects every pending call and aborts every in-flight handler;
|
| 78 |
+
peers ping every five seconds and declare a silent line dead after fifteen, which catches a wedged
|
| 79 |
+
process that never closes; callers may pass a `signal` (sends `cancel`) or a `timeoutMs`. Timeouts
|
| 80 |
+
are opt-in per call site, because an agent turn has no bounded duration.
|
| 81 |
+
|
| 82 |
+
## State replication
|
| 83 |
+
|
| 84 |
+
The worker holds every live object. A presentation holds a `LaneSnapshot` and nothing else.
|
| 85 |
+
|
| 86 |
+
Each presentation gets **its own** `lane.watch()` in the worker, in the harness's two phases:
|
| 87 |
+
`watch()` captures a snapshot while the harness buffers that subscription's events, and `start()`
|
| 88 |
+
drains them. Because the harness pairs snapshot and stream with no gap and no duplicate, the client
|
| 89 |
+
needs no buffering or ordering logic of its own — it applies events with the harness's own
|
| 90 |
+
`reduceLaneSnapshot`. When the fold answers `{ rebase: true }`, which a completed navigation does, the
|
| 91 |
+
presentation takes a fresh subscription and drops the old one. First attach and rebase are the same
|
| 92 |
+
function.
|
| 93 |
+
|
| 94 |
+
Rendering is a pure function of that snapshot. Transcript entries are keyed by id and appended, so a
|
| 95 |
+
streamed token costs one markdown rebuild rather than a repaint; the view repaints wholesale only
|
| 96 |
+
when the entry-id prefix diverges, which is what compaction and navigation do.
|
| 97 |
+
|
| 98 |
+
## What it is not
|
| 99 |
+
|
| 100 |
+
No slash-command system, extensions, skills, hooks, themes beyond the shared one, session picker, or
|
| 101 |
+
tree navigation. Interactive login and model selection exist because they proved the awkward
|
| 102 |
+
directions of the protocol: server-to-client requests, and identity-based configuration.
|
| 103 |
+
|
| 104 |
+
Two known shortcuts: the server broadcasts a worker's events to every attached presentation, so with
|
| 105 |
+
N presentations each event crosses N² times and each client discards the copies that are not its own;
|
| 106 |
+
and cancelling an interactive login only interrupts at a prompt, since the worker's login has no
|
| 107 |
+
abort path yet.
|
packages/coding-agent/src/experimental/mini/main.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* The `mini` command. Runs the presentation host.
|
| 3 |
+
*
|
| 4 |
+
* Run: node packages/coding-agent/src/experimental/mini/main.ts [--continue]
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
import { runTui, type TuiOptions } from "./tui/run.ts";
|
| 8 |
+
|
| 9 |
+
function parseArgs(argv: readonly string[]): TuiOptions {
|
| 10 |
+
const options: TuiOptions = {};
|
| 11 |
+
for (const arg of argv) {
|
| 12 |
+
if (arg === "--continue" || arg === "-c") options.continueSession = true;
|
| 13 |
+
else throw new Error(`Unknown argument: ${arg}`);
|
| 14 |
+
}
|
| 15 |
+
return options;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
runTui(parseArgs(process.argv.slice(2))).then(
|
| 19 |
+
() => process.exit(0),
|
| 20 |
+
(error: unknown) => {
|
| 21 |
+
console.error(error);
|
| 22 |
+
process.exit(1);
|
| 23 |
+
},
|
| 24 |
+
);
|
packages/coding-agent/src/experimental/mini/server/entry.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Server process entry: `node server/entry.ts <socketPath> <sessionsRoot>`. Spawned detached by the CLI. */
|
| 2 |
+
|
| 3 |
+
import { socketTransport } from "../shared/transport.ts";
|
| 4 |
+
import { runServer } from "./run.ts";
|
| 5 |
+
|
| 6 |
+
const [socketPath, sessionsRoot] = process.argv.slice(2);
|
| 7 |
+
if (!socketPath || !sessionsRoot) throw new Error("Server requires <socketPath> <sessionsRoot>");
|
| 8 |
+
|
| 9 |
+
void runServer({ transport: socketTransport(socketPath), sessionsRoot }).catch((error: unknown) => {
|
| 10 |
+
console.error(error);
|
| 11 |
+
process.exit(1);
|
| 12 |
+
});
|
packages/coding-agent/src/experimental/mini/server/run.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Session server: accepts client connections, spawns one worker process per session, routes.
|
| 3 |
+
*
|
| 4 |
+
* It provides `Sessions` and holds no agent state. Any other service name is forwarded to the worker
|
| 5 |
+
* the calling client is attached to, and every worker event is pushed back to that worker's clients.
|
| 6 |
+
* Workers reach `Sessions` over the same peer, because the routing rule is symmetric.
|
| 7 |
+
*/
|
| 8 |
+
|
| 9 |
+
import { spawn } from "node:child_process";
|
| 10 |
+
import { extname } from "node:path";
|
| 11 |
+
import { fileURLToPath } from "node:url";
|
| 12 |
+
// Narrow entries: the server routes and lists sessions, it never runs an agent.
|
| 13 |
+
import { BACKGROUND_CONTEXT } from "@earendil-works/pi-agent-core/harness/context";
|
| 14 |
+
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/harness/env/nodejs";
|
| 15 |
+
import { JsonlSessionRepo } from "@earendil-works/pi-agent-core/harness/session";
|
| 16 |
+
import { type SessionSummary, Sessions, type SessionsServiceApi, Worker } from "../shared/protocol.ts";
|
| 17 |
+
import { createPeer, type RpcPeer } from "../shared/rpc.ts";
|
| 18 |
+
import { childConnection, type Transport } from "../shared/transport.ts";
|
| 19 |
+
|
| 20 |
+
const SELF_EXTENSION = extname(fileURLToPath(import.meta.url));
|
| 21 |
+
const WORKER_ENTRY = fileURLToPath(new URL(`../worker/entry${SELF_EXTENSION}`, import.meta.url));
|
| 22 |
+
const WORKER_START_TIMEOUT_MS = 30_000;
|
| 23 |
+
/** Grace period before an idle server retires, so a reconnecting presentation does not race it. */
|
| 24 |
+
const IDLE_SHUTDOWN_MS = 10_000;
|
| 25 |
+
|
| 26 |
+
/** The server's routing entry for one session worker process. The worker knows none of this. */
|
| 27 |
+
interface Route {
|
| 28 |
+
sessionId: string;
|
| 29 |
+
/** Peer to the worker process. */
|
| 30 |
+
worker: RpcPeer;
|
| 31 |
+
/** Attached presentations by id, so an addressed event goes to exactly one of them. */
|
| 32 |
+
subscribers: Map<string, RpcPeer>;
|
| 33 |
+
stop(): void;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
async function listSessions(sessionsRoot: string): Promise<SessionSummary[]> {
|
| 37 |
+
const executionEnv = new NodeExecutionEnv({ cwd: process.cwd() });
|
| 38 |
+
const repo = new JsonlSessionRepo({ fileSystem: executionEnv, sessionsRoot });
|
| 39 |
+
try {
|
| 40 |
+
return (await repo.list(undefined, BACKGROUND_CONTEXT)).map((metadata) => ({
|
| 41 |
+
id: metadata.id,
|
| 42 |
+
path: metadata.path,
|
| 43 |
+
cwd: metadata.cwd,
|
| 44 |
+
createdAt: metadata.createdAt,
|
| 45 |
+
}));
|
| 46 |
+
} finally {
|
| 47 |
+
await repo.close(BACKGROUND_CONTEXT);
|
| 48 |
+
await executionEnv.cleanup(BACKGROUND_CONTEXT);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export async function runServer(options: { transport: Transport; sessionsRoot: string }): Promise<void> {
|
| 53 |
+
const routes = new Map<string, Route>();
|
| 54 |
+
let presentations = 0;
|
| 55 |
+
let retire = (): void => {};
|
| 56 |
+
const retired = new Promise<void>((resolve) => {
|
| 57 |
+
retire = resolve;
|
| 58 |
+
});
|
| 59 |
+
let idleTimer: NodeJS.Timeout | undefined;
|
| 60 |
+
/** Nothing to serve and nothing running: leave, so the next start always gets current code. */
|
| 61 |
+
const considerRetiring = (): void => {
|
| 62 |
+
if (idleTimer) clearTimeout(idleTimer);
|
| 63 |
+
if (presentations > 0 || routes.size > 0) return;
|
| 64 |
+
idleTimer = setTimeout(() => {
|
| 65 |
+
if (presentations === 0 && routes.size === 0) retire();
|
| 66 |
+
}, IDLE_SHUTDOWN_MS);
|
| 67 |
+
idleTimer.unref();
|
| 68 |
+
};
|
| 69 |
+
/** Concurrent attaches to one session must share a worker: two writers would corrupt the session. */
|
| 70 |
+
const spawning = new Map<string, Promise<Route>>();
|
| 71 |
+
|
| 72 |
+
const spawnWorker = async (sessionId: string | undefined, cwd: string): Promise<Route> => {
|
| 73 |
+
const args = [...process.execArgv, WORKER_ENTRY, options.sessionsRoot, cwd, ...(sessionId ? [sessionId] : [])];
|
| 74 |
+
const child = spawn(process.execPath, args, { stdio: ["pipe", "pipe", "inherit"] });
|
| 75 |
+
const connection = childConnection(child);
|
| 76 |
+
const peer = createPeer(connection);
|
| 77 |
+
// Workers consume `Sessions` through this same peer.
|
| 78 |
+
peer.provide(Sessions, { list, attach: attachUnsupported });
|
| 79 |
+
// A worker that never answers must not wedge the attach that spawned it.
|
| 80 |
+
const described = await peer.use(Worker, { timeoutMs: WORKER_START_TIMEOUT_MS }).describe();
|
| 81 |
+
const route: Route = {
|
| 82 |
+
sessionId: described.sessionId,
|
| 83 |
+
worker: peer,
|
| 84 |
+
subscribers: new Map(),
|
| 85 |
+
stop: () => child.kill(),
|
| 86 |
+
};
|
| 87 |
+
// Routing is the server's job. An addressed event reaches one presentation; the rest are shared.
|
| 88 |
+
peer.onEvent((service, payload, to) => {
|
| 89 |
+
if (to !== undefined) {
|
| 90 |
+
route.subscribers.get(to)?.emitRaw(service, payload);
|
| 91 |
+
return;
|
| 92 |
+
}
|
| 93 |
+
for (const subscriber of route.subscribers.values()) subscriber.emitRaw(service, payload);
|
| 94 |
+
});
|
| 95 |
+
peer.onClose(() => {
|
| 96 |
+
routes.delete(route.sessionId);
|
| 97 |
+
considerRetiring();
|
| 98 |
+
});
|
| 99 |
+
routes.set(route.sessionId, route);
|
| 100 |
+
return route;
|
| 101 |
+
};
|
| 102 |
+
|
| 103 |
+
const ensureRoute = (sessionId: string | null, cwd: string): Promise<Route> => {
|
| 104 |
+
if (sessionId === null) return spawnWorker(undefined, cwd);
|
| 105 |
+
const existing = routes.get(sessionId);
|
| 106 |
+
if (existing) return Promise.resolve(existing);
|
| 107 |
+
const pending = spawning.get(sessionId);
|
| 108 |
+
if (pending) return pending;
|
| 109 |
+
const started = spawnWorker(sessionId, cwd).finally(() => spawning.delete(sessionId));
|
| 110 |
+
spawning.set(sessionId, started);
|
| 111 |
+
return started;
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
const list = (): Promise<SessionSummary[]> => listSessions(options.sessionsRoot);
|
| 115 |
+
const attachUnsupported = async (): Promise<string> => {
|
| 116 |
+
throw new Error("Only presentations attach to sessions");
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const listener = await options.transport.listen((connection) => {
|
| 120 |
+
presentations += 1;
|
| 121 |
+
let route: Route | undefined;
|
| 122 |
+
let attachedAs: string | undefined;
|
| 123 |
+
const sessions: SessionsServiceApi = {
|
| 124 |
+
list,
|
| 125 |
+
attach: async (sessionId, cwd, presentationId) => {
|
| 126 |
+
route?.subscribers.delete(attachedAs ?? "");
|
| 127 |
+
attachedAs = presentationId;
|
| 128 |
+
route = await ensureRoute(sessionId, cwd);
|
| 129 |
+
route.subscribers.set(presentationId, presentation);
|
| 130 |
+
return route.sessionId;
|
| 131 |
+
},
|
| 132 |
+
};
|
| 133 |
+
const presentation: RpcPeer = createPeer(connection, {
|
| 134 |
+
forward: (method, args) => {
|
| 135 |
+
if (!route) throw new Error("Not attached to a session");
|
| 136 |
+
const service = method.slice(0, method.indexOf("."));
|
| 137 |
+
if (!route.worker.announced.has(service)) {
|
| 138 |
+
throw new Error(
|
| 139 |
+
`No host provides ${service}: server has [${[...presentation.provided]}], worker has [${[...route.worker.announced]}]`,
|
| 140 |
+
);
|
| 141 |
+
}
|
| 142 |
+
return route.worker.call(method, ...args);
|
| 143 |
+
},
|
| 144 |
+
});
|
| 145 |
+
presentation.provide(Sessions, sessions);
|
| 146 |
+
connection.onClose(() => {
|
| 147 |
+
presentations -= 1;
|
| 148 |
+
if (route && attachedAs !== undefined) {
|
| 149 |
+
route.subscribers.delete(attachedAs);
|
| 150 |
+
// One worker per session, kept alive only while someone is looking at it.
|
| 151 |
+
if (route.subscribers.size === 0) route.stop();
|
| 152 |
+
}
|
| 153 |
+
considerRetiring();
|
| 154 |
+
});
|
| 155 |
+
});
|
| 156 |
+
|
| 157 |
+
considerRetiring();
|
| 158 |
+
await retired;
|
| 159 |
+
await listener.close();
|
| 160 |
+
}
|
packages/coding-agent/src/experimental/mini/shared/protocol.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** Service contracts and everything that crosses the wire. */
|
| 2 |
+
|
| 3 |
+
import type { HarnessEvent, LaneSnapshot } from "@earendil-works/pi-agent-core";
|
| 4 |
+
import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai";
|
| 5 |
+
|
| 6 |
+
/** Commands answer with data, never exceptions, exactly like a remote call would. */
|
| 7 |
+
export type CommandResult = { ok: true } | { ok: false; error: string };
|
| 8 |
+
|
| 9 |
+
/** Durable model identity. Nothing outside a worker holds a `Model` object. */
|
| 10 |
+
export interface ModelRef {
|
| 11 |
+
provider: string;
|
| 12 |
+
modelId: string;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export interface ModelSummary extends ModelRef {
|
| 16 |
+
name: string;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export interface ProviderAccount {
|
| 20 |
+
id: string;
|
| 21 |
+
name: string;
|
| 22 |
+
authType: "oauth" | "api_key";
|
| 23 |
+
configured: boolean;
|
| 24 |
+
/** Where the credential came from, for display: "stored", "environment", an env var name. */
|
| 25 |
+
source?: string;
|
| 26 |
+
/** False for ambient credentials pi cannot collect itself, such as AWS profiles or env vars. */
|
| 27 |
+
interactive: boolean;
|
| 28 |
+
methodName?: string;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export interface ModelsState {
|
| 32 |
+
readonly models: readonly ModelSummary[];
|
| 33 |
+
readonly accounts: readonly ProviderAccount[];
|
| 34 |
+
readonly refreshing: boolean;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/** An auth prompt without its `AbortSignal`: the part that can cross a transport. */
|
| 38 |
+
export type AuthPromptRequest = AuthPrompt extends infer Prompt
|
| 39 |
+
? Prompt extends unknown
|
| 40 |
+
? Omit<Prompt, "signal">
|
| 41 |
+
: never
|
| 42 |
+
: never;
|
| 43 |
+
|
| 44 |
+
/** A service seen from the other side of a connection: every method returns a promise. */
|
| 45 |
+
export type Remote<T> = {
|
| 46 |
+
[K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
/** Names one service and carries its call and event types. Names are globally unique. */
|
| 50 |
+
export interface ServiceToken<TApi extends object, TEvent = never> {
|
| 51 |
+
readonly name: string;
|
| 52 |
+
/** Phantom, never read: keeps the types attached to the token. */
|
| 53 |
+
readonly types?: (api: TApi, event: TEvent) => void;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
export function defineService<TApi extends object, TEvent = never>(name: string): ServiceToken<TApi, TEvent> {
|
| 57 |
+
return { name };
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export interface SessionSummary {
|
| 61 |
+
id: string;
|
| 62 |
+
path: string;
|
| 63 |
+
cwd: string;
|
| 64 |
+
createdAt: number;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export interface SessionSnapshot {
|
| 68 |
+
sessionId: string;
|
| 69 |
+
cwd: string;
|
| 70 |
+
sessionPath: string;
|
| 71 |
+
/** Carries the lane configuration, queues, and stats: no side-channel replication. */
|
| 72 |
+
lane: LaneSnapshot;
|
| 73 |
+
models: ModelsState;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/** Everything the `Models` service publishes. */
|
| 77 |
+
export type ModelsEvent =
|
| 78 |
+
| { type: "state"; state: ModelsState }
|
| 79 |
+
// Login runs the wrong way round: the request is an event, the answer is an ordinary call.
|
| 80 |
+
| { type: "prompt"; requestId: string; request: AuthPromptRequest }
|
| 81 |
+
| { type: "notice"; notice: AuthEvent };
|
| 82 |
+
|
| 83 |
+
/** The login half, for whatever drives the dialog. */
|
| 84 |
+
export type AuthEventPayload = Exclude<ModelsEvent, { type: "state" }>;
|
| 85 |
+
|
| 86 |
+
/** One presentation's subscription: a `lane.watch()` in the worker, named so its events can be filtered. */
|
| 87 |
+
export interface LaneSubscription {
|
| 88 |
+
subscriptionId: string;
|
| 89 |
+
snapshot: SessionSnapshot;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/** Lane events are addressed to the subscription whose watch produced them. */
|
| 93 |
+
export interface LaneEvent {
|
| 94 |
+
subscriptionId: string;
|
| 95 |
+
event: HarnessEvent;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
export interface LaneServiceApi {
|
| 99 |
+
/**
|
| 100 |
+
* Capture a snapshot and open a subscription for one presentation. Its events are addressed to
|
| 101 |
+
* `presentationId`, so the server routes them instead of broadcasting. Buffered until `start`.
|
| 102 |
+
*/
|
| 103 |
+
watch(presentationId: string): Promise<LaneSubscription>;
|
| 104 |
+
/** Begin delivery, draining everything buffered since the snapshot. */
|
| 105 |
+
start(subscriptionId: string): Promise<void>;
|
| 106 |
+
unwatch(subscriptionId: string): Promise<void>;
|
| 107 |
+
prompt(text: string): Promise<CommandResult>;
|
| 108 |
+
steer(text: string): Promise<CommandResult>;
|
| 109 |
+
followUp(text: string): Promise<CommandResult>;
|
| 110 |
+
compact(): Promise<CommandResult>;
|
| 111 |
+
abort(): Promise<CommandResult>;
|
| 112 |
+
setModel(ref: ModelRef): Promise<CommandResult>;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
export interface ModelsServiceApi {
|
| 116 |
+
refresh(): Promise<CommandResult>;
|
| 117 |
+
login(providerId: string, authType: "oauth" | "api_key"): Promise<CommandResult>;
|
| 118 |
+
authReply(requestId: string, answer: string | null): Promise<void>;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/** Provided by a worker so the server can identify the session it opened, without naming lane methods. */
|
| 122 |
+
export interface WorkerServiceApi {
|
| 123 |
+
describe(): Promise<{ sessionId: string }>;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
export interface SessionsServiceApi {
|
| 127 |
+
list(): Promise<SessionSummary[]>;
|
| 128 |
+
attach(sessionId: string | null, cwd: string, presentationId: string): Promise<string>;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/** Provided by the worker. One subscription per presentation; `watch` again to rebase. */
|
| 132 |
+
export const Lane = defineService<LaneServiceApi, LaneEvent>("lane");
|
| 133 |
+
/** Provided by the worker. Small enough to publish whole. */
|
| 134 |
+
export const Models = defineService<ModelsServiceApi, ModelsEvent>("models");
|
| 135 |
+
/** Provided by the worker, consumed only by the server. */
|
| 136 |
+
export const Worker = defineService<WorkerServiceApi>("worker");
|
| 137 |
+
/** Provided by the server. */
|
| 138 |
+
export const Sessions = defineService<SessionsServiceApi>("sessions");
|