@shanepadgett/tau-agent 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/extensions/context/sync.ts +1 -9
  2. package/extensions/context-pruning/prune.ts +2 -2
  3. package/extensions/explore/README.md +13 -21
  4. package/extensions/explore/ast/adapter.ts +97 -0
  5. package/extensions/explore/ast/engine.ts +470 -0
  6. package/extensions/explore/ast/format/ast-search.ts +97 -0
  7. package/extensions/explore/ast/format/composite.ts +26 -0
  8. package/extensions/explore/ast/format/context.ts +66 -0
  9. package/extensions/explore/ast/format/deps.ts +86 -0
  10. package/extensions/explore/ast/format/discover.ts +85 -0
  11. package/extensions/explore/ast/format/impact.ts +105 -0
  12. package/extensions/explore/ast/format/outline.ts +66 -0
  13. package/extensions/explore/ast/format/relationships.ts +81 -0
  14. package/extensions/explore/ast/format/show.ts +38 -0
  15. package/extensions/explore/ast/grammars/kotlin.wasm +0 -0
  16. package/extensions/explore/ast/grammars/manifest.json +41 -0
  17. package/extensions/explore/ast/grammars/manifest.ts +99 -0
  18. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  19. package/extensions/explore/ast/grammars/swift.wasm +0 -0
  20. package/extensions/explore/ast/graph/file-graph.ts +360 -0
  21. package/extensions/explore/ast/graph/relationships.ts +597 -0
  22. package/extensions/explore/ast/identity.ts +119 -0
  23. package/extensions/explore/ast/ir.ts +136 -0
  24. package/extensions/explore/ast/languages/csharp-file-deps.ts +189 -0
  25. package/extensions/explore/ast/languages/csharp.ts +493 -0
  26. package/extensions/explore/ast/languages/file-dep-util.ts +120 -0
  27. package/extensions/explore/ast/languages/fixtures/sample.cs +17 -0
  28. package/extensions/explore/ast/languages/fixtures/sample.java +21 -0
  29. package/extensions/explore/ast/languages/fixtures/sample.kt +15 -0
  30. package/extensions/explore/ast/languages/fixtures/sample.odin +28 -0
  31. package/extensions/explore/ast/languages/fixtures/sample.rs +31 -0
  32. package/extensions/explore/ast/languages/fixtures/sample.swift +25 -0
  33. package/extensions/explore/ast/languages/go-file-deps.ts +139 -0
  34. package/extensions/explore/ast/languages/go.ts +545 -0
  35. package/extensions/explore/ast/languages/java.ts +412 -0
  36. package/extensions/explore/ast/languages/jvm-file-deps.ts +217 -0
  37. package/extensions/explore/ast/languages/kotlin.ts +611 -0
  38. package/extensions/explore/ast/languages/odin-file-deps.ts +175 -0
  39. package/extensions/explore/ast/languages/odin.ts +407 -0
  40. package/extensions/explore/ast/languages/rust-file-deps.ts +159 -0
  41. package/extensions/explore/ast/languages/rust.ts +506 -0
  42. package/extensions/explore/ast/languages/swift-file-deps.ts +59 -0
  43. package/extensions/explore/ast/languages/swift.ts +466 -0
  44. package/extensions/explore/ast/languages/tree.ts +220 -0
  45. package/extensions/explore/ast/languages/typescript-file-deps.ts +259 -0
  46. package/extensions/explore/ast/languages/typescript-package-surface.ts +440 -0
  47. package/extensions/explore/ast/languages/typescript.ts +751 -0
  48. package/extensions/explore/ast/markdown.ts +145 -0
  49. package/extensions/explore/ast/package-surface.ts +42 -0
  50. package/extensions/explore/ast/queries/ast-search.ts +306 -0
  51. package/extensions/explore/ast/queries/composite-target.ts +77 -0
  52. package/extensions/explore/ast/queries/context.ts +406 -0
  53. package/extensions/explore/ast/queries/discover.ts +427 -0
  54. package/extensions/explore/ast/queries/impact.ts +250 -0
  55. package/extensions/explore/ast/queries/outline.ts +203 -0
  56. package/extensions/explore/ast/queries/show.ts +262 -0
  57. package/extensions/explore/ast/query.ts +48 -0
  58. package/extensions/explore/ast/read/hook.ts +125 -0
  59. package/extensions/explore/ast/read/policy.ts +55 -0
  60. package/extensions/explore/ast/registry.ts +120 -0
  61. package/extensions/explore/ast/scan.ts +76 -0
  62. package/extensions/explore/ast/slice.ts +44 -0
  63. package/extensions/explore/ast/tools/ast-search.ts +138 -0
  64. package/extensions/explore/ast/tools/context.ts +126 -0
  65. package/extensions/explore/ast/tools/deps.ts +72 -0
  66. package/extensions/explore/ast/tools/discover.ts +208 -0
  67. package/extensions/explore/ast/tools/impact.ts +135 -0
  68. package/extensions/explore/ast/tools/outline.ts +200 -0
  69. package/extensions/explore/ast/tools/relationships.ts +160 -0
  70. package/extensions/explore/ast/tools/render.ts +121 -0
  71. package/extensions/explore/ast/tools/reverse-deps.ts +72 -0
  72. package/extensions/explore/ast/tools/show.ts +116 -0
  73. package/extensions/explore/guidance.ts +52 -0
  74. package/extensions/explore/index.ts +82 -92
  75. package/extensions/explore/read/autoread.ts +118 -0
  76. package/extensions/explore/settings.ts +49 -31
  77. package/extensions/explore/traverse.ts +254 -91
  78. package/extensions/handoff/index.ts +1 -1
  79. package/extensions/publish/index.ts +18 -85
  80. package/extensions/publish/publish-ui.ts +0 -4
  81. package/extensions/soul/README.md +7 -4
  82. package/extensions/soul/index.ts +11 -5
  83. package/extensions/soul/prompt.ts +31 -13
  84. package/extensions/soul/settings.ts +7 -2
  85. package/extensions/subagent/agents/context-sync.md +6 -10
  86. package/extensions/subagent/agents/review.md +24 -55
  87. package/extensions/subagent/agents/scout.md +38 -115
  88. package/extensions/subagent/run.ts +1 -1
  89. package/extensions/tau-help/help.md +2 -4
  90. package/package.json +6 -4
  91. package/schemas/tau.schema.json +35 -30
  92. package/{extensions/explore → shared}/autoread.ts +21 -7
  93. package/shared/events.ts +1 -1
  94. package/shared/full-file-knowledge.ts +49 -0
  95. package/extensions/explore/ast-guidance.ts +0 -67
  96. package/extensions/explore/ast-languages.ts +0 -61
  97. package/extensions/explore/ast-tools.ts +0 -1994
  98. package/extensions/explore/ast-worker.ts +0 -1025
  99. package/extensions/explore/find.ts +0 -147
  100. package/extensions/explore/full-file-knowledge.ts +0 -234
  101. package/extensions/explore/grep.ts +0 -749
  102. package/extensions/explore/limits.ts +0 -22
  103. package/extensions/explore/ls.ts +0 -113
  104. package/extensions/explore/orientation-state.ts +0 -280
  105. package/extensions/explore/path-display.ts +0 -41
  106. package/extensions/explore/path-tree.ts +0 -150
  107. package/extensions/explore/read-cache.ts +0 -273
  108. package/extensions/explore/read-snapshots.ts +0 -57
  109. package/extensions/explore/read-stats-panel.ts +0 -176
  110. package/extensions/explore/read-stats.ts +0 -138
  111. package/extensions/explore/read.ts +0 -372
  112. package/extensions/explore/result.ts +0 -29
  113. package/native-bin/darwin-arm64/tau-ast +0 -0
