@shanepadgett/tau-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/docs/extending-tau-agent.md +119 -0
- package/docs/subagents.md +57 -0
- package/docs/tui.md +107 -0
- package/extensions/appshot/README.md +11 -0
- package/extensions/appshot/capture.swift +191 -0
- package/extensions/appshot/index.ts +245 -0
- package/extensions/appshot/native-helper.ts +102 -0
- package/extensions/attention/README.md +23 -0
- package/extensions/attention/index.ts +71 -0
- package/extensions/auto-name/README.md +26 -0
- package/extensions/auto-name/index.ts +163 -0
- package/extensions/branch/README.md +22 -0
- package/extensions/branch/index.ts +145 -0
- package/extensions/branch/panel.ts +148 -0
- package/extensions/clear-screen/README.md +29 -0
- package/extensions/clear-screen/index.ts +75 -0
- package/extensions/commit/README.md +15 -0
- package/extensions/commit/commit-plan.ts +328 -0
- package/extensions/commit/git-change-set.ts +268 -0
- package/extensions/commit/index.ts +138 -0
- package/extensions/commit/review-ui.ts +745 -0
- package/extensions/explore/README.md +7 -0
- package/extensions/explore/autoread.ts +140 -0
- package/extensions/explore/find.ts +147 -0
- package/extensions/explore/grep.ts +749 -0
- package/extensions/explore/index.ts +17 -0
- package/extensions/explore/limits.ts +22 -0
- package/extensions/explore/ls.ts +113 -0
- package/extensions/explore/path-display.ts +41 -0
- package/extensions/explore/path-tree.ts +150 -0
- package/extensions/explore/read.ts +160 -0
- package/extensions/explore/result.ts +29 -0
- package/extensions/explore/traverse.ts +226 -0
- package/extensions/footer/README.md +23 -0
- package/extensions/footer/index.ts +534 -0
- package/extensions/footer/settings.ts +13 -0
- package/extensions/ideas/README.md +10 -0
- package/extensions/ideas/browser.ts +66 -0
- package/extensions/ideas/index.ts +31 -0
- package/extensions/ideas/store.ts +32 -0
- package/extensions/image-gen/README.md +11 -0
- package/extensions/image-gen/client.ts +186 -0
- package/extensions/image-gen/index.ts +152 -0
- package/extensions/manage-sessions/README.md +12 -0
- package/extensions/manage-sessions/index.ts +58 -0
- package/extensions/manage-sessions/manager-ui.ts +323 -0
- package/extensions/manage-sessions/sessions.ts +126 -0
- package/extensions/patch/README.md +48 -0
- package/extensions/patch/executor.ts +355 -0
- package/extensions/patch/index.ts +170 -0
- package/extensions/patch/matcher.ts +236 -0
- package/extensions/patch/parser.ts +347 -0
- package/extensions/patch/render.ts +247 -0
- package/extensions/patch/summary.ts +35 -0
- package/extensions/publish/README.md +11 -0
- package/extensions/publish/index.ts +225 -0
- package/extensions/qna/README.md +45 -0
- package/extensions/qna/additional-context-body.ts +51 -0
- package/extensions/qna/body-render.ts +6 -0
- package/extensions/qna/choice-question-body.ts +322 -0
- package/extensions/qna/index.ts +245 -0
- package/extensions/qna/inline-editor-row.ts +56 -0
- package/extensions/qna/input-question-body.ts +89 -0
- package/extensions/qna/model.ts +317 -0
- package/extensions/qna/panel.ts +240 -0
- package/extensions/qna/ui.ts +16 -0
- package/extensions/reference/README.md +47 -0
- package/extensions/reference/index.ts +76 -0
- package/extensions/reference/panel.ts +874 -0
- package/extensions/reference/settings.ts +29 -0
- package/extensions/run-summary/README.md +5 -0
- package/extensions/run-summary/index.ts +98 -0
- package/extensions/silent-command-runner/README.md +25 -0
- package/extensions/silent-command-runner/index.ts +468 -0
- package/extensions/silent-command-runner/settings.ts +65 -0
- package/extensions/soul/README.md +19 -0
- package/extensions/soul/index.ts +20 -0
- package/extensions/soul/prompt.ts +254 -0
- package/extensions/soul/settings.ts +13 -0
- package/extensions/stash/README.md +48 -0
- package/extensions/stash/browser.ts +51 -0
- package/extensions/stash/index.ts +55 -0
- package/extensions/stash/store.ts +36 -0
- package/extensions/subagent/README.md +34 -0
- package/extensions/subagent/agents/scout.md +85 -0
- package/extensions/subagent/agents/web-research.md +95 -0
- package/extensions/subagent/agents.ts +196 -0
- package/extensions/subagent/index.ts +214 -0
- package/extensions/subagent/render.ts +89 -0
- package/extensions/subagent/run.ts +325 -0
- package/extensions/tau/README.md +17 -0
- package/extensions/tau/index.ts +122 -0
- package/extensions/tau-help/README.md +3 -0
- package/extensions/tau-help/help.md +125 -0
- package/extensions/tau-help/index.ts +68 -0
- package/extensions/turn-budget/README.md +14 -0
- package/extensions/turn-budget/index.ts +150 -0
- package/extensions/turn-budget/settings.ts +35 -0
- package/extensions/web/README.md +13 -0
- package/extensions/web/codesearch.ts +79 -0
- package/extensions/web/exa.ts +101 -0
- package/extensions/web/html.ts +67 -0
- package/extensions/web/index.ts +13 -0
- package/extensions/web/limits.ts +11 -0
- package/extensions/web/tool-output.ts +54 -0
- package/extensions/web/webfetch.ts +177 -0
- package/extensions/web/websearch.ts +100 -0
- package/package.json +57 -0
- package/prompts/.gitkeep +1 -0
- package/prompts/cavemanify.md +25 -0
- package/prompts/implement.md +25 -0
- package/prompts/interview.md +48 -0
- package/prompts/plan-feature.md +30 -0
- package/prompts/plan-implementation.md +50 -0
- package/schemas/tau.schema.json +205 -0
- package/shared/agent-blocked.ts +12 -0
- package/shared/description.ts +28 -0
- package/shared/events.ts +135 -0
- package/shared/git.ts +41 -0
- package/shared/injected-context.ts +69 -0
- package/shared/jsonl-store.ts +108 -0
- package/shared/model-fallback/index.ts +278 -0
- package/shared/model-fallback/settings.ts +18 -0
- package/shared/model-fallback/types.ts +8 -0
- package/shared/ranges.ts +21 -0
- package/shared/settings/define.ts +15 -0
- package/shared/settings/files.ts +20 -0
- package/shared/settings/json.ts +41 -0
- package/shared/settings/load.ts +59 -0
- package/shared/settings/merge.ts +21 -0
- package/shared/settings/paths.ts +41 -0
- package/shared/settings/schema.ts +25 -0
- package/shared/settings/specs.ts +23 -0
- package/shared/text.ts +11 -0
- package/shared/tool-row-state.ts +43 -0
- package/skills/writing-preferences/SKILL.md +136 -0
- package/themes/.gitkeep +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
3
|
+
import { registerAutoread } from "./autoread.ts";
|
|
4
|
+
import { createFindTool } from "./find.ts";
|
|
5
|
+
import { createGrepTool } from "./grep.ts";
|
|
6
|
+
import { createLsTool } from "./ls.ts";
|
|
7
|
+
import { createExploreReadTool } from "./read.ts";
|
|
8
|
+
|
|
9
|
+
export default function exploreExtension(pi: ExtensionAPI): void {
|
|
10
|
+
const rowState = createToolRowStateStore(pi, "explore.tool-row-state");
|
|
11
|
+
registerAutoread(pi, rowState);
|
|
12
|
+
pi.registerTool(createLsTool(rowState));
|
|
13
|
+
pi.registerTool(createFindTool(rowState));
|
|
14
|
+
pi.registerTool(createGrepTool(rowState));
|
|
15
|
+
pi.registerTool(createExploreReadTool(rowState));
|
|
16
|
+
pi.on("session_start", () => rowState.clear());
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function normalizeNonNegativeInteger(value: number | undefined, fallback: number): number {
|
|
2
|
+
if (value === undefined) return fallback;
|
|
3
|
+
if (!Number.isFinite(value)) return fallback;
|
|
4
|
+
return Math.max(0, Math.floor(value));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeCountLimit(value: number | undefined, fallback: number): number {
|
|
8
|
+
if (value === undefined) return fallback;
|
|
9
|
+
if (!Number.isFinite(value)) return fallback;
|
|
10
|
+
return Math.max(1, Math.floor(value));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function divideBudget(limit: number, count: number): number[] {
|
|
14
|
+
if (count <= 0) return [];
|
|
15
|
+
const base = Math.max(1, Math.floor(limit / count));
|
|
16
|
+
let remainder = Math.max(0, limit - base * count);
|
|
17
|
+
return Array.from({ length: count }, () => {
|
|
18
|
+
const extra = remainder > 0 ? 1 : 0;
|
|
19
|
+
if (remainder > 0) remainder -= 1;
|
|
20
|
+
return base + extra;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { defineTool, formatSize } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
|
+
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
5
|
+
import { divideBudget, normalizeCountLimit, normalizeNonNegativeInteger } from "./limits.ts";
|
|
6
|
+
import { formatPathForDisplay, pathResolutionError, resolveExplorePath, stripLeadingAt } from "./path-display.ts";
|
|
7
|
+
import { type PathTreeEntry, renderPathTree } from "./path-tree.ts";
|
|
8
|
+
import { createExploreTextResult, type ExploreTextDetails, expandedExploreText } from "./result.ts";
|
|
9
|
+
import { collectPaths, type TraversalEntry } from "./traverse.ts";
|
|
10
|
+
|
|
11
|
+
const lsParams = Type.Object(
|
|
12
|
+
{
|
|
13
|
+
paths: Type.Optional(Type.Array(Type.String())),
|
|
14
|
+
depth: Type.Optional(Type.Number()),
|
|
15
|
+
limit: Type.Optional(Type.Number()),
|
|
16
|
+
all: Type.Optional(Type.Boolean()),
|
|
17
|
+
long: Type.Optional(Type.Boolean()),
|
|
18
|
+
},
|
|
19
|
+
{ additionalProperties: false },
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
type LsParams = Static<typeof lsParams>;
|
|
23
|
+
|
|
24
|
+
function formatTime(date: Date): string {
|
|
25
|
+
return date.toISOString().slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function entryMetadata(entry: TraversalEntry, long: boolean): string | undefined {
|
|
29
|
+
if (!long) return undefined;
|
|
30
|
+
const size = entry.type === "dir" ? "dir" : formatSize(entry.stats.size);
|
|
31
|
+
return `${size} ${formatTime(entry.stats.mtime)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pathTreeEntry(entry: TraversalEntry, long: boolean): PathTreeEntry {
|
|
35
|
+
return {
|
|
36
|
+
displayPath: entry.displayPath,
|
|
37
|
+
type: entry.type,
|
|
38
|
+
metadata: entryMetadata(entry, long),
|
|
39
|
+
empty: entry.empty,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderCallSummary(args: LsParams | undefined): string {
|
|
44
|
+
const paths = args?.paths && args.paths.length > 0 ? args.paths.map(stripLeadingAt).join(" ") : ".";
|
|
45
|
+
const depth = normalizeNonNegativeInteger(args?.depth, 1);
|
|
46
|
+
const limit = normalizeCountLimit(args?.limit, 100);
|
|
47
|
+
const flags = [args?.all ? "all" : "", args?.long ? "long" : ""].filter(Boolean).join(" ");
|
|
48
|
+
return `paths=${paths} depth=${depth} limit=${limit}${flags ? ` ${flags}` : ""}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createLsTool(rowState: ToolRowStateStore) {
|
|
52
|
+
return defineTool<typeof lsParams, ExploreTextDetails | undefined>({
|
|
53
|
+
name: "ls",
|
|
54
|
+
label: "ls",
|
|
55
|
+
description: "List files and directories with compact path output.",
|
|
56
|
+
promptSnippet: "List files and directories",
|
|
57
|
+
parameters: lsParams,
|
|
58
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
59
|
+
const requestedPaths = params.paths && params.paths.length > 0 ? params.paths : ["."];
|
|
60
|
+
const depth = normalizeNonNegativeInteger(params.depth, 1);
|
|
61
|
+
const limit = normalizeCountLimit(params.limit, 100);
|
|
62
|
+
const budgets = divideBudget(limit, requestedPaths.length);
|
|
63
|
+
const all = params.all === true;
|
|
64
|
+
const humanParts: string[] = [];
|
|
65
|
+
const agentParts: string[] = [];
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < requestedPaths.length; i += 1) {
|
|
68
|
+
const input = requestedPaths[i] ?? ".";
|
|
69
|
+
const root = resolveExplorePath(ctx.cwd, input);
|
|
70
|
+
const entries = await collectPaths({
|
|
71
|
+
cwd: ctx.cwd,
|
|
72
|
+
root,
|
|
73
|
+
maxDepth: depth,
|
|
74
|
+
includeRoot: true,
|
|
75
|
+
includeHidden: all,
|
|
76
|
+
includeIgnored: all,
|
|
77
|
+
includeNoise: all,
|
|
78
|
+
}).catch((error: unknown) => {
|
|
79
|
+
throw pathResolutionError(error, input);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const budget = budgets[i] ?? limit;
|
|
83
|
+
const shown = entries.slice(0, budget);
|
|
84
|
+
const omitted = entries.length - shown.length;
|
|
85
|
+
const notice = omitted > 0 ? `… omitted ${omitted} entries (limit ${budget})` : undefined;
|
|
86
|
+
const rootPath = formatPathForDisplay(root, ctx.cwd);
|
|
87
|
+
const tree = renderPathTree(
|
|
88
|
+
shown.map((entry) => pathTreeEntry(entry, params.long === true)),
|
|
89
|
+
{
|
|
90
|
+
rootPath,
|
|
91
|
+
omissionNotice: notice,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
humanParts.push(tree.humanText);
|
|
95
|
+
agentParts.push(tree.agentText);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return createExploreTextResult(agentParts.join("\n\n"), humanParts.join("\n\n"));
|
|
99
|
+
},
|
|
100
|
+
renderCall(args, theme, context) {
|
|
101
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
102
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
103
|
+
const title = formatToolRowTitle(rowState, context.toolCallId, "ls", theme);
|
|
104
|
+
text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
|
|
105
|
+
return text;
|
|
106
|
+
},
|
|
107
|
+
renderResult(result, _options, _theme, context) {
|
|
108
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
109
|
+
text.setText(context.expanded ? expandedExploreText(result) : "");
|
|
110
|
+
return text;
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
|
|
3
|
+
export function stripLeadingAt(value: string): string {
|
|
4
|
+
return value.startsWith("@") ? value.slice(1) : value;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function toSlashPath(value: string): string {
|
|
8
|
+
return value.replace(/\\/g, "/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolveExplorePath(cwd: string, input: string): string {
|
|
12
|
+
const path = stripLeadingAt(input);
|
|
13
|
+
if (path.length === 0) return resolve(cwd);
|
|
14
|
+
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function pathResolutionError(error: unknown, input: string): Error {
|
|
18
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
19
|
+
return new Error(`Path not found: ${stripLeadingAt(input)}`);
|
|
20
|
+
}
|
|
21
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isWithinPath(parent: string, child: string): boolean {
|
|
25
|
+
const rel = relative(resolve(parent), resolve(child));
|
|
26
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatPathForDisplay(absolutePath: string, cwd: string): string {
|
|
30
|
+
const resolvedCwd = resolve(cwd);
|
|
31
|
+
const resolvedPath = resolve(absolutePath);
|
|
32
|
+
const rel = relative(resolvedCwd, resolvedPath);
|
|
33
|
+
if (rel === "") return ".";
|
|
34
|
+
if (!rel.startsWith("..") && !isAbsolute(rel)) return toSlashPath(rel);
|
|
35
|
+
return toSlashPath(resolvedPath);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function relativeSlash(from: string, to: string): string {
|
|
39
|
+
const rel = relative(resolve(from), resolve(to));
|
|
40
|
+
return rel === "" ? "." : toSlashPath(rel);
|
|
41
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
export type PathTreeEntryType = "file" | "dir";
|
|
2
|
+
|
|
3
|
+
export interface PathTreeEntry {
|
|
4
|
+
displayPath: string;
|
|
5
|
+
type: PathTreeEntryType;
|
|
6
|
+
metadata?: string;
|
|
7
|
+
empty?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PathTreeRenderOptions {
|
|
11
|
+
rootPath?: string;
|
|
12
|
+
omissionNotice?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PathTreeRender {
|
|
16
|
+
humanText: string;
|
|
17
|
+
agentText: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TreeNode {
|
|
21
|
+
name: string;
|
|
22
|
+
type: PathTreeEntryType;
|
|
23
|
+
metadata?: string;
|
|
24
|
+
empty?: boolean;
|
|
25
|
+
children: Map<string, TreeNode>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function splitDisplayPath(displayPath: string): string[] {
|
|
29
|
+
if (displayPath === ".") return [];
|
|
30
|
+
if (displayPath === "/") return ["/"];
|
|
31
|
+
if (displayPath.startsWith("/")) return ["/", ...displayPath.slice(1).split("/").filter(Boolean)];
|
|
32
|
+
return displayPath.split("/").filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isUnderRoot(rootPath: string, displayPath: string): boolean {
|
|
36
|
+
if (rootPath === "/") return displayPath.startsWith("/");
|
|
37
|
+
if (rootPath === ".") return !displayPath.startsWith("/");
|
|
38
|
+
return displayPath === rootPath || displayPath.startsWith(`${rootPath}/`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function relativeToRoot(rootPath: string, displayPath: string): string {
|
|
42
|
+
if (rootPath === "/") return displayPath === "/" ? "" : displayPath.slice(1);
|
|
43
|
+
if (rootPath === ".") return displayPath === "." ? "" : displayPath;
|
|
44
|
+
if (displayPath === rootPath) return "";
|
|
45
|
+
return displayPath.slice(rootPath.length + 1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function entryLabel(node: TreeNode): string {
|
|
49
|
+
const name = node.name === "/" ? "/" : node.type === "dir" ? `${node.name}/` : node.name;
|
|
50
|
+
return node.metadata ? `${name} ${node.metadata}` : name;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sortChildren(node: TreeNode): TreeNode[] {
|
|
54
|
+
return [...node.children.values()].sort((a, b) => {
|
|
55
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
56
|
+
return a.name.localeCompare(b.name);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createNode(name: string, type: PathTreeEntryType): TreeNode {
|
|
61
|
+
return { name, type, children: new Map() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function insert(root: TreeNode, relativePath: string, entry: PathTreeEntry): void {
|
|
65
|
+
const parts = splitDisplayPath(relativePath);
|
|
66
|
+
if (parts.length === 0) {
|
|
67
|
+
root.type = entry.type;
|
|
68
|
+
root.metadata = entry.metadata;
|
|
69
|
+
root.empty = entry.empty;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let current = root;
|
|
74
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
75
|
+
const part = parts[i];
|
|
76
|
+
if (!part) continue;
|
|
77
|
+
const isLeaf = i === parts.length - 1;
|
|
78
|
+
const type = isLeaf ? entry.type : "dir";
|
|
79
|
+
let child = current.children.get(part);
|
|
80
|
+
if (!child) {
|
|
81
|
+
child = createNode(part, type);
|
|
82
|
+
current.children.set(part, child);
|
|
83
|
+
}
|
|
84
|
+
if (isLeaf) {
|
|
85
|
+
child.type = entry.type;
|
|
86
|
+
child.metadata = entry.metadata;
|
|
87
|
+
child.empty = entry.empty;
|
|
88
|
+
}
|
|
89
|
+
current = child;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function renderHumanNode(node: TreeNode, depth: number, lines: string[]): void {
|
|
94
|
+
lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
|
|
95
|
+
if (node.empty && node.children.size === 0) lines.push(`${" ".repeat(depth + 1)}[empty]`);
|
|
96
|
+
for (const child of sortChildren(node)) renderHumanNode(child, depth + 1, lines);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function renderAgentNode(node: TreeNode, depth: number, lines: string[]): void {
|
|
100
|
+
if (node.type === "file") {
|
|
101
|
+
lines.push(`${" ".repeat(depth)}${entryLabel(node)}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const children = sortChildren(node);
|
|
106
|
+
const files = children.filter((child) => child.type === "file").map(entryLabel);
|
|
107
|
+
let line = `${" ".repeat(depth)}${entryLabel(node)}`;
|
|
108
|
+
if (files.length > 0) line += `: ${files.join(", ")}`;
|
|
109
|
+
else if (node.empty && children.length === 0) line += " [empty]";
|
|
110
|
+
lines.push(line);
|
|
111
|
+
|
|
112
|
+
for (const child of children) {
|
|
113
|
+
if (child.type === "dir") renderAgentNode(child, depth + 1, lines);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildTree(entries: readonly PathTreeEntry[], rootPath: string | undefined): TreeNode {
|
|
118
|
+
const rootName = rootPath === undefined ? "" : rootPath === "." ? "." : rootPath;
|
|
119
|
+
const root = createNode(rootName, "dir");
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (rootPath !== undefined && isUnderRoot(rootPath, entry.displayPath)) {
|
|
122
|
+
insert(root, relativeToRoot(rootPath, entry.displayPath), entry);
|
|
123
|
+
} else {
|
|
124
|
+
insert(root, entry.displayPath, entry);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return root;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendNotice(lines: string[], notice: string | undefined): void {
|
|
131
|
+
if (notice) lines.push(notice);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderPathTree(entries: readonly PathTreeEntry[], options: PathTreeRenderOptions = {}): PathTreeRender {
|
|
135
|
+
const root = buildTree(entries, options.rootPath);
|
|
136
|
+
const humanLines: string[] = [];
|
|
137
|
+
const agentLines: string[] = [];
|
|
138
|
+
|
|
139
|
+
if (options.rootPath === undefined) {
|
|
140
|
+
for (const child of sortChildren(root)) renderHumanNode(child, 0, humanLines);
|
|
141
|
+
for (const child of sortChildren(root)) renderAgentNode(child, 0, agentLines);
|
|
142
|
+
} else {
|
|
143
|
+
renderHumanNode(root, 0, humanLines);
|
|
144
|
+
renderAgentNode(root, 0, agentLines);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
appendNotice(humanLines, options.omissionNotice);
|
|
148
|
+
appendNotice(agentLines, options.omissionNotice);
|
|
149
|
+
return { humanText: humanLines.join("\n"), agentText: agentLines.join("\n") };
|
|
150
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createReadToolDefinition,
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
formatSize,
|
|
5
|
+
truncateHead,
|
|
6
|
+
type ReadToolDetails,
|
|
7
|
+
type ToolDefinition,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { isAbsolute, resolve } from "node:path";
|
|
12
|
+
import { Type, type Static } from "typebox";
|
|
13
|
+
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
14
|
+
import { normalizeCountLimit } from "./limits.ts";
|
|
15
|
+
import { stripLeadingAt } from "./path-display.ts";
|
|
16
|
+
|
|
17
|
+
const readSchema = Type.Object({
|
|
18
|
+
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
19
|
+
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
20
|
+
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
21
|
+
lineNumbers: Type.Optional(Type.Boolean({ description: "Prefix text lines with their 1-indexed file line number" })),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
type ReadToolInput = Static<typeof readSchema>;
|
|
25
|
+
type BaseReadDefinition = ReturnType<typeof createReadToolDefinition>;
|
|
26
|
+
type ReadDefinition = ToolDefinition<typeof readSchema, ReadToolDetails | undefined>;
|
|
27
|
+
type ReadExecute = ReadDefinition["execute"];
|
|
28
|
+
type ReadRenderCall = NonNullable<ReadDefinition["renderCall"]>;
|
|
29
|
+
type ReadRenderResult = NonNullable<ReadDefinition["renderResult"]>;
|
|
30
|
+
|
|
31
|
+
const readDefinitionByCwd = new Map<string, BaseReadDefinition>();
|
|
32
|
+
|
|
33
|
+
function readDefinitionForCwd(cwd: string): BaseReadDefinition {
|
|
34
|
+
const existing = readDefinitionByCwd.get(cwd);
|
|
35
|
+
if (existing) return existing;
|
|
36
|
+
const definition = createReadToolDefinition(cwd);
|
|
37
|
+
readDefinitionByCwd.set(cwd, definition);
|
|
38
|
+
return definition;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeReadParams(params: ReadToolInput): ReadToolInput {
|
|
42
|
+
return {
|
|
43
|
+
...params,
|
|
44
|
+
path: stripLeadingAt(params.path),
|
|
45
|
+
limit: params.limit === undefined ? undefined : normalizeCountLimit(params.limit, 1),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isSupportedImage(buffer: Buffer): boolean {
|
|
50
|
+
return (
|
|
51
|
+
(buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) ||
|
|
52
|
+
buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) ||
|
|
53
|
+
buffer.subarray(0, 6).toString("ascii") === "GIF87a" ||
|
|
54
|
+
buffer.subarray(0, 6).toString("ascii") === "GIF89a" ||
|
|
55
|
+
(buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") ||
|
|
56
|
+
buffer.subarray(0, 2).toString("ascii") === "BM"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function renderCallSummary(args: ReadToolInput | undefined): string {
|
|
61
|
+
if (!args) return "";
|
|
62
|
+
const path = stripLeadingAt(args.path);
|
|
63
|
+
if (args.offset === undefined && args.limit === undefined) return path;
|
|
64
|
+
const start = args.offset ?? 1;
|
|
65
|
+
const limit = args.limit === undefined ? undefined : normalizeCountLimit(args.limit, 1);
|
|
66
|
+
const end = limit === undefined ? "" : start + limit - 1;
|
|
67
|
+
return `${path}:${start}${end === "" ? "" : `-${end}`}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createExploreReadTool(rowState: ToolRowStateStore): ReadDefinition {
|
|
71
|
+
const baseDefinition = readDefinitionForCwd(process.cwd());
|
|
72
|
+
return {
|
|
73
|
+
...baseDefinition,
|
|
74
|
+
parameters: readSchema,
|
|
75
|
+
async execute(
|
|
76
|
+
toolCallId: Parameters<ReadExecute>[0],
|
|
77
|
+
params: Parameters<ReadExecute>[1],
|
|
78
|
+
signal: Parameters<ReadExecute>[2],
|
|
79
|
+
onUpdate: Parameters<ReadExecute>[3],
|
|
80
|
+
ctx: Parameters<ReadExecute>[4],
|
|
81
|
+
) {
|
|
82
|
+
const definition = readDefinitionForCwd(ctx.cwd);
|
|
83
|
+
const normalized = normalizeReadParams(params);
|
|
84
|
+
const path = isAbsolute(normalized.path) ? normalized.path : resolve(ctx.cwd, normalized.path);
|
|
85
|
+
const buffer = await readFile(path);
|
|
86
|
+
if (isSupportedImage(buffer)) {
|
|
87
|
+
return definition.execute(
|
|
88
|
+
toolCallId,
|
|
89
|
+
{ path: normalized.path, offset: normalized.offset, limit: normalized.limit },
|
|
90
|
+
signal,
|
|
91
|
+
onUpdate,
|
|
92
|
+
ctx,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (signal?.aborted) throw new Error("Operation aborted");
|
|
97
|
+
const allLines = buffer.toString("utf-8").split("\n");
|
|
98
|
+
const startLine = normalized.offset ? Math.max(0, normalized.offset - 1) : 0;
|
|
99
|
+
const startLineDisplay = startLine + 1;
|
|
100
|
+
if (startLine >= allLines.length) {
|
|
101
|
+
throw new Error(`Offset ${normalized.offset} is beyond end of file (${allLines.length} lines total)`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const endLine =
|
|
105
|
+
normalized.limit === undefined ? allLines.length : Math.min(startLine + normalized.limit, allLines.length);
|
|
106
|
+
const selectedLines = allLines.slice(startLine, endLine);
|
|
107
|
+
const selectedContent = selectedLines
|
|
108
|
+
.map((line, index) => (normalized.lineNumbers ? `${startLineDisplay + index}: ${line}` : line))
|
|
109
|
+
.join("\n");
|
|
110
|
+
const truncation = truncateHead(selectedContent);
|
|
111
|
+
let outputText: string;
|
|
112
|
+
let details: ReadToolDetails | undefined;
|
|
113
|
+
if (truncation.firstLineExceedsLimit) {
|
|
114
|
+
const firstLineSize = formatSize(Buffer.byteLength(selectedLines[0] ?? "", "utf-8"));
|
|
115
|
+
outputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${normalized.path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
116
|
+
details = { truncation };
|
|
117
|
+
} else if (truncation.truncated) {
|
|
118
|
+
const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
|
|
119
|
+
const nextOffset = endLineDisplay + 1;
|
|
120
|
+
outputText = truncation.content;
|
|
121
|
+
outputText +=
|
|
122
|
+
truncation.truncatedBy === "lines"
|
|
123
|
+
? `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${allLines.length}. Use offset=${nextOffset} to continue.]`
|
|
124
|
+
: `\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${allLines.length} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
|
125
|
+
details = { truncation };
|
|
126
|
+
} else if (endLine < allLines.length) {
|
|
127
|
+
outputText = `${truncation.content}\n\n[${allLines.length - endLine} more lines in file. Use offset=${endLine + 1} to continue.]`;
|
|
128
|
+
} else {
|
|
129
|
+
outputText = truncation.content;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { content: [{ type: "text", text: outputText }], details };
|
|
133
|
+
},
|
|
134
|
+
renderCall(
|
|
135
|
+
args: Parameters<ReadRenderCall>[0],
|
|
136
|
+
theme: Parameters<ReadRenderCall>[1],
|
|
137
|
+
context: Parameters<ReadRenderCall>[2],
|
|
138
|
+
) {
|
|
139
|
+
rowState.watch(context.toolCallId, context.invalidate);
|
|
140
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
141
|
+
const title = formatToolRowTitle(rowState, context.toolCallId, "read", theme);
|
|
142
|
+
text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
|
|
143
|
+
return text;
|
|
144
|
+
},
|
|
145
|
+
renderResult(
|
|
146
|
+
result: Parameters<ReadRenderResult>[0],
|
|
147
|
+
options: Parameters<ReadRenderResult>[1],
|
|
148
|
+
theme: Parameters<ReadRenderResult>[2],
|
|
149
|
+
context: Parameters<ReadRenderResult>[3],
|
|
150
|
+
) {
|
|
151
|
+
if (!context.expanded) {
|
|
152
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
153
|
+
text.setText("");
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
const definition = readDefinitionForCwd(context.cwd);
|
|
157
|
+
return definition.renderResult?.(result, { ...options, expanded: true }, theme, context) ?? new Text("", 0, 0);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface ExploreTextDetails {
|
|
4
|
+
humanText?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function createExploreTextResult(
|
|
8
|
+
agentText: string,
|
|
9
|
+
humanText?: string,
|
|
10
|
+
): AgentToolResult<ExploreTextDetails | undefined> {
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: "text", text: agentText }],
|
|
13
|
+
details: humanText !== undefined && humanText !== agentText ? { humanText } : undefined,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function firstTextContent(result: { content: readonly { type: string; text?: string }[] }): string {
|
|
18
|
+
for (const item of result.content) {
|
|
19
|
+
if (item.type === "text" && typeof item.text === "string") return item.text;
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function expandedExploreText(result: {
|
|
25
|
+
content: readonly { type: string; text?: string }[];
|
|
26
|
+
details?: ExploreTextDetails;
|
|
27
|
+
}): string {
|
|
28
|
+
return result.details?.humanText ?? firstTextContent(result);
|
|
29
|
+
}
|