@shanepadgett/tau-agent 0.33.1 → 0.34.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.
Files changed (190) hide show
  1. package/docs/context.md +29 -20
  2. package/docs/extending-tau-agent.md +63 -24
  3. package/extensions/appshot/index.ts +112 -111
  4. package/extensions/aside/README.md +7 -0
  5. package/extensions/aside/index.ts +168 -0
  6. package/extensions/aside/panel.ts +83 -0
  7. package/extensions/branch/index.ts +38 -21
  8. package/extensions/cache-diagnostics/index.ts +70 -58
  9. package/extensions/checkpoint/README.md +9 -0
  10. package/extensions/checkpoint/checkpoint-budget.ts +79 -0
  11. package/extensions/checkpoint/checkpoint.ts +277 -0
  12. package/extensions/checkpoint/index.ts +102 -0
  13. package/extensions/checkpoint/messages.ts +169 -0
  14. package/extensions/checkpoint/prompt.ts +24 -0
  15. package/extensions/checkpoint/settings.ts +27 -0
  16. package/extensions/commit/commit-effort.ts +3 -3
  17. package/extensions/context/README.md +12 -8
  18. package/extensions/context/definitions.ts +178 -55
  19. package/extensions/context/index.ts +192 -160
  20. package/extensions/context/panel.ts +57 -37
  21. package/extensions/context/sync.ts +139 -89
  22. package/extensions/context/validation.ts +37 -25
  23. package/extensions/effort/README.md +1 -1
  24. package/extensions/effort/index.ts +3 -3
  25. package/extensions/effort/state.ts +3 -18
  26. package/extensions/explore/guidance.ts +6 -47
  27. package/extensions/explore/index.ts +38 -53
  28. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  29. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  30. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  31. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  32. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  33. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  34. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  35. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  36. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  37. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  38. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  39. package/extensions/explore/tools/target-params.ts +9 -0
  40. package/extensions/footer/index.ts +75 -56
  41. package/extensions/handoff/index.ts +8 -30
  42. package/extensions/image-gen/index.ts +46 -40
  43. package/extensions/patch/executor.ts +190 -135
  44. package/extensions/patch/matcher.ts +82 -48
  45. package/extensions/patch/parser.ts +149 -105
  46. package/extensions/patch/render.ts +32 -39
  47. package/extensions/patch/summary.ts +30 -26
  48. package/extensions/ready/README.md +7 -0
  49. package/extensions/ready/index.ts +58 -0
  50. package/extensions/ready/model.ts +128 -0
  51. package/extensions/ready/packs.ts +272 -0
  52. package/extensions/ready/render.ts +445 -0
  53. package/extensions/ready/scan.ts +760 -0
  54. package/extensions/reference/panel.ts +28 -16
  55. package/extensions/review/index.ts +63 -51
  56. package/extensions/review/panel.ts +5 -34
  57. package/extensions/runtime-context/context.ts +30 -26
  58. package/extensions/script-runner/README.md +2 -2
  59. package/extensions/script-runner/index.ts +122 -82
  60. package/extensions/silent-command-runner/index.ts +38 -23
  61. package/extensions/subagent/agents/context-sync.md +185 -33
  62. package/extensions/subagent/agents/scout.md +2 -2
  63. package/extensions/subagent/agents.ts +137 -84
  64. package/extensions/subagent/cmux-dashboard.ts +39 -30
  65. package/extensions/subagent/index.ts +79 -59
  66. package/extensions/subagent/resume.ts +1 -1
  67. package/extensions/subagent/run.ts +304 -193
  68. package/extensions/subagent/runtime.ts +781 -343
  69. package/extensions/tau/index.ts +47 -38
  70. package/extensions/tau-help/help.md +14 -6
  71. package/extensions/tool-loader/README.md +4 -2
  72. package/extensions/tool-loader/index.ts +77 -52
  73. package/extensions/web/index.ts +6 -3
  74. package/extensions/web/webfetch.ts +114 -86
  75. package/package.json +3 -3
  76. package/schemas/tau.schema.json +20 -33
  77. package/shared/bounded-text-result.ts +1 -0
  78. package/shared/checkpoint-visibility.ts +9 -0
  79. package/shared/events.ts +28 -10
  80. package/shared/isolated-session.ts +71 -29
  81. package/shared/model-effort.ts +9 -19
  82. package/shared/model-fallback/index.ts +66 -41
  83. package/shared/settings/load.ts +45 -38
  84. package/shared/temporary-output-store.ts +16 -10
  85. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  86. package/{extensions/explore → src}/ast/engine.ts +54 -38
  87. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  88. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  89. package/src/ast/format/deps.ts +94 -0
  90. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  91. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  92. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  93. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  94. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  95. package/src/ast/format/targeted-result.ts +20 -0
  96. package/src/ast/grammars/c_sharp.wasm +0 -0
  97. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  98. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  99. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  100. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  101. package/{extensions/explore → src}/ast/ir.ts +6 -0
  102. package/src/ast/languages/csharp-file-deps.ts +90 -0
  103. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  104. package/src/ast/languages/dotted-index.ts +131 -0
  105. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  106. package/src/ast/languages/fixtures/sample.cs +53 -0
  107. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  108. package/src/ast/languages/fixtures/sample.go +37 -0
  109. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  110. package/src/ast/languages/fixtures/sample.java +40 -0
  111. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  112. package/src/ast/languages/fixtures/sample.kt +31 -0
  113. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  114. package/src/ast/languages/fixtures/sample.md +23 -0
  115. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  116. package/src/ast/languages/fixtures/sample.rs +54 -0
  117. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  118. package/src/ast/languages/fixtures/sample.swift +45 -0
  119. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  120. package/src/ast/languages/fixtures/sample.ts +45 -0
  121. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  122. package/src/ast/languages/fixtures/sample.tsx +36 -0
  123. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  124. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  125. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  126. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  127. package/src/ast/languages/jvm-file-deps.ts +135 -0
  128. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  129. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  130. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  131. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  132. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  133. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  134. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  135. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  136. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  137. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  138. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  139. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  140. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  141. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  142. package/src/ast/queries/impact.ts +291 -0
  143. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  144. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  145. package/{extensions/explore → src}/ast/query.ts +1 -1
  146. package/{extensions/explore → src}/ast/registry.ts +1 -2
  147. package/{extensions/explore → src}/ast/scan.ts +1 -1
  148. package/src/ast/session.ts +50 -0
  149. package/{extensions/explore → src}/ast/slice.ts +8 -0
  150. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  151. package/src/file-injection/index.ts +547 -0
  152. package/src/image-generation/client.ts +50 -31
  153. package/src/image-generation/index.ts +177 -134
  154. package/src/index.ts +2 -0
  155. package/src/tool-loading/index.ts +58 -0
  156. package/extensions/context/evidence.ts +0 -517
  157. package/extensions/context/projection.ts +0 -141
  158. package/extensions/context/state.ts +0 -30
  159. package/extensions/context/write-scope.ts +0 -109
  160. package/extensions/explore/ast/format/deps.ts +0 -86
  161. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  162. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  163. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  164. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  165. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  166. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  167. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  168. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  169. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  170. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  171. package/extensions/explore/ast/languages/odin.ts +0 -407
  172. package/extensions/explore/ast/queries/impact.ts +0 -250
  173. package/extensions/explore/outline-injection.ts +0 -151
  174. package/extensions/explore/read/autoread.ts +0 -118
  175. package/extensions/working-memory/README.md +0 -17
  176. package/extensions/working-memory/checkpoint.ts +0 -242
  177. package/extensions/working-memory/index.ts +0 -341
  178. package/extensions/working-memory/memory.ts +0 -235
  179. package/extensions/working-memory/render.ts +0 -185
  180. package/extensions/working-memory/settings.ts +0 -38
  181. package/extensions/working-memory/state.ts +0 -158
  182. package/shared/autoread.ts +0 -234
  183. package/shared/context-messages.ts +0 -19
  184. package/shared/outline-injection.ts +0 -56
  185. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  186. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  187. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  188. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  189. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  190. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -257,6 +257,25 @@ class ReferencePanel implements Component {
257
257
  ];
258
258
  }
259
259
 
260
+ private branchSuffix(branch: { isCurrent: boolean; updatedAt?: number }): string {
261
+ if (branch.isCurrent) return "current";
262
+ if (branch.updatedAt === undefined) return "remote";
263
+ return formatAge(branch.updatedAt);
264
+ }
265
+
266
+ private renderBranchRow(
267
+ branch: { name: string; isCurrent: boolean; updatedAt?: number },
268
+ active: boolean,
269
+ renderWidth: number,
270
+ ): string {
271
+ const pointer = active ? this.theme.fg("accent", "› ") : " ";
272
+ const labelText = `${branch.name} ${this.branchSuffix(branch)}`;
273
+ const label = active
274
+ ? this.theme.fg("accent", labelText)
275
+ : this.theme.fg(branch.isCurrent ? "accent" : "text", labelText);
276
+ return truncateToWidth(`${pointer}${label}`, renderWidth, "");
277
+ }
278
+
260
279
  private renderBranchPicker(width: number): string[] {
261
280
  const picker = this.branchPicker;
262
281
  if (!picker) return [];
@@ -279,18 +298,7 @@ class ReferencePanel implements Component {
279
298
  for (let index = window.start; index < window.end; index++) {
280
299
  const branch = picker.branches[index];
281
300
  if (!branch) continue;
282
- const active = index === picker.cursor;
283
- const pointer = active ? this.theme.fg("accent", "› ") : " ";
284
- const suffix = branch.isCurrent
285
- ? "current"
286
- : branch.updatedAt === undefined
287
- ? "remote"
288
- : formatAge(branch.updatedAt);
289
- const labelText = `${branch.name} ${suffix}`;
290
- const label = active
291
- ? this.theme.fg("accent", labelText)
292
- : this.theme.fg(branch.isCurrent ? "accent" : "text", labelText);
293
- lines.push(truncateToWidth(`${pointer}${label}`, renderWidth, ""));
301
+ lines.push(this.renderBranchRow(branch, index === picker.cursor, renderWidth));
294
302
  }
295
303
  if (window.start > 0 || window.end < picker.branches.length) {
296
304
  lines.push(
@@ -303,6 +311,12 @@ class ReferencePanel implements Component {
303
311
  return lines;
304
312
  }
305
313
 
314
+ private referenceStatusMark(state: NonNullable<ReferenceListItem["state"]>): string {
315
+ if (state === "failed") return this.theme.fg("error", "!");
316
+ if (state === "updated") return this.theme.fg("success", "✓");
317
+ return this.theme.fg("muted", "…");
318
+ }
319
+
306
320
  private renderReferenceRow(item: ReferenceListItem, active: boolean, width: number): string[] {
307
321
  const separator = item.displayName.lastIndexOf("/");
308
322
  const repository = separator < 0 ? item.displayName : item.displayName.slice(separator + 1);
@@ -310,9 +324,7 @@ class ReferencePanel implements Component {
310
324
  const name = item.dirty ? `${repository} *` : repository;
311
325
  const label = active ? this.theme.fg("accent", name) : this.theme.fg(item.dirty ? "warning" : "text", name);
312
326
  const branch = item.branch ? this.theme.fg("muted", ` (${item.branch})`) : "";
313
- const status = item.state
314
- ? ` ${this.theme.fg(item.state === "failed" ? "error" : item.state === "updated" ? "success" : "muted", item.state === "failed" ? "!" : item.state === "updated" ? "✓" : "…")}`
315
- : "";
327
+ const status = item.state ? ` ${this.referenceStatusMark(item.state)}` : "";
316
328
  return [truncateToWidth(`${label}${owner}${branch}${status}`, width, "")];
317
329
  }
318
330
 
@@ -871,7 +883,7 @@ function referenceNameFromUrl(url: string): string {
871
883
  return name;
872
884
  }
873
885
 
874
- export function referenceDisplayName(remoteUrl: string, fallback: string): string {
886
+ function referenceDisplayName(remoteUrl: string, fallback: string): string {
875
887
  const remote = remoteUrl
876
888
  .trim()
877
889
  .replace(/[?#].*$/, "")
@@ -26,30 +26,11 @@ export default function reviewExtension(pi: ExtensionAPI): void {
26
26
  await ctx.waitForIdle();
27
27
  const requested = args.trim().toLowerCase();
28
28
  if (requested === "show") {
29
- const latest = latestReview(ctx.sessionManager.getBranch());
30
- if (!latest) {
31
- ctx.ui.notify("No review exists on this session branch", "warning");
32
- return;
33
- }
34
- await showReview(pi, ctx, latest);
29
+ await showLatestReview(pi, ctx);
35
30
  return;
36
31
  }
37
-
38
- let mode: ReviewMode | undefined;
39
- if (requested) {
40
- if (!isReviewMode(requested)) {
41
- ctx.ui.notify("Usage: /review [simplify|architecture|correctness|show]", "warning");
42
- return;
43
- }
44
- mode = requested;
45
- } else {
46
- const selected = await ctx.ui.select("Review mode", ["Simplify", "Architecture", "Correctness"]);
47
- if (!selected) return;
48
- const normalized = selected.toLowerCase();
49
- if (!isReviewMode(normalized)) return;
50
- mode = normalized;
51
- }
52
-
32
+ const mode = await resolveReviewMode(requested, ctx);
33
+ if (!mode) return;
53
34
  const status = await loadRepoStatus(createGitRunner(pi, ctx));
54
35
  if (!status) {
55
36
  ctx.ui.notify("/review requires a Git repository", "warning");
@@ -59,41 +40,15 @@ export default function reviewExtension(pi: ExtensionAPI): void {
59
40
  ctx.ui.notify("Nothing to review: working tree is clean", "info");
60
41
  return;
61
42
  }
62
-
63
- const controller = new AbortController();
64
- const signal = ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal;
65
- let failure: string | undefined;
66
- const output = await ctx.ui.custom<ReviewRecord | undefined>((tui, theme, keys, done) => {
67
- const panel = new ReviewProgressPanel(tui, theme, mode, keys, () => controller.abort());
68
- void runReview({
69
- ctx,
70
- root: status.root,
71
- mode,
72
- parentThinkingLevel: pi.getThinkingLevel(),
73
- signal,
74
- onProgress: (line) => panel.update(line),
75
- })
76
- .then((result) => done({ ...result, mode, root: status.root, createdAt: new Date().toISOString() }))
77
- .catch((error: unknown) => {
78
- failure = errorText(error);
79
- done(undefined);
80
- });
81
- return panel;
82
- });
83
- if (!output) {
84
- ctx.ui.notify(
85
- signal.aborted ? "Review cancelled" : `Review failed: ${failure ?? "unknown error"}`,
86
- signal.aborted ? "info" : "error",
87
- );
88
- return;
89
- }
43
+ const output = await runReviewWithPanel(pi, ctx, mode, status.root);
44
+ if (!output) return;
90
45
  pi.appendEntry(REVIEW_ENTRY_TYPE, output);
91
46
  await showReview(pi, ctx, output);
92
47
  },
93
48
  });
94
49
  }
95
50
 
96
- export function latestReview(entries: readonly SessionEntry[]): ReviewRecord | undefined {
51
+ function latestReview(entries: readonly SessionEntry[]): ReviewRecord | undefined {
97
52
  for (let index = entries.length - 1; index >= 0; index -= 1) {
98
53
  const entry = entries[index];
99
54
  if (entry?.type !== "custom" || entry.customType !== REVIEW_ENTRY_TYPE) continue;
@@ -102,6 +57,63 @@ export function latestReview(entries: readonly SessionEntry[]): ReviewRecord | u
102
57
  return undefined;
103
58
  }
104
59
 
60
+ async function showLatestReview(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
61
+ const latest = latestReview(ctx.sessionManager.getBranch());
62
+ if (!latest) {
63
+ ctx.ui.notify("No review exists on this session branch", "warning");
64
+ return;
65
+ }
66
+ await showReview(pi, ctx, latest);
67
+ }
68
+
69
+ async function resolveReviewMode(requested: string, ctx: ExtensionContext): Promise<ReviewMode | undefined> {
70
+ if (requested) {
71
+ if (!isReviewMode(requested)) {
72
+ ctx.ui.notify("Usage: /review [simplify|architecture|correctness|show]", "warning");
73
+ return undefined;
74
+ }
75
+ return requested;
76
+ }
77
+ const selected = await ctx.ui.select("Review mode", ["Simplify", "Architecture", "Correctness"]);
78
+ if (!selected) return undefined;
79
+ const normalized = selected.toLowerCase();
80
+ return isReviewMode(normalized) ? normalized : undefined;
81
+ }
82
+
83
+ async function runReviewWithPanel(
84
+ pi: ExtensionAPI,
85
+ ctx: ExtensionContext,
86
+ mode: ReviewMode,
87
+ root: string,
88
+ ): Promise<ReviewRecord | undefined> {
89
+ const controller = new AbortController();
90
+ const signal = ctx.signal ? AbortSignal.any([controller.signal, ctx.signal]) : controller.signal;
91
+ let failure: string | undefined;
92
+ const output = await ctx.ui.custom<ReviewRecord | undefined>((tui, theme, keys, done) => {
93
+ const panel = new ReviewProgressPanel(tui, theme, mode, keys, () => controller.abort());
94
+ void runReview({
95
+ ctx,
96
+ root,
97
+ mode,
98
+ parentThinkingLevel: pi.getThinkingLevel(),
99
+ signal,
100
+ onProgress: (line) => panel.update(line),
101
+ })
102
+ .then((result) => done({ ...result, mode, root, createdAt: new Date().toISOString() }))
103
+ .catch((error: unknown) => {
104
+ failure = errorText(error);
105
+ done(undefined);
106
+ });
107
+ return panel;
108
+ });
109
+ if (output) return output;
110
+ ctx.ui.notify(
111
+ signal.aborted ? "Review cancelled" : `Review failed: ${failure ?? "unknown error"}`,
112
+ signal.aborted ? "info" : "error",
113
+ );
114
+ return undefined;
115
+ }
116
+
105
117
  async function showReview(pi: ExtensionAPI, ctx: ExtensionContext, review: ReviewRecord): Promise<void> {
106
118
  const action = await ctx.ui.custom<ReviewResultAction>(
107
119
  (tui, theme, keys, done) => new ReviewResultPanel(tui, theme, keys, review, done),
@@ -1,6 +1,6 @@
1
- import { bindingHint, bindingsHint, rawHint, ToolPanel } from "@shanepadgett/tau-tui";
2
- import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
3
- import { Markdown, type Component, type KeybindingsManager, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
1
+ import { bindingHint, bindingsHint, rawHint, ScrollableMarkdown, ToolPanel } from "@shanepadgett/tau-tui";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { type Component, type KeybindingsManager, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
4
4
  import { formatReviewMarkdown, reviewModeLabel, type ReviewMode, type ReviewRecord } from "./model.ts";
5
5
 
6
6
  export class ReviewProgressPanel implements Component {
@@ -53,7 +53,7 @@ export type ReviewResultAction = "send" | "export" | "close";
53
53
 
54
54
  export class ReviewResultPanel implements Component {
55
55
  private readonly panel: ToolPanel;
56
- private readonly body: ReviewMarkdownBody;
56
+ private readonly body: ScrollableMarkdown;
57
57
 
58
58
  constructor(
59
59
  tui: TUI,
@@ -62,7 +62,7 @@ export class ReviewResultPanel implements Component {
62
62
  review: ReviewRecord,
63
63
  done: (action: ReviewResultAction) => void,
64
64
  ) {
65
- this.body = new ReviewMarkdownBody(tui, formatReviewMarkdown(review));
65
+ this.body = new ScrollableMarkdown(tui, formatReviewMarkdown(review), 9);
66
66
  this.panel = new ToolPanel(theme, {
67
67
  title: `${reviewModeLabel(review.mode)} review · ${review.verdict}`,
68
68
  secondary: `${review.findings.length} finding${review.findings.length === 1 ? "" : "s"} · not in parent context`,
@@ -97,32 +97,3 @@ export class ReviewResultPanel implements Component {
97
97
  this.panel.invalidate();
98
98
  }
99
99
  }
100
-
101
- class ReviewMarkdownBody implements Component {
102
- private readonly tui: TUI;
103
- private readonly markdown: Markdown;
104
- private offset = 0;
105
- private maxOffset = 0;
106
-
107
- constructor(tui: TUI, content: string) {
108
- this.tui = tui;
109
- this.markdown = new Markdown(content, 0, 0, getMarkdownTheme());
110
- }
111
-
112
- scroll(delta: number): void {
113
- this.offset = Math.max(0, Math.min(this.maxOffset, this.offset + delta));
114
- this.tui.requestRender();
115
- }
116
-
117
- render(width: number): string[] {
118
- const lines = this.markdown.render(width);
119
- const height = Math.max(4, Math.floor(this.tui.terminal.rows * 0.9) - 9);
120
- this.maxOffset = Math.max(0, lines.length - height);
121
- this.offset = Math.min(this.offset, this.maxOffset);
122
- return lines.slice(this.offset, this.offset + height);
123
- }
124
-
125
- invalidate(): void {
126
- this.markdown.invalidate();
127
- }
128
- }
@@ -1,5 +1,4 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { createHash } from "node:crypto";
3
2
  import { type Dirent, readdirSync } from "node:fs";
4
3
  import { join, resolve } from "node:path";
5
4
 
@@ -19,13 +18,6 @@ export function freezeRuntimeContext(cwd: string): RuntimeContext {
19
18
  return { cwd: cwd.replace(/\\/g, "/"), rootSnapshot: listRootSnapshot(cwd) };
20
19
  }
21
20
 
22
- export function formatLocalDateKey(date: Date): string {
23
- const year = date.getFullYear();
24
- const month = String(date.getMonth() + 1).padStart(2, "0");
25
- const day = String(date.getDate()).padStart(2, "0");
26
- return `${year}-${month}-${day}`;
27
- }
28
-
29
21
  export function formatLocalDisplayDate(date: Date): string {
30
22
  const months = [
31
23
  "January",
@@ -44,12 +36,6 @@ export function formatLocalDisplayDate(date: Date): string {
44
36
  return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()}`;
45
37
  }
46
38
 
47
- export function fingerprintRuntimeSnapshot(context: RuntimeContext): string {
48
- return createHash("sha256")
49
- .update(JSON.stringify({ version: 1, cwd: context.cwd, paths: context.rootSnapshot }))
50
- .digest("hex");
51
- }
52
-
53
39
  export function formatRuntimeContextMessage(displayDate: string, rootSnapshot: readonly string[] | undefined): string {
54
40
  const blocks = [`Current local date: ${displayDate}`];
55
41
  if (rootSnapshot?.length) {
@@ -58,42 +44,60 @@ export function formatRuntimeContextMessage(displayDate: string, rootSnapshot: r
58
44
  return blocks.join("\n");
59
45
  }
60
46
 
61
- function listRootSnapshot(cwd: string): string[] {
62
- const root = resolve(cwd);
47
+ function visibleRootEntries(root: string): SnapshotEntry[] {
63
48
  const rootEntries = listSnapshotEntries(root, ".");
64
49
  const ignoredRootPaths = gitIgnoredPaths(
65
50
  root,
66
51
  rootEntries.map((entry) => entry.path),
67
52
  );
68
- const visibleRootEntries = rootEntries.filter(
53
+ return rootEntries.filter(
69
54
  (entry) => !isAlwaysHiddenFromSnapshot(entry.dirent.name) && !ignoredRootPaths.has(entry.path),
70
55
  );
71
- const childEntriesByParent = new Map<string, SnapshotEntry[]>();
72
- const childPaths: string[] = [];
56
+ }
73
57
 
74
- for (const entry of visibleRootEntries) {
58
+ function childEntriesByParent(
59
+ root: string,
60
+ roots: readonly SnapshotEntry[],
61
+ ): {
62
+ byParent: Map<string, SnapshotEntry[]>;
63
+ ignoredChildPaths: Set<string>;
64
+ } {
65
+ const byParent = new Map<string, SnapshotEntry[]>();
66
+ const childPaths: string[] = [];
67
+ for (const entry of roots) {
75
68
  if (!entry.dirent.isDirectory()) continue;
76
69
  const childEntries = listSnapshotEntries(root, entry.path).filter(
77
70
  (childEntry) => !isAlwaysHiddenFromSnapshot(childEntry.dirent.name),
78
71
  );
79
- childEntriesByParent.set(entry.path, childEntries);
72
+ byParent.set(entry.path, childEntries);
80
73
  childPaths.push(...childEntries.map((childEntry) => childEntry.path));
81
74
  }
75
+ return { byParent, ignoredChildPaths: gitIgnoredPaths(root, childPaths) };
76
+ }
82
77
 
83
- const ignoredChildPaths = gitIgnoredPaths(root, childPaths);
78
+ function collectSnapshotPaths(
79
+ roots: readonly SnapshotEntry[],
80
+ byParent: Map<string, SnapshotEntry[]>,
81
+ ignoredChildPaths: ReadonlySet<string>,
82
+ ): string[] {
84
83
  const paths: string[] = [];
85
-
86
- for (const entry of visibleRootEntries) {
84
+ for (const entry of roots) {
87
85
  pushSnapshotPath(paths, entry);
88
86
  if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
89
-
90
- for (const childEntry of childEntriesByParent.get(entry.path) ?? []) {
87
+ for (const childEntry of byParent.get(entry.path) ?? []) {
91
88
  if (ignoredChildPaths.has(childEntry.path)) continue;
92
89
  pushSnapshotPath(paths, childEntry);
93
90
  if (paths.length >= ROOT_SNAPSHOT_MAX_PATHS) break;
94
91
  }
95
92
  }
93
+ return paths;
94
+ }
96
95
 
96
+ function listRootSnapshot(cwd: string): string[] {
97
+ const root = resolve(cwd);
98
+ const roots = visibleRootEntries(root);
99
+ const children = childEntriesByParent(root, roots);
100
+ const paths = collectSnapshotPaths(roots, children.byParent, children.ignoredChildPaths);
97
101
  return paths.length === ROOT_SNAPSHOT_MAX_PATHS ? [...paths, "..."] : paths;
98
102
  }
99
103
 
@@ -1,7 +1,7 @@
1
1
  # Script Runner
2
2
 
3
- Gives the agent a first-class `script_runner` tool for running Python 3 and TypeScript instead of falling back to bash. The agent picks whichever language is more efficient for the task.
3
+ Gives the agent a first-class `script_runner` tool for running Python 3, Node.js, and Deno scripts instead of falling back to bash. The agent picks whichever runtime is more efficient for the task.
4
4
 
5
5
  When a run fails, the tool keeps the script and returns a `scriptId`. The agent retries with targeted `{oldText, newText}` edits against what it just wrote instead of resending the whole script, saving output tokens and keeping duplicate scripts out of context. Only the source the agent already sent is referenced; no file path is exposed.
6
6
 
7
- Languages are detected from the environment: Python 3 via `python3`, TypeScript via Node with `--experimental-strip-types` (Node 22.6 or newer). The tool registers only the languages actually available and is hidden from the prompt entirely when neither is present.
7
+ Runtimes are detected from the environment: Python 3 via `python3`, Node via the current process when Node is 22.6 or newer (`node --experimental-strip-types`), Deno via `deno` (`deno run -A`). The `node` language is the local Node.js runtime with full Node APIs; scripts may be TypeScript with erasable syntax or plain JavaScript. The `deno` language is the local Deno runtime with full permissions and native TypeScript/JavaScript. The tool registers only the runtimes actually available and is hidden from the prompt entirely when none are present.