@shanepadgett/tau-agent 0.26.0 → 0.28.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/docs/subagents.md +1 -1
- package/extensions/appshot/README.md +1 -1
- package/extensions/appshot/index.ts +5 -11
- package/extensions/context/sync.ts +1 -9
- package/extensions/context-pruning/README.md +2 -2
- package/extensions/context-pruning/index.ts +58 -57
- package/extensions/context-pruning/prune.ts +2 -2
- package/extensions/context-pruning/render.ts +51 -39
- package/extensions/context-pruning/settings.ts +4 -5
- package/extensions/explore/README.md +13 -14
- package/extensions/explore/ast/adapter.ts +97 -0
- package/extensions/explore/ast/engine.ts +470 -0
- package/extensions/explore/ast/format/ast-search.ts +97 -0
- package/extensions/explore/ast/format/composite.ts +26 -0
- package/extensions/explore/ast/format/context.ts +66 -0
- package/extensions/explore/ast/format/deps.ts +86 -0
- package/extensions/explore/ast/format/discover.ts +85 -0
- package/extensions/explore/ast/format/impact.ts +105 -0
- package/extensions/explore/ast/format/outline.ts +66 -0
- package/extensions/explore/ast/format/relationships.ts +81 -0
- package/extensions/explore/ast/format/show.ts +38 -0
- package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
- package/extensions/explore/ast/grammars/manifest.json +41 -0
- package/extensions/explore/ast/grammars/manifest.ts +99 -0
- package/extensions/explore/ast/grammars/odin.wasm +0 -0
- package/extensions/explore/ast/grammars/swift.wasm +0 -0
- package/extensions/explore/ast/graph/file-graph.ts +360 -0
- package/extensions/explore/ast/graph/relationships.ts +597 -0
- package/extensions/explore/ast/identity.ts +119 -0
- package/extensions/explore/ast/ir.ts +130 -0
- package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
- package/extensions/explore/ast/languages/csharp.ts +493 -0
- package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
- package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
- package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
- package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
- package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
- package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
- package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
- package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
- package/extensions/explore/ast/languages/go.ts +545 -0
- package/extensions/explore/ast/languages/java.ts +412 -0
- package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
- package/extensions/explore/ast/languages/kotlin.ts +611 -0
- package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
- package/extensions/explore/ast/languages/odin.ts +407 -0
- package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
- package/extensions/explore/ast/languages/rust.ts +506 -0
- package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
- package/extensions/explore/ast/languages/swift.ts +466 -0
- package/extensions/explore/ast/languages/tree.ts +220 -0
- package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
- package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
- package/extensions/explore/ast/languages/typescript.ts +716 -0
- package/extensions/explore/ast/markdown.ts +145 -0
- package/extensions/explore/ast/package-surface.ts +42 -0
- package/extensions/explore/ast/queries/ast-search.ts +306 -0
- package/extensions/explore/ast/queries/composite-target.ts +77 -0
- package/extensions/explore/ast/queries/context.ts +406 -0
- package/extensions/explore/ast/queries/discover.ts +427 -0
- package/extensions/explore/ast/queries/impact.ts +250 -0
- package/extensions/explore/ast/queries/outline.ts +203 -0
- package/extensions/explore/ast/queries/show.ts +262 -0
- package/extensions/explore/ast/query.ts +48 -0
- package/extensions/explore/ast/read/hook.ts +125 -0
- package/extensions/explore/ast/read/policy.ts +55 -0
- package/extensions/explore/ast/registry.ts +120 -0
- package/extensions/explore/ast/scan.ts +76 -0
- package/extensions/explore/ast/slice.ts +31 -0
- package/extensions/explore/ast/tools/ast-search.ts +138 -0
- package/extensions/explore/ast/tools/context.ts +126 -0
- package/extensions/explore/ast/tools/deps.ts +72 -0
- package/extensions/explore/ast/tools/discover.ts +208 -0
- package/extensions/explore/ast/tools/impact.ts +135 -0
- package/extensions/explore/ast/tools/outline.ts +200 -0
- package/extensions/explore/ast/tools/relationships.ts +160 -0
- package/extensions/explore/ast/tools/render.ts +121 -0
- package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
- package/extensions/explore/ast/tools/show.ts +116 -0
- package/extensions/explore/guidance.ts +52 -0
- package/extensions/explore/index.ts +91 -45
- package/extensions/explore/read/autoread.ts +118 -0
- package/extensions/explore/settings.ts +64 -0
- package/extensions/explore/traverse.ts +260 -86
- package/extensions/handoff/index.ts +1 -1
- package/extensions/patch/executor.ts +95 -1
- package/extensions/patch/index.ts +1 -0
- package/extensions/publish/index.ts +18 -85
- package/extensions/publish/publish-ui.ts +0 -4
- package/extensions/soul/README.md +7 -4
- package/extensions/soul/index.ts +11 -5
- package/extensions/soul/prompt.ts +30 -14
- package/extensions/soul/settings.ts +7 -2
- package/extensions/subagent/agents/context-sync.md +6 -10
- package/extensions/subagent/agents/review.md +44 -24
- package/extensions/subagent/agents/scout.md +53 -34
- package/extensions/subagent/run.ts +1 -1
- package/extensions/tau-help/help.md +2 -2
- package/package.json +6 -5
- package/schemas/tau.schema.json +51 -6
- package/{extensions/explore → shared}/autoread.ts +21 -7
- package/shared/bounded-text-result.ts +304 -0
- package/shared/events.ts +1 -0
- package/shared/full-file-knowledge.ts +49 -0
- package/shared/temporary-output-store.ts +275 -0
- package/extensions/explore/ast-tools.ts +0 -828
- package/extensions/explore/ast-worker.ts +0 -356
- package/extensions/explore/find.ts +0 -147
- package/extensions/explore/full-file-knowledge.ts +0 -234
- package/extensions/explore/grep.ts +0 -749
- package/extensions/explore/limits.ts +0 -22
- package/extensions/explore/ls.ts +0 -113
- package/extensions/explore/path-display.ts +0 -41
- package/extensions/explore/path-tree.ts +0 -150
- package/extensions/explore/read-cache.ts +0 -273
- package/extensions/explore/read-snapshots.ts +0 -57
- package/extensions/explore/read-stats-panel.ts +0 -154
- package/extensions/explore/read-stats.ts +0 -120
- package/extensions/explore/read.ts +0 -335
- package/extensions/explore/result.ts +0 -29
- package/native-bin/darwin-arm64/tau-ast +0 -0
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type Component, type KeybindingsManager, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import { bindingHint, Tabs, ToolPanel, type ToolPanelConfig } from "@shanepadgett/tau-tui";
|
|
4
|
-
import type { ReadCacheMode } from "./read-cache.ts";
|
|
5
|
-
|
|
6
|
-
export interface ReadSavingsSnapshot {
|
|
7
|
-
label: string;
|
|
8
|
-
secondary: string;
|
|
9
|
-
baselineTokens: number;
|
|
10
|
-
returnedTokens: number;
|
|
11
|
-
costSaved: number;
|
|
12
|
-
unchangedCost: number;
|
|
13
|
-
diffCost: number;
|
|
14
|
-
counts: Record<ReadCacheMode, number>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function createReadStatsPanel(
|
|
18
|
-
tui: TUI,
|
|
19
|
-
theme: Theme,
|
|
20
|
-
keybindings: KeybindingsManager,
|
|
21
|
-
done: (result: unknown) => void,
|
|
22
|
-
current: ReadSavingsSnapshot,
|
|
23
|
-
whole: ReadSavingsSnapshot,
|
|
24
|
-
): Component {
|
|
25
|
-
return new ReadStatsPanel(tui, theme, keybindings, done, current, whole);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
class ReadStatsPanel implements Component {
|
|
29
|
-
private readonly tui: TUI;
|
|
30
|
-
private readonly keybindings: KeybindingsManager;
|
|
31
|
-
private readonly done: (result: unknown) => void;
|
|
32
|
-
private readonly snapshots: readonly [ReadSavingsSnapshot, ReadSavingsSnapshot];
|
|
33
|
-
private readonly tabs: Tabs;
|
|
34
|
-
private readonly config: ToolPanelConfig;
|
|
35
|
-
private readonly panel: ToolPanel;
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
38
|
-
tui: TUI,
|
|
39
|
-
theme: Theme,
|
|
40
|
-
keybindings: KeybindingsManager,
|
|
41
|
-
done: (result: unknown) => void,
|
|
42
|
-
current: ReadSavingsSnapshot,
|
|
43
|
-
whole: ReadSavingsSnapshot,
|
|
44
|
-
) {
|
|
45
|
-
this.tui = tui;
|
|
46
|
-
this.keybindings = keybindings;
|
|
47
|
-
this.done = done;
|
|
48
|
-
this.snapshots = [current, whole];
|
|
49
|
-
this.tabs = new Tabs(
|
|
50
|
-
theme,
|
|
51
|
-
this.snapshots.map((snapshot) => ({
|
|
52
|
-
id: snapshot.label,
|
|
53
|
-
label: snapshot.label,
|
|
54
|
-
body: new SavingsBody(theme, snapshot),
|
|
55
|
-
})),
|
|
56
|
-
current.label,
|
|
57
|
-
);
|
|
58
|
-
this.config = {
|
|
59
|
-
title: "Read savings",
|
|
60
|
-
secondary: current.secondary,
|
|
61
|
-
body: this.tabs,
|
|
62
|
-
footer: {
|
|
63
|
-
kind: "hints",
|
|
64
|
-
hints: [...this.tabs.getKeyHints(), bindingHint("tui.select.cancel", "close")],
|
|
65
|
-
},
|
|
66
|
-
border: "box",
|
|
67
|
-
};
|
|
68
|
-
this.panel = new ToolPanel(theme, this.config);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
handleInput(data: string): void {
|
|
72
|
-
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
73
|
-
this.done(undefined);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
this.tabs.handleInput(data);
|
|
77
|
-
const snapshot = this.snapshots.find((item) => item.label === this.tabs.getActiveId()) ?? this.snapshots[0];
|
|
78
|
-
this.config.secondary = snapshot.secondary;
|
|
79
|
-
this.tui.requestRender();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
render(width: number): string[] {
|
|
83
|
-
return this.panel.render(width);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
invalidate(): void {
|
|
87
|
-
this.panel.invalidate();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
class SavingsBody implements Component {
|
|
92
|
-
private readonly theme: Theme;
|
|
93
|
-
private readonly snapshot: ReadSavingsSnapshot;
|
|
94
|
-
|
|
95
|
-
constructor(theme: Theme, snapshot: ReadSavingsSnapshot) {
|
|
96
|
-
this.theme = theme;
|
|
97
|
-
this.snapshot = snapshot;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
render(width: number): string[] {
|
|
101
|
-
const avoided = Math.max(0, this.snapshot.baselineTokens - this.snapshot.returnedTokens);
|
|
102
|
-
const reduction = this.snapshot.baselineTokens > 0 ? avoided / this.snapshot.baselineTokens : 0;
|
|
103
|
-
return [
|
|
104
|
-
this.theme.bold("Tokens"),
|
|
105
|
-
metricRow("Without cache", formatTokens(this.snapshot.baselineTokens), width),
|
|
106
|
-
metricRow("With cache", formatTokens(this.snapshot.returnedTokens), width),
|
|
107
|
-
metricRow(
|
|
108
|
-
"Saved",
|
|
109
|
-
this.theme.fg("success", `${formatTokens(avoided)} tokens (${Math.round(reduction * 100)}%)`),
|
|
110
|
-
width,
|
|
111
|
-
),
|
|
112
|
-
"",
|
|
113
|
-
this.theme.bold("Cost saved"),
|
|
114
|
-
metricRow("Total", this.theme.fg("success", this.theme.bold(formatCost(this.snapshot.costSaved))), width),
|
|
115
|
-
metricRow("Unchanged", formatCost(this.snapshot.unchangedCost), width),
|
|
116
|
-
metricRow("Changes only", formatCost(this.snapshot.diffCost), width),
|
|
117
|
-
"",
|
|
118
|
-
this.theme.bold("Read results"),
|
|
119
|
-
countRow("Baseline", this.snapshot.counts.baseline, "Unchanged", this.snapshot.counts.unchanged, width),
|
|
120
|
-
countRow("Changes", this.snapshot.counts.diff, "Recovery", this.snapshot.counts.recovery, width),
|
|
121
|
-
"",
|
|
122
|
-
truncateToWidth(
|
|
123
|
-
this.theme.fg("dim", "Estimates include repeated chat history and prompt caching."),
|
|
124
|
-
width,
|
|
125
|
-
"",
|
|
126
|
-
),
|
|
127
|
-
];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
invalidate(): void {}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function formatTokens(value: number): string {
|
|
134
|
-
if (value >= 1_000_000) return `~${(value / 1_000_000).toFixed(2)}M`;
|
|
135
|
-
if (value >= 1_000) return `~${Math.round(value / 1_000)}k`;
|
|
136
|
-
return `~${Math.max(0, Math.round(value))}`;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function formatCost(value: number): string {
|
|
140
|
-
if (value > 0 && value < 0.01) return "<$0.01";
|
|
141
|
-
return `~$${Math.max(0, value).toFixed(2)}`;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function metricRow(label: string, value: string, width: number): string {
|
|
145
|
-
return truncateToWidth(`${label.padEnd(16)}${value}`, width, "");
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function countRow(left: string, leftCount: number, right: string, rightCount: number, width: number): string {
|
|
149
|
-
return truncateToWidth(
|
|
150
|
-
`${left.padEnd(11)}${String(leftCount).padEnd(6)}${right.padEnd(11)}${rightCount}`,
|
|
151
|
-
width,
|
|
152
|
-
"",
|
|
153
|
-
);
|
|
154
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { readMetaFromMessage, type ReadCacheMode } from "./read-cache.ts";
|
|
3
|
-
import { createReadStatsPanel, type ReadSavingsSnapshot } from "./read-stats-panel.ts";
|
|
4
|
-
|
|
5
|
-
interface ContextTotals {
|
|
6
|
-
baseline: number;
|
|
7
|
-
returned: number;
|
|
8
|
-
unchangedSaved: number;
|
|
9
|
-
diffSaved: number;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function showReadStats(ctx: ExtensionCommandContext): Promise<void> {
|
|
13
|
-
const entries = ctx.sessionManager.getEntries();
|
|
14
|
-
const branchIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
|
|
15
|
-
const current = calculateSnapshot(entries, branchIds, "Current chat");
|
|
16
|
-
const whole = calculateSnapshot(entries, undefined, "Whole session");
|
|
17
|
-
await ctx.ui.custom(
|
|
18
|
-
(tui, theme, keybindings, done) => createReadStatsPanel(tui, theme, keybindings, done, current, whole),
|
|
19
|
-
{
|
|
20
|
-
overlay: true,
|
|
21
|
-
overlayOptions: { anchor: "center", width: "54%", minWidth: 50, maxHeight: "85%", margin: 2 },
|
|
22
|
-
},
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function calculateSnapshot(
|
|
27
|
-
entries: readonly unknown[],
|
|
28
|
-
includedIds: ReadonlySet<string> | undefined,
|
|
29
|
-
label: string,
|
|
30
|
-
): ReadSavingsSnapshot {
|
|
31
|
-
const states = new Map<string, ContextTotals>();
|
|
32
|
-
const counts: Record<ReadCacheMode, number> = { baseline: 0, recovery: 0, unchanged: 0, diff: 0 };
|
|
33
|
-
let baselineTokens = 0;
|
|
34
|
-
let returnedTokens = 0;
|
|
35
|
-
let costSaved = 0;
|
|
36
|
-
let unchangedCost = 0;
|
|
37
|
-
let diffCost = 0;
|
|
38
|
-
let readCount = 0;
|
|
39
|
-
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
if (!isRecord(entry) || typeof entry.id !== "string") continue;
|
|
42
|
-
const parent = typeof entry.parentId === "string" ? states.get(entry.parentId) : undefined;
|
|
43
|
-
const state: ContextTotals = parent
|
|
44
|
-
? { ...parent }
|
|
45
|
-
: { baseline: 0, returned: 0, unchangedSaved: 0, diffSaved: 0 };
|
|
46
|
-
if (entry.type === "compaction") {
|
|
47
|
-
state.baseline = 0;
|
|
48
|
-
state.returned = 0;
|
|
49
|
-
state.unchangedSaved = 0;
|
|
50
|
-
state.diffSaved = 0;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if (entry.type === "message" && "message" in entry) {
|
|
54
|
-
const meta = readMetaFromMessage(entry.message);
|
|
55
|
-
if (meta) {
|
|
56
|
-
state.baseline += meta.baselineTokens;
|
|
57
|
-
state.returned += meta.returnedTokens;
|
|
58
|
-
const saved = Math.max(0, meta.baselineTokens - meta.returnedTokens);
|
|
59
|
-
if (meta.mode === "unchanged") state.unchangedSaved += saved;
|
|
60
|
-
if (meta.mode === "diff") state.diffSaved += saved;
|
|
61
|
-
if (!includedIds || includedIds.has(entry.id)) {
|
|
62
|
-
counts[meta.mode] += 1;
|
|
63
|
-
readCount += 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if ((!includedIds || includedIds.has(entry.id)) && isAssistantMessage(entry.message)) {
|
|
68
|
-
baselineTokens += state.baseline;
|
|
69
|
-
returnedTokens += state.returned;
|
|
70
|
-
const inputTokens =
|
|
71
|
-
entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
|
|
72
|
-
const inputCost =
|
|
73
|
-
entry.message.usage.cost.input +
|
|
74
|
-
entry.message.usage.cost.cacheRead +
|
|
75
|
-
entry.message.usage.cost.cacheWrite;
|
|
76
|
-
const costPerToken = inputTokens > 0 ? inputCost / inputTokens : 0;
|
|
77
|
-
unchangedCost += state.unchangedSaved * costPerToken;
|
|
78
|
-
diffCost += state.diffSaved * costPerToken;
|
|
79
|
-
costSaved += (state.baseline - state.returned) * costPerToken;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
states.set(entry.id, state);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
label,
|
|
87
|
-
secondary: `${readCount} read${readCount === 1 ? "" : "s"} ${label === "Current chat" ? "in this chat" : "across everything done in this session"}`,
|
|
88
|
-
baselineTokens,
|
|
89
|
-
returnedTokens,
|
|
90
|
-
costSaved,
|
|
91
|
-
unchangedCost,
|
|
92
|
-
diffCost,
|
|
93
|
-
counts,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function isAssistantMessage(value: unknown): value is {
|
|
98
|
-
role: "assistant";
|
|
99
|
-
usage: {
|
|
100
|
-
input: number;
|
|
101
|
-
cacheRead: number;
|
|
102
|
-
cacheWrite: number;
|
|
103
|
-
cost: { input: number; cacheRead: number; cacheWrite: number };
|
|
104
|
-
};
|
|
105
|
-
} {
|
|
106
|
-
if (!isRecord(value) || value.role !== "assistant" || !isRecord(value.usage)) return false;
|
|
107
|
-
if (!isRecord(value.usage.cost)) return false;
|
|
108
|
-
return (
|
|
109
|
-
typeof value.usage.input === "number" &&
|
|
110
|
-
typeof value.usage.cacheRead === "number" &&
|
|
111
|
-
typeof value.usage.cacheWrite === "number" &&
|
|
112
|
-
typeof value.usage.cost.input === "number" &&
|
|
113
|
-
typeof value.usage.cost.cacheRead === "number" &&
|
|
114
|
-
typeof value.usage.cost.cacheWrite === "number"
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
119
|
-
return typeof value === "object" && value !== null;
|
|
120
|
-
}
|
|
@@ -1,335 +0,0 @@
|
|
|
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 { Container, Text } from "@earendil-works/pi-tui";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
import { readFile } from "node:fs/promises";
|
|
12
|
-
import { isAbsolute, resolve } from "node:path";
|
|
13
|
-
import { Type, type Static } from "typebox";
|
|
14
|
-
import { formatToolRowTitle, type ToolRowStateStore } from "../../shared/tool-row-state.js";
|
|
15
|
-
import {
|
|
16
|
-
createCompleteFileMeta,
|
|
17
|
-
estimateTokens,
|
|
18
|
-
readScopeKey,
|
|
19
|
-
selectCompleteFileResponse,
|
|
20
|
-
} from "./full-file-knowledge.ts";
|
|
21
|
-
import { normalizeCountLimit } from "./limits.ts";
|
|
22
|
-
import { stripLeadingAt } from "./path-display.ts";
|
|
23
|
-
import {
|
|
24
|
-
createReadCacheStore,
|
|
25
|
-
type ReadCacheMetaV1,
|
|
26
|
-
type ReadCachePresentation,
|
|
27
|
-
type ReadCacheStore,
|
|
28
|
-
} from "./read-cache.ts";
|
|
29
|
-
import { createReadSnapshotStore, type ReadSnapshotStore } from "./read-snapshots.ts";
|
|
30
|
-
|
|
31
|
-
const readSchema = Type.Object({
|
|
32
|
-
path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
|
|
33
|
-
offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
|
|
34
|
-
limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
|
|
35
|
-
lineNumbers: Type.Optional(Type.Boolean({ description: "Prefix text lines with their 1-indexed file line number" })),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
type ReadToolInput = Static<typeof readSchema>;
|
|
39
|
-
type BaseReadDefinition = ReturnType<typeof createReadToolDefinition>;
|
|
40
|
-
interface ExploreReadDetails extends ReadToolDetails {
|
|
41
|
-
readCache?: ReadCacheMetaV1;
|
|
42
|
-
}
|
|
43
|
-
type ReadDefinition = ToolDefinition<typeof readSchema, ExploreReadDetails | undefined>;
|
|
44
|
-
type ReadExecute = ReadDefinition["execute"];
|
|
45
|
-
type ReadRenderCall = NonNullable<ReadDefinition["renderCall"]>;
|
|
46
|
-
type ReadRenderResult = NonNullable<ReadDefinition["renderResult"]>;
|
|
47
|
-
|
|
48
|
-
interface BaselineTextResult {
|
|
49
|
-
text: string;
|
|
50
|
-
details: ReadToolDetails | undefined;
|
|
51
|
-
totalLines: number;
|
|
52
|
-
startLine: number;
|
|
53
|
-
endLine: number;
|
|
54
|
-
completeFile: boolean;
|
|
55
|
-
scopeKey: string;
|
|
56
|
-
presentation: ReadCachePresentation;
|
|
57
|
-
summary: string;
|
|
58
|
-
cacheable: boolean;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const readDefinitionByCwd = new Map<string, BaseReadDefinition>();
|
|
62
|
-
|
|
63
|
-
function readDefinitionForCwd(cwd: string): BaseReadDefinition {
|
|
64
|
-
const existing = readDefinitionByCwd.get(cwd);
|
|
65
|
-
if (existing) return existing;
|
|
66
|
-
const definition = createReadToolDefinition(cwd);
|
|
67
|
-
readDefinitionByCwd.set(cwd, definition);
|
|
68
|
-
return definition;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function normalizeReadParams(params: ReadToolInput): ReadToolInput {
|
|
72
|
-
return {
|
|
73
|
-
...params,
|
|
74
|
-
path: stripLeadingAt(params.path),
|
|
75
|
-
limit: params.limit === undefined ? undefined : normalizeCountLimit(params.limit, 1),
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function isSupportedImage(buffer: Buffer): boolean {
|
|
80
|
-
return (
|
|
81
|
-
(buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) ||
|
|
82
|
-
buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) ||
|
|
83
|
-
buffer.subarray(0, 6).toString("ascii") === "GIF87a" ||
|
|
84
|
-
buffer.subarray(0, 6).toString("ascii") === "GIF89a" ||
|
|
85
|
-
(buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP") ||
|
|
86
|
-
buffer.subarray(0, 2).toString("ascii") === "BM"
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function renderCallSummary(args: ReadToolInput | undefined): string {
|
|
91
|
-
if (!args) return "";
|
|
92
|
-
const path = stripLeadingAt(args.path);
|
|
93
|
-
if (args.offset === undefined && args.limit === undefined) return path;
|
|
94
|
-
const start = args.offset ?? 1;
|
|
95
|
-
const limit = args.limit === undefined ? undefined : normalizeCountLimit(args.limit, 1);
|
|
96
|
-
const end = limit === undefined ? "" : start + limit - 1;
|
|
97
|
-
return `${path}:${start}${end === "" ? "" : `-${end}`}`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function baselineText(text: string, params: ReadToolInput): BaselineTextResult {
|
|
101
|
-
const allLines = text.split("\n");
|
|
102
|
-
const startIndex = params.offset ? Math.max(0, params.offset - 1) : 0;
|
|
103
|
-
const startLine = startIndex + 1;
|
|
104
|
-
if (startIndex >= allLines.length) {
|
|
105
|
-
throw new Error(`Offset ${params.offset} is beyond end of file (${allLines.length} lines total)`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const selectedEnd =
|
|
109
|
-
params.limit === undefined ? allLines.length : Math.min(startIndex + params.limit, allLines.length);
|
|
110
|
-
const selectedLines = allLines.slice(startIndex, selectedEnd);
|
|
111
|
-
const selectedContent = selectedLines
|
|
112
|
-
.map((line, index) => (params.lineNumbers ? `${startLine + index}: ${line}` : line))
|
|
113
|
-
.join("\n");
|
|
114
|
-
const truncation = truncateHead(selectedContent);
|
|
115
|
-
let outputText: string;
|
|
116
|
-
let details: ReadToolDetails | undefined;
|
|
117
|
-
let outputLines = selectedLines.length;
|
|
118
|
-
let cacheable = true;
|
|
119
|
-
|
|
120
|
-
if (truncation.firstLineExceedsLimit) {
|
|
121
|
-
const firstLineSize = formatSize(Buffer.byteLength(selectedLines[0] ?? "", "utf-8"));
|
|
122
|
-
outputText = `[Line ${startLine} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLine}p' ${params.path} | head -c ${DEFAULT_MAX_BYTES}]`;
|
|
123
|
-
details = { truncation };
|
|
124
|
-
outputLines = 0;
|
|
125
|
-
cacheable = false;
|
|
126
|
-
} else if (truncation.truncated) {
|
|
127
|
-
outputLines = truncation.outputLines;
|
|
128
|
-
const endLineDisplay = startLine + outputLines - 1;
|
|
129
|
-
const nextOffset = endLineDisplay + 1;
|
|
130
|
-
outputText = truncation.content;
|
|
131
|
-
outputText +=
|
|
132
|
-
truncation.truncatedBy === "lines"
|
|
133
|
-
? `\n\n[Showing lines ${startLine}-${endLineDisplay} of ${allLines.length}. Use offset=${nextOffset} to continue.]`
|
|
134
|
-
: `\n\n[Showing lines ${startLine}-${endLineDisplay} of ${allLines.length} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;
|
|
135
|
-
details = { truncation };
|
|
136
|
-
} else if (selectedEnd < allLines.length) {
|
|
137
|
-
outputText = `${truncation.content}\n\n[${allLines.length - selectedEnd} more lines in file. Use offset=${selectedEnd + 1} to continue.]`;
|
|
138
|
-
} else {
|
|
139
|
-
outputText = truncation.content;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const endLine = outputLines === 0 ? startLine : startLine + outputLines - 1;
|
|
143
|
-
const completeFile = startIndex === 0 && selectedEnd === allLines.length && !truncation.truncated;
|
|
144
|
-
const presentation: ReadCachePresentation = params.lineNumbers ? "line-numbered" : "plain";
|
|
145
|
-
const scopeKey = readScopeKey(completeFile, startLine, endLine, presentation);
|
|
146
|
-
const summary = completeFile ? `${allLines.length} lines` : `${outputLines} lines`;
|
|
147
|
-
return {
|
|
148
|
-
text: outputText,
|
|
149
|
-
details,
|
|
150
|
-
totalLines: allLines.length,
|
|
151
|
-
startLine,
|
|
152
|
-
endLine,
|
|
153
|
-
completeFile,
|
|
154
|
-
scopeKey,
|
|
155
|
-
presentation,
|
|
156
|
-
summary,
|
|
157
|
-
cacheable,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function withMeta(baseline: BaselineTextResult, meta: ReadCacheMetaV1, text = baseline.text) {
|
|
162
|
-
return {
|
|
163
|
-
content: [{ type: "text" as const, text }],
|
|
164
|
-
details: { ...baseline.details, readCache: meta },
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function createMeta(
|
|
169
|
-
baseline: BaselineTextResult,
|
|
170
|
-
pathKey: string,
|
|
171
|
-
hash: string,
|
|
172
|
-
mode: ReadCacheMetaV1["mode"],
|
|
173
|
-
returnedText: string,
|
|
174
|
-
baseHash?: string,
|
|
175
|
-
summary = baseline.summary,
|
|
176
|
-
): ReadCacheMetaV1 {
|
|
177
|
-
if (baseline.completeFile) {
|
|
178
|
-
return createCompleteFileMeta({
|
|
179
|
-
pathKey,
|
|
180
|
-
presentation: baseline.presentation,
|
|
181
|
-
servedHash: hash,
|
|
182
|
-
mode,
|
|
183
|
-
sourceText: baseline.text,
|
|
184
|
-
returnedText,
|
|
185
|
-
totalLines: baseline.totalLines,
|
|
186
|
-
summary,
|
|
187
|
-
baseHash,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
v: 1,
|
|
192
|
-
pathKey,
|
|
193
|
-
scopeKey: baseline.scopeKey,
|
|
194
|
-
presentation: baseline.presentation,
|
|
195
|
-
servedHash: hash,
|
|
196
|
-
baseHash,
|
|
197
|
-
mode,
|
|
198
|
-
baselineTokens: estimateTokens(baseline.text),
|
|
199
|
-
returnedTokens: estimateTokens(returnedText),
|
|
200
|
-
totalLines: baseline.totalLines,
|
|
201
|
-
summary,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function createExploreReadTool(
|
|
206
|
-
rowState: ToolRowStateStore,
|
|
207
|
-
cache: ReadCacheStore = createReadCacheStore(),
|
|
208
|
-
snapshots: ReadSnapshotStore = createReadSnapshotStore(),
|
|
209
|
-
): ReadDefinition {
|
|
210
|
-
const baseDefinition = readDefinitionForCwd(process.cwd());
|
|
211
|
-
return {
|
|
212
|
-
...baseDefinition,
|
|
213
|
-
description:
|
|
214
|
-
"Read a relevant file as a whole by omitting offset, limit, and lineNumbers. Later whole-file reads avoid repeating unchanged content and return useful diffs or current source after changes. Use ranges only for targeted peripheral inspection or to continue after truncation.",
|
|
215
|
-
parameters: readSchema,
|
|
216
|
-
async execute(
|
|
217
|
-
toolCallId: Parameters<ReadExecute>[0],
|
|
218
|
-
params: Parameters<ReadExecute>[1],
|
|
219
|
-
signal: Parameters<ReadExecute>[2],
|
|
220
|
-
onUpdate: Parameters<ReadExecute>[3],
|
|
221
|
-
ctx: Parameters<ReadExecute>[4],
|
|
222
|
-
) {
|
|
223
|
-
const definition = readDefinitionForCwd(ctx.cwd);
|
|
224
|
-
const normalized = normalizeReadParams(params);
|
|
225
|
-
const path = isAbsolute(normalized.path) ? resolve(normalized.path) : resolve(ctx.cwd, normalized.path);
|
|
226
|
-
const snapshotEpoch = snapshots.epoch();
|
|
227
|
-
const buffer = await readFile(path);
|
|
228
|
-
if (isSupportedImage(buffer)) {
|
|
229
|
-
return definition.execute(
|
|
230
|
-
toolCallId,
|
|
231
|
-
{ path: normalized.path, offset: normalized.offset, limit: normalized.limit },
|
|
232
|
-
signal,
|
|
233
|
-
onUpdate,
|
|
234
|
-
ctx,
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
239
|
-
let text: string;
|
|
240
|
-
try {
|
|
241
|
-
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer);
|
|
242
|
-
} catch {
|
|
243
|
-
return definition.execute(toolCallId, normalized, signal, onUpdate, ctx);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const baseline = baselineText(text, normalized);
|
|
247
|
-
if (!baseline.cacheable) {
|
|
248
|
-
return { content: [{ type: "text", text: baseline.text }], details: baseline.details };
|
|
249
|
-
}
|
|
250
|
-
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
251
|
-
const staleEpoch = !snapshots.isCurrent(snapshotEpoch);
|
|
252
|
-
const decision = staleEpoch
|
|
253
|
-
? { baseHash: undefined, baselineText: undefined, recovery: false }
|
|
254
|
-
: cache.decision(ctx, path, baseline.scopeKey);
|
|
255
|
-
let output = baseline.text;
|
|
256
|
-
let mode: ReadCacheMetaV1["mode"] = decision.recovery ? "recovery" : "baseline";
|
|
257
|
-
let summary = baseline.summary;
|
|
258
|
-
|
|
259
|
-
if (baseline.completeFile) {
|
|
260
|
-
const selected = selectCompleteFileResponse({
|
|
261
|
-
displayPath: normalized.path,
|
|
262
|
-
currentText: text,
|
|
263
|
-
currentHash: hash,
|
|
264
|
-
fullText: baseline.text,
|
|
265
|
-
totalLines: baseline.totalLines,
|
|
266
|
-
recovery: decision.recovery,
|
|
267
|
-
baseHash: decision.baseHash,
|
|
268
|
-
baselineText:
|
|
269
|
-
decision.baselineText ??
|
|
270
|
-
(decision.baseHash === undefined ? undefined : snapshots.get(decision.baseHash)),
|
|
271
|
-
});
|
|
272
|
-
output = selected.text;
|
|
273
|
-
mode = selected.mode;
|
|
274
|
-
summary = selected.summary;
|
|
275
|
-
} else if (!decision.recovery && decision.baseHash === hash) {
|
|
276
|
-
output = `unchanged, lines ${baseline.startLine}-${baseline.endLine} of ${baseline.totalLines}`;
|
|
277
|
-
mode = "unchanged";
|
|
278
|
-
summary = output;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (signal?.aborted) throw new Error("Operation aborted");
|
|
282
|
-
snapshots.set(hash, text, buffer.byteLength, snapshotEpoch);
|
|
283
|
-
const meta = createMeta(baseline, path, hash, mode, output, decision.baseHash, summary);
|
|
284
|
-
return withMeta(baseline, meta, output);
|
|
285
|
-
},
|
|
286
|
-
renderCall(
|
|
287
|
-
args: Parameters<ReadRenderCall>[0],
|
|
288
|
-
theme: Parameters<ReadRenderCall>[1],
|
|
289
|
-
context: Parameters<ReadRenderCall>[2],
|
|
290
|
-
) {
|
|
291
|
-
rowState.watch(context.toolCallId, context.invalidate);
|
|
292
|
-
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
293
|
-
if (context.executionStarted || !context.isPartial) {
|
|
294
|
-
text.setText("");
|
|
295
|
-
return text;
|
|
296
|
-
}
|
|
297
|
-
const title = formatToolRowTitle(rowState, context.toolCallId, "read", theme);
|
|
298
|
-
text.setText(`${title} ${theme.fg("muted", renderCallSummary(args))}`);
|
|
299
|
-
return text;
|
|
300
|
-
},
|
|
301
|
-
renderResult(
|
|
302
|
-
result: Parameters<ReadRenderResult>[0],
|
|
303
|
-
options: Parameters<ReadRenderResult>[1],
|
|
304
|
-
theme: Parameters<ReadRenderResult>[2],
|
|
305
|
-
context: Parameters<ReadRenderResult>[3],
|
|
306
|
-
) {
|
|
307
|
-
rowState.watch(context.toolCallId, context.invalidate);
|
|
308
|
-
const container = new Container();
|
|
309
|
-
const title = formatToolRowTitle(rowState, context.toolCallId, "read", theme);
|
|
310
|
-
const args = context.args as ReadToolInput | undefined;
|
|
311
|
-
const details = result.details as ExploreReadDetails | undefined;
|
|
312
|
-
const summary = details?.readCache?.summary;
|
|
313
|
-
const summaryText = context.isError
|
|
314
|
-
? theme.fg("error", "error")
|
|
315
|
-
: summary
|
|
316
|
-
? theme.fg(
|
|
317
|
-
details?.readCache?.mode === "unchanged"
|
|
318
|
-
? "success"
|
|
319
|
-
: details?.readCache?.mode === "diff"
|
|
320
|
-
? "accent"
|
|
321
|
-
: "muted",
|
|
322
|
-
summary,
|
|
323
|
-
)
|
|
324
|
-
: "";
|
|
325
|
-
const header = `${title} ${theme.fg("muted", renderCallSummary(args))}${summaryText ? ` ${summaryText}` : ""}`;
|
|
326
|
-
container.addChild(new Text(header, 0, 0));
|
|
327
|
-
if (options.expanded) {
|
|
328
|
-
const definition = readDefinitionForCwd(context.cwd);
|
|
329
|
-
const body = definition.renderResult?.(result, { ...options, expanded: true }, theme, context);
|
|
330
|
-
if (body) container.addChild(body);
|
|
331
|
-
}
|
|
332
|
-
return container;
|
|
333
|
-
},
|
|
334
|
-
};
|
|
335
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
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
|
-
}
|
|
Binary file
|