@@ -1,273 +0,0 @@
1
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { resolve } from "node:path";
3
- import { replayContextPruningState } from "../../shared/context-pruning-state.ts";
4
- import { applyCompleteFileDiff, COMPLETE_FILE_SCOPE, decodeCompleteFileSource } from "./full-file-knowledge.ts";
5
-
6
- export type ReadCacheMode = "baseline" | "recovery" | "unchanged" | "diff";
7
- export type ReadCachePresentation = "plain" | "line-numbered";
8
-
9
- export interface ReadCacheMetaV1 {
10
- v: 1;
11
- pathKey: string;
12
- scopeKey: string;
13
- presentation: ReadCachePresentation;
14
- servedHash: string;
15
- baseHash?: string;
16
- mode: ReadCacheMode;
17
- baselineTokens: number;
18
- returnedTokens: number;
19
- totalLines: number;
20
- summary: string;
21
- }
22
-
23
- export interface ReadCacheScopeTrust {
24
- hash: string;
25
- text?: string;
26
- rowIds: readonly string[];
27
- }
28
-
29
- export interface AcceptedReadCacheRow {
30
- rowId: string;
31
- pathKey: string;
32
- scopeKey: string;
33
- meta: ReadCacheMetaV1;
34
- dependencyRowIds: readonly string[];
35
- }
36
-
37
- export interface CompleteFileDependencyChain {
38
- pathKey: string;
39
- servedHash: string;
40
- rowIds: readonly string[];
41
- sourceText: string;
42
- }
43
-
44
- export interface ReadCacheReplayResult {
45
- scopeTrust: ReadonlyMap<string, ReadonlyMap<string, ReadCacheScopeTrust>>;
46
- acceptedRows: readonly AcceptedReadCacheRow[];
47
- completeFileChains: ReadonlyMap<string, CompleteFileDependencyChain>;
48
- failedPatchRecoveryPaths: ReadonlySet<string>;
49
- }
50
-
51
- export interface ReadCacheDecision {
52
- baseHash?: string;
53
- baselineText?: string;
54
- recovery: boolean;
55
- }
56
-
57
- export interface ReadCacheStore {
58
- decision(ctx: ExtensionContext, pathKey: string, scopeKey: string): ReadCacheDecision;
59
- }
60
-
61
- export function createReadCacheStore(): ReadCacheStore {
62
- return {
63
- decision(ctx, pathKey, scopeKey) {
64
- const manager = ctx.sessionManager;
65
- if (!manager) {
66
- return { baseHash: undefined, baselineText: undefined, recovery: false };
67
- }
68
- const pruning = replayContextPruningState(manager.getBranch());
69
- const ignoredRowIds = new Set([...pruning.prunedToolCallIds, ...pruning.prunedAutoreadRowIds]);
70
- for (const id of pruning.retainedToolCallIds) ignoredRowIds.delete(id);
71
- const state = replayReadCache(manager.buildContextEntries(), ctx.cwd, ignoredRowIds);
72
- const trust = state.scopeTrust.get(pathKey)?.get(scopeKey);
73
- return {
74
- baseHash: trust?.hash,
75
- baselineText: trust?.text,
76
- recovery: state.failedPatchRecoveryPaths.has(pathKey),
77
- };
78
- },
79
- };
80
- }
81
-
82
- function parseReadCacheMeta(value: unknown): ReadCacheMetaV1 | undefined {
83
- if (!isRecord(value) || value.v !== 1) return undefined;
84
- if (typeof value.pathKey !== "string" || typeof value.scopeKey !== "string") return undefined;
85
- if (value.presentation !== "plain" && value.presentation !== "line-numbered") return undefined;
86
- if (typeof value.servedHash !== "string" || value.servedHash.length === 0) return undefined;
87
- if (!isReadCacheMode(value.mode)) return undefined;
88
- if (!isNonNegativeFinite(value.baselineTokens) || !isNonNegativeFinite(value.returnedTokens)) return undefined;
89
- if (!isNonNegativeFinite(value.totalLines) || typeof value.summary !== "string") return undefined;
90
- if (value.baseHash !== undefined && typeof value.baseHash !== "string") return undefined;
91
- if ((value.mode === "unchanged" || value.mode === "diff") && !value.baseHash) return undefined;
92
- return {
93
- v: 1,
94
- pathKey: value.pathKey,
95
- scopeKey: value.scopeKey,
96
- presentation: value.presentation,
97
- servedHash: value.servedHash,
98
- baseHash: value.baseHash,
99
- mode: value.mode,
100
- baselineTokens: value.baselineTokens,
101
- returnedTokens: value.returnedTokens,
102
- totalLines: value.totalLines,
103
- summary: value.summary,
104
- };
105
- }
106
-
107
- export function readMetaFromMessage(message: unknown): ReadCacheMetaV1 | undefined {
108
- if (!isRecord(message)) return undefined;
109
- const isReadResult = message.role === "toolResult" && message.toolName === "read";
110
- const isAutoread = message.customType === "tau.autoread";
111
- if (!isReadResult && !isAutoread) return undefined;
112
- if (!isRecord(message.details)) return undefined;
113
- if (isAutoread && message.details.status !== "read") return undefined;
114
- return parseReadCacheMeta(message.details.readCache);
115
- }
116
-
117
- export function replayReadCache(
118
- entries: readonly unknown[],
119
- cwd: string,
120
- ignoredRowIds: ReadonlySet<string> = new Set(),
121
- ): ReadCacheReplayResult {
122
- const trust = new Map<string, Map<string, ReadCacheScopeTrust>>();
123
- const acceptedRows: AcceptedReadCacheRow[] = [];
124
- const failedPatchRecoveryPaths = new Set<string>();
125
- for (const entry of entries) {
126
- if (!isRecord(entry)) continue;
127
- let message: unknown;
128
- if (entry.type === "message" && "message" in entry) message = entry.message;
129
- else if (entry.type === "custom_message") message = entry;
130
- else continue;
131
- const parsedMeta = readMetaFromMessage(message);
132
- if (parsedMeta) {
133
- const rowId = readRowId(message);
134
- if (!rowId || ignoredRowIds.has(rowId)) continue;
135
- const pathKey = resolve(cwd, parsedMeta.pathKey);
136
- const meta = pathKey === parsedMeta.pathKey ? parsedMeta : { ...parsedMeta, pathKey };
137
- if (applyReadMeta(trust, failedPatchRecoveryPaths, message, meta, rowId)) {
138
- const acceptedTrust = trust.get(meta.pathKey)?.get(meta.scopeKey);
139
- if (acceptedTrust)
140
- acceptedRows.push({
141
- rowId,
142
- pathKey: meta.pathKey,
143
- scopeKey: meta.scopeKey,
144
- meta,
145
- dependencyRowIds: [...acceptedTrust.rowIds],
146
- });
147
- }
148
- continue;
149
- }
150
- for (const path of failedPatchPaths(message, cwd)) failedPatchRecoveryPaths.add(path);
151
- }
152
- const completeFileChains = new Map<string, CompleteFileDependencyChain>();
153
- for (const [pathKey, scopes] of trust) {
154
- const complete = scopes.get(COMPLETE_FILE_SCOPE);
155
- if (!complete || complete.text === undefined) continue;
156
- completeFileChains.set(pathKey, {
157
- pathKey,
158
- servedHash: complete.hash,
159
- rowIds: [...complete.rowIds],
160
- sourceText: complete.text,
161
- });
162
- }
163
- return { scopeTrust: trust, acceptedRows, completeFileChains, failedPatchRecoveryPaths };
164
- }
165
-
166
- function applyReadMeta(
167
- trust: Map<string, Map<string, ReadCacheScopeTrust>>,
168
- failedPatchRecoveryPaths: Set<string>,
169
- message: unknown,
170
- meta: ReadCacheMetaV1,
171
- rowId: string,
172
- ): boolean {
173
- const existing = trust.get(meta.pathKey)?.get(meta.scopeKey);
174
- if (meta.mode === "baseline" || meta.mode === "recovery") {
175
- let text: string | undefined;
176
- if (meta.scopeKey === COMPLETE_FILE_SCOPE) {
177
- if (!isRecord(message)) return false;
178
- let pathHeader: string | undefined;
179
- if (message.customType === "tau.autoread") {
180
- if (!isRecord(message.details)) return false;
181
- if (typeof message.details.path !== "string" || typeof message.details.cwd !== "string") return false;
182
- if (resolve(message.details.cwd, message.details.path) !== meta.pathKey) return false;
183
- pathHeader = message.details.path;
184
- }
185
- const decoded = decodeCompleteFileSource({
186
- content: message.content,
187
- autoread: message.customType === "tau.autoread",
188
- ...(pathHeader === undefined ? {} : { pathHeader }),
189
- presentation: meta.presentation,
190
- servedHash: meta.servedHash,
191
- });
192
- if (!decoded.valid || decoded.text === undefined) return false;
193
- text = decoded.text;
194
- }
195
- setTrust(trust, meta.pathKey, meta.scopeKey, { hash: meta.servedHash, text, rowIds: [rowId] });
196
- failedPatchRecoveryPaths.delete(meta.pathKey);
197
- return true;
198
- }
199
- if (!meta.baseHash || existing?.hash !== meta.baseHash) return false;
200
- if (meta.mode === "unchanged" && meta.servedHash !== meta.baseHash) return false;
201
- let text = meta.mode === "unchanged" ? existing.text : undefined;
202
- if (meta.scopeKey === COMPLETE_FILE_SCOPE && meta.mode === "diff") {
203
- if (existing.text === undefined) return false;
204
- const decoded = applyCompleteFileDiff({
205
- content: isRecord(message) ? message.content : undefined,
206
- baseText: existing.text,
207
- baseHash: meta.baseHash,
208
- servedHash: meta.servedHash,
209
- });
210
- if (!decoded.valid || decoded.text === undefined) return false;
211
- text = decoded.text;
212
- }
213
- setTrust(trust, meta.pathKey, meta.scopeKey, {
214
- hash: meta.servedHash,
215
- text,
216
- rowIds: [...existing.rowIds, rowId],
217
- });
218
- return true;
219
- }
220
-
221
- function readRowId(message: unknown): string | undefined {
222
- if (!isRecord(message)) return undefined;
223
- if (message.role === "toolResult" && message.toolName === "read") {
224
- return typeof message.toolCallId === "string" && message.toolCallId.length > 0 ? message.toolCallId : undefined;
225
- }
226
- if (message.customType !== "tau.autoread" || !isRecord(message.details)) return undefined;
227
- return typeof message.details.rowId === "string" && message.details.rowId.length > 0
228
- ? message.details.rowId
229
- : undefined;
230
- }
231
-
232
- function failedPatchPaths(message: unknown, cwd: string): string[] {
233
- if (!isRecord(message) || message.role !== "toolResult" || message.toolName !== "patch") return [];
234
- if (
235
- !isRecord(message.details) ||
236
- message.details.status === "completed" ||
237
- !Array.isArray(message.details.failures)
238
- ) {
239
- return [];
240
- }
241
- const paths: string[] = [];
242
- for (const failure of message.details.failures) {
243
- if (!isRecord(failure) || typeof failure.path !== "string" || failure.path.trim().length === 0) continue;
244
- paths.push(resolve(cwd, failure.path));
245
- }
246
- return paths;
247
- }
248
-
249
- function setTrust(
250
- trust: Map<string, Map<string, ReadCacheScopeTrust>>,
251
- pathKey: string,
252
- scopeKey: string,
253
- value: ReadCacheScopeTrust,
254
- ): void {
255
- let scopes = trust.get(pathKey);
256
- if (!scopes) {
257
- scopes = new Map();
258
- trust.set(pathKey, scopes);
259
- }
260
- scopes.set(scopeKey, value);
261
- }
262
-
263
- function isReadCacheMode(value: unknown): value is ReadCacheMode {
264
- return value === "baseline" || value === "recovery" || value === "unchanged" || value === "diff";
265
- }
266
-
267
- function isNonNegativeFinite(value: unknown): value is number {
268
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
269
- }
270
-
271
- function isRecord(value: unknown): value is Record<string, unknown> {
272
- return typeof value === "object" && value !== null;
273
- }
@@ -1,57 +0,0 @@
1
- import { MAX_COMPLETE_FILE_SNAPSHOT_BYTES } from "./full-file-knowledge.ts";
2
-
3
- const MAX_TOTAL_BYTES = 16 * 1024 * 1024;
4
-
5
- interface Snapshot {
6
- text: string;
7
- bytes: number;
8
- }
9
-
10
- export interface ReadSnapshotStore {
11
- get(hash: string): string | undefined;
12
- epoch(): number;
13
- isCurrent(epoch: number): boolean;
14
- set(hash: string, text: string, bytes: number, epoch: number): boolean;
15
- clear(): void;
16
- }
17
-
18
- export function createReadSnapshotStore(): ReadSnapshotStore {
19
- const snapshots = new Map<string, Snapshot>();
20
- let totalBytes = 0;
21
- let epoch = 0;
22
-
23
- return {
24
- get(hash) {
25
- const snapshot = snapshots.get(hash);
26
- if (!snapshot) return undefined;
27
- snapshots.delete(hash);
28
- snapshots.set(hash, snapshot);
29
- return snapshot.text;
30
- },
31
- epoch() {
32
- return epoch;
33
- },
34
- isCurrent(candidate) {
35
- return candidate === epoch;
36
- },
37
- set(hash, text, bytes, candidateEpoch) {
38
- if (candidateEpoch !== epoch || bytes > MAX_COMPLETE_FILE_SNAPSHOT_BYTES) return false;
39
- if (snapshots.has(hash)) return true;
40
- snapshots.set(hash, { text, bytes });
41
- totalBytes += bytes;
42
- while (totalBytes > MAX_TOTAL_BYTES) {
43
- const oldestHash = snapshots.keys().next().value as string | undefined;
44
- if (oldestHash === undefined) break;
45
- const oldest = snapshots.get(oldestHash);
46
- snapshots.delete(oldestHash);
47
- totalBytes -= oldest?.bytes ?? 0;
48
- }
49
- return true;
50
- },
51
- clear() {
52
- snapshots.clear();
53
- totalBytes = 0;
54
- epoch += 1;
55
- },
56
- };
57
- }
@@ -1,176 +0,0 @@
1
- import { formatSize, 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
- import type { OrientationTelemetry } from "./orientation-state.ts";
6
-
7
- export interface ReadSavingsSnapshot {
8
- label: string;
9
- secondary: string;
10
- baselineTokens: number;
11
- returnedTokens: number;
12
- costSaved: number;
13
- unchangedCost: number;
14
- diffCost: number;
15
- counts: Record<ReadCacheMode, number>;
16
- gate: OrientationTelemetry;
17
- }
18
-
19
- export function createReadStatsPanel(
20
- tui: TUI,
21
- theme: Theme,
22
- keybindings: KeybindingsManager,
23
- done: (result: unknown) => void,
24
- current: ReadSavingsSnapshot,
25
- whole: ReadSavingsSnapshot,
26
- ): Component {
27
- return new ReadStatsPanel(tui, theme, keybindings, done, current, whole);
28
- }
29
-
30
- class ReadStatsPanel implements Component {
31
- private readonly tui: TUI;
32
- private readonly keybindings: KeybindingsManager;
33
- private readonly done: (result: unknown) => void;
34
- private readonly snapshots: readonly [ReadSavingsSnapshot, ReadSavingsSnapshot];
35
- private readonly tabs: Tabs;
36
- private readonly config: ToolPanelConfig;
37
- private readonly panel: ToolPanel;
38
-
39
- constructor(
40
- tui: TUI,
41
- theme: Theme,
42
- keybindings: KeybindingsManager,
43
- done: (result: unknown) => void,
44
- current: ReadSavingsSnapshot,
45
- whole: ReadSavingsSnapshot,
46
- ) {
47
- this.tui = tui;
48
- this.keybindings = keybindings;
49
- this.done = done;
50
- this.snapshots = [current, whole];
51
- this.tabs = new Tabs(
52
- theme,
53
- this.snapshots.map((snapshot) => ({
54
- id: snapshot.label,
55
- label: snapshot.label,
56
- body: new SavingsBody(theme, snapshot),
57
- })),
58
- current.label,
59
- );
60
- this.config = {
61
- title: "Read savings",
62
- secondary: current.secondary,
63
- body: this.tabs,
64
- footer: {
65
- kind: "hints",
66
- hints: [...this.tabs.getKeyHints(), bindingHint("tui.select.cancel", "close")],
67
- },
68
- border: "box",
69
- };
70
- this.panel = new ToolPanel(theme, this.config);
71
- }
72
-
73
- handleInput(data: string): void {
74
- if (this.keybindings.matches(data, "tui.select.cancel")) {
75
- this.done(undefined);
76
- return;
77
- }
78
- this.tabs.handleInput(data);
79
- const snapshot = this.snapshots.find((item) => item.label === this.tabs.getActiveId()) ?? this.snapshots[0];
80
- this.config.secondary = snapshot.secondary;
81
- this.tui.requestRender();
82
- }
83
-
84
- render(width: number): string[] {
85
- return this.panel.render(width);
86
- }
87
-
88
- invalidate(): void {
89
- this.panel.invalidate();
90
- }
91
- }
92
-
93
- class SavingsBody implements Component {
94
- private readonly theme: Theme;
95
- private readonly snapshot: ReadSavingsSnapshot;
96
-
97
- constructor(theme: Theme, snapshot: ReadSavingsSnapshot) {
98
- this.theme = theme;
99
- this.snapshot = snapshot;
100
- }
101
-
102
- render(width: number): string[] {
103
- const avoided = Math.max(0, this.snapshot.baselineTokens - this.snapshot.returnedTokens);
104
- const reduction = this.snapshot.baselineTokens > 0 ? avoided / this.snapshot.baselineTokens : 0;
105
- const sourceAvoided = Math.max(0, this.snapshot.gate.sourceBytesDeflected - this.snapshot.gate.overflowReadBytes);
106
- return [
107
- this.theme.bold("Tokens"),
108
- metricRow("Without cache", formatTokens(this.snapshot.baselineTokens), width),
109
- metricRow("With cache", formatTokens(this.snapshot.returnedTokens), width),
110
- metricRow(
111
- "Saved",
112
- this.theme.fg("success", `${formatTokens(avoided)} tokens (${Math.round(reduction * 100)}%)`),
113
- width,
114
- ),
115
- "",
116
- this.theme.bold("Cost saved"),
117
- metricRow("Total", this.theme.fg("success", this.theme.bold(formatCost(this.snapshot.costSaved))), width),
118
- metricRow("Unchanged", formatCost(this.snapshot.unchangedCost), width),
119
- metricRow("Changes only", formatCost(this.snapshot.diffCost), width),
120
- "",
121
- this.theme.bold("Read results"),
122
- countRow("Baseline", this.snapshot.counts.baseline, "Unchanged", this.snapshot.counts.unchanged, width),
123
- countRow("Changes", this.snapshot.counts.diff, "Recovery", this.snapshot.counts.recovery, width),
124
- "",
125
- this.theme.bold("AST-first reads"),
126
- countRow(
127
- "Blocked",
128
- this.snapshot.gate.blockedReadAttempts,
129
- "Permitted",
130
- this.snapshot.gate.permittedReadAttempts,
131
- width,
132
- ),
133
- metricRow("Fallback reads", String(this.snapshot.gate.fallbackReadAttempts), width),
134
- metricRow("Source avoided", formatSize(sourceAvoided), width),
135
- metricRow("Direct returned", formatSize(this.snapshot.gate.directReadBytes), width),
136
- metricRow("Overflow reread", formatSize(this.snapshot.gate.overflowReadBytes), width),
137
- "",
138
- this.theme.bold("AST bytes"),
139
- metricRow("Worker input", formatSize(this.snapshot.gate.workerInputBytes), width),
140
- metricRow("Full rendered", formatSize(this.snapshot.gate.completeRenderedBytes), width),
141
- metricRow("Model visible", formatSize(this.snapshot.gate.modelVisibleAstBytes), width),
142
- metricRow("Temporary", formatSize(this.snapshot.gate.temporaryOutputBytes), width),
143
- "",
144
- truncateToWidth(
145
- this.theme.fg("dim", "Estimates include repeated chat history and prompt caching."),
146
- width,
147
- "",
148
- ),
149
- ];
150
- }
151
-
152
- invalidate(): void {}
153
- }
154
-
155
- function formatTokens(value: number): string {
156
- if (value >= 1_000_000) return `~${(value / 1_000_000).toFixed(2)}M`;
157
- if (value >= 1_000) return `~${Math.round(value / 1_000)}k`;
158
- return `~${Math.max(0, Math.round(value))}`;
159
- }
160
-
161
- function formatCost(value: number): string {
162
- if (value > 0 && value < 0.01) return "<$0.01";
163
- return `~$${Math.max(0, value).toFixed(2)}`;
164
- }
165
-
166
- function metricRow(label: string, value: string, width: number): string {
167
- return truncateToWidth(`${label.padEnd(16)}${value}`, width, "");
168
- }
169
-
170
- function countRow(left: string, leftCount: number, right: string, rightCount: number, width: number): string {
171
- return truncateToWidth(
172
- `${left.padEnd(11)}${String(leftCount).padEnd(6)}${right.padEnd(11)}${rightCount}`,
173
- width,
174
- "",
175
- );
176
- }
@@ -1,138 +0,0 @@
1
- import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import type { OrientationState } from "./orientation-state.ts";
3
- import { readMetaFromMessage, type ReadCacheMode } from "./read-cache.ts";
4
- import { createReadStatsPanel, type ReadSavingsSnapshot } from "./read-stats-panel.ts";
5
-
6
- interface ContextTotals {
7
- baseline: number;
8
- returned: number;
9
- unchangedSaved: number;
10
- diffSaved: number;
11
- }
12
-
13
- export async function showReadStats(ctx: ExtensionCommandContext, orientation: OrientationState): Promise<void> {
14
- const entries = ctx.sessionManager.getEntries();
15
- const branchIds = new Set(ctx.sessionManager.getBranch().map((entry) => entry.id));
16
- const current = calculateSnapshot(
17
- entries,
18
- branchIds,
19
- "Current chat",
20
- orientation.telemetry(toolCallIds(entries, branchIds)),
21
- );
22
- const whole = calculateSnapshot(entries, undefined, "Whole session", orientation.telemetry(undefined));
23
- await ctx.ui.custom(
24
- (tui, theme, keybindings, done) => createReadStatsPanel(tui, theme, keybindings, done, current, whole),
25
- {
26
- overlay: true,
27
- overlayOptions: { anchor: "center", width: "54%", minWidth: 50, maxHeight: "85%", margin: 2 },
28
- },
29
- );
30
- }
31
-
32
- function calculateSnapshot(
33
- entries: readonly unknown[],
34
- includedIds: ReadonlySet<string> | undefined,
35
- label: string,
36
- gate: ReadSavingsSnapshot["gate"],
37
- ): ReadSavingsSnapshot {
38
- const states = new Map<string, ContextTotals>();
39
- const counts: Record<ReadCacheMode, number> = { baseline: 0, recovery: 0, unchanged: 0, diff: 0 };
40
- let baselineTokens = 0;
41
- let returnedTokens = 0;
42
- let costSaved = 0;
43
- let unchangedCost = 0;
44
- let diffCost = 0;
45
- let readCount = 0;
46
-
47
- for (const entry of entries) {
48
- if (!isRecord(entry) || typeof entry.id !== "string") continue;
49
- const parent = typeof entry.parentId === "string" ? states.get(entry.parentId) : undefined;
50
- const state: ContextTotals = parent
51
- ? { ...parent }
52
- : { baseline: 0, returned: 0, unchangedSaved: 0, diffSaved: 0 };
53
- if (entry.type === "compaction") {
54
- state.baseline = 0;
55
- state.returned = 0;
56
- state.unchangedSaved = 0;
57
- state.diffSaved = 0;
58
- }
59
-
60
- if (entry.type === "message" && "message" in entry) {
61
- const meta = readMetaFromMessage(entry.message);
62
- if (meta) {
63
- state.baseline += meta.baselineTokens;
64
- state.returned += meta.returnedTokens;
65
- const saved = Math.max(0, meta.baselineTokens - meta.returnedTokens);
66
- if (meta.mode === "unchanged") state.unchangedSaved += saved;
67
- if (meta.mode === "diff") state.diffSaved += saved;
68
- if (!includedIds || includedIds.has(entry.id)) {
69
- counts[meta.mode] += 1;
70
- readCount += 1;
71
- }
72
- }
73
-
74
- if ((!includedIds || includedIds.has(entry.id)) && isAssistantMessage(entry.message)) {
75
- baselineTokens += state.baseline;
76
- returnedTokens += state.returned;
77
- const inputTokens =
78
- entry.message.usage.input + entry.message.usage.cacheRead + entry.message.usage.cacheWrite;
79
- const inputCost =
80
- entry.message.usage.cost.input +
81
- entry.message.usage.cost.cacheRead +
82
- entry.message.usage.cost.cacheWrite;
83
- const costPerToken = inputTokens > 0 ? inputCost / inputTokens : 0;
84
- unchangedCost += state.unchangedSaved * costPerToken;
85
- diffCost += state.diffSaved * costPerToken;
86
- costSaved += (state.baseline - state.returned) * costPerToken;
87
- }
88
- }
89
- states.set(entry.id, state);
90
- }
91
-
92
- return {
93
- label,
94
- secondary: `${readCount} read${readCount === 1 ? "" : "s"} ${label === "Current chat" ? "in this chat" : "across everything done in this session"}`,
95
- baselineTokens,
96
- returnedTokens,
97
- costSaved,
98
- unchangedCost,
99
- diffCost,
100
- counts,
101
- gate,
102
- };
103
- }
104
-
105
- function toolCallIds(entries: readonly unknown[], includedIds: ReadonlySet<string>): Set<string> {
106
- const ids = new Set<string>();
107
- for (const entry of entries) {
108
- if (!isRecord(entry) || typeof entry.id !== "string" || !includedIds.has(entry.id)) continue;
109
- if (entry.type !== "message" || !isRecord(entry.message)) continue;
110
- if (typeof entry.message.toolCallId === "string") ids.add(entry.message.toolCallId);
111
- }
112
- return ids;
113
- }
114
-
115
- function isAssistantMessage(value: unknown): value is {
116
- role: "assistant";
117
- usage: {
118
- input: number;
119
- cacheRead: number;
120
- cacheWrite: number;
121
- cost: { input: number; cacheRead: number; cacheWrite: number };
122
- };
123
- } {
124
- if (!isRecord(value) || value.role !== "assistant" || !isRecord(value.usage)) return false;
125
- if (!isRecord(value.usage.cost)) return false;
126
- return (
127
- typeof value.usage.input === "number" &&
128
- typeof value.usage.cacheRead === "number" &&
129
- typeof value.usage.cacheWrite === "number" &&
130
- typeof value.usage.cost.input === "number" &&
131
- typeof value.usage.cost.cacheRead === "number" &&
132
- typeof value.usage.cost.cacheWrite === "number"
133
- );
134
- }
135
-
136
- function isRecord(value: unknown): value is Record<string, unknown> {
137
- return typeof value === "object" && value !== null;
138
- }