@shanepadgett/tau-agent 0.33.1 → 0.35.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 (203) 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/bash-approval/README.md +20 -0
  8. package/extensions/bash-approval/index.ts +235 -0
  9. package/extensions/bash-approval/settings.ts +24 -0
  10. package/extensions/branch/index.ts +38 -21
  11. package/extensions/cache-diagnostics/index.ts +70 -58
  12. package/extensions/checkpoint/README.md +9 -0
  13. package/extensions/checkpoint/checkpoint-budget.ts +66 -0
  14. package/extensions/checkpoint/checkpoint.ts +279 -0
  15. package/extensions/checkpoint/index.ts +102 -0
  16. package/extensions/checkpoint/messages.ts +170 -0
  17. package/extensions/checkpoint/prompt.ts +24 -0
  18. package/extensions/checkpoint/settings.ts +27 -0
  19. package/extensions/commit/commit-effort.ts +3 -3
  20. package/extensions/commit/commit-plan.ts +2 -2
  21. package/extensions/context/README.md +12 -8
  22. package/extensions/context/definitions.ts +178 -55
  23. package/extensions/context/index.ts +192 -160
  24. package/extensions/context/panel.ts +57 -37
  25. package/extensions/context/sync.ts +139 -89
  26. package/extensions/context/validation.ts +37 -25
  27. package/extensions/effort/README.md +1 -1
  28. package/extensions/effort/index.ts +3 -3
  29. package/extensions/effort/state.ts +3 -18
  30. package/extensions/explore/guidance.ts +6 -47
  31. package/extensions/explore/index.ts +38 -53
  32. package/extensions/explore/{ast/read → read}/hook.ts +12 -7
  33. package/extensions/explore/{ast/tools → tools}/ast-search.ts +15 -19
  34. package/extensions/explore/{ast/tools → tools}/context.ts +16 -23
  35. package/extensions/explore/{ast/tools → tools}/deps.ts +11 -15
  36. package/extensions/explore/{ast/tools → tools}/discover.ts +15 -17
  37. package/extensions/explore/{ast/tools → tools}/impact.ts +20 -23
  38. package/extensions/explore/{ast/tools → tools}/outline.ts +21 -24
  39. package/extensions/explore/{ast/tools → tools}/relationships.ts +23 -32
  40. package/extensions/explore/{ast/tools → tools}/render.ts +1 -1
  41. package/extensions/explore/{ast/tools → tools}/reverse-deps.ts +11 -15
  42. package/extensions/explore/{ast/tools → tools}/show.ts +16 -19
  43. package/extensions/explore/tools/target-params.ts +9 -0
  44. package/extensions/footer/index.ts +75 -56
  45. package/extensions/handoff/index.ts +8 -30
  46. package/extensions/image-gen/index.ts +46 -40
  47. package/extensions/patch/executor.ts +190 -135
  48. package/extensions/patch/matcher.ts +82 -48
  49. package/extensions/patch/parser.ts +149 -105
  50. package/extensions/patch/render.ts +32 -39
  51. package/extensions/patch/summary.ts +30 -26
  52. package/extensions/ready/README.md +7 -0
  53. package/extensions/ready/index.ts +58 -0
  54. package/extensions/ready/model.ts +128 -0
  55. package/extensions/ready/packs.ts +272 -0
  56. package/extensions/ready/render.ts +445 -0
  57. package/extensions/ready/scan.ts +760 -0
  58. package/extensions/reference/index.ts +2 -0
  59. package/extensions/reference/panel.ts +94 -31
  60. package/extensions/review/README.md +14 -6
  61. package/extensions/review/index.ts +58 -99
  62. package/extensions/review/model.ts +13 -33
  63. package/extensions/review/session.ts +9 -23
  64. package/extensions/runtime-context/context.ts +30 -26
  65. package/extensions/script-runner/README.md +2 -2
  66. package/extensions/script-runner/index.ts +122 -82
  67. package/extensions/silent-command-runner/index.ts +38 -23
  68. package/extensions/soul/README.md +2 -2
  69. package/extensions/soul/index.ts +4 -4
  70. package/extensions/soul/prompt.ts +6 -10
  71. package/extensions/soul/settings.ts +6 -3
  72. package/extensions/subagent/agents/context-sync.md +185 -33
  73. package/extensions/subagent/agents/scout.md +2 -2
  74. package/extensions/subagent/agents/web-research.md +2 -2
  75. package/extensions/subagent/agents.ts +137 -84
  76. package/extensions/subagent/cmux-dashboard.ts +39 -30
  77. package/extensions/subagent/index.ts +79 -59
  78. package/extensions/subagent/resume.ts +1 -1
  79. package/extensions/subagent/run.ts +308 -193
  80. package/extensions/subagent/runtime.ts +781 -343
  81. package/extensions/tau/index.ts +47 -38
  82. package/extensions/tau-help/help.md +20 -8
  83. package/extensions/tool-loader/README.md +4 -2
  84. package/extensions/tool-loader/index.ts +77 -52
  85. package/extensions/web/index.ts +6 -3
  86. package/extensions/web/webfetch.ts +114 -86
  87. package/package.json +3 -3
  88. package/schemas/tau.schema.json +38 -35
  89. package/shared/bounded-text-result.ts +1 -0
  90. package/shared/checkpoint-visibility.ts +9 -0
  91. package/shared/events.ts +28 -10
  92. package/shared/isolated-session.ts +72 -30
  93. package/shared/model-effort.ts +23 -22
  94. package/shared/model-fallback/index.ts +66 -41
  95. package/shared/settings/load.ts +45 -38
  96. package/shared/temporary-output-store.ts +16 -10
  97. package/{extensions/explore → src}/ast/adapter.ts +11 -2
  98. package/{extensions/explore → src}/ast/engine.ts +54 -38
  99. package/{extensions/explore → src}/ast/format/ast-search.ts +15 -6
  100. package/{extensions/explore → src}/ast/format/context.ts +3 -6
  101. package/src/ast/format/deps.ts +94 -0
  102. package/{extensions/explore → src}/ast/format/discover.ts +2 -2
  103. package/{extensions/explore → src}/ast/format/impact.ts +9 -11
  104. package/{extensions/explore → src}/ast/format/outline.ts +2 -2
  105. package/{extensions/explore → src}/ast/format/relationships.ts +11 -7
  106. package/{extensions/explore → src}/ast/format/show.ts +1 -1
  107. package/src/ast/format/targeted-result.ts +20 -0
  108. package/src/ast/grammars/c_sharp.wasm +0 -0
  109. package/{extensions/explore → src}/ast/grammars/manifest.json +9 -12
  110. package/{extensions/explore → src}/ast/grammars/manifest.ts +0 -4
  111. package/{extensions/explore → src}/ast/graph/file-graph.ts +210 -98
  112. package/{extensions/explore → src}/ast/graph/relationships.ts +285 -72
  113. package/{extensions/explore → src}/ast/ir.ts +6 -0
  114. package/src/ast/languages/csharp-file-deps.ts +90 -0
  115. package/{extensions/explore → src}/ast/languages/csharp.ts +9 -40
  116. package/src/ast/languages/dotted-index.ts +131 -0
  117. package/{extensions/explore → src}/ast/languages/file-dep-util.ts +22 -13
  118. package/src/ast/languages/fixtures/sample.cs +53 -0
  119. package/src/ast/languages/fixtures/sample.cs.expected.json +156 -0
  120. package/src/ast/languages/fixtures/sample.go +37 -0
  121. package/src/ast/languages/fixtures/sample.go.expected.json +96 -0
  122. package/src/ast/languages/fixtures/sample.java +40 -0
  123. package/src/ast/languages/fixtures/sample.java.expected.json +126 -0
  124. package/src/ast/languages/fixtures/sample.kt +31 -0
  125. package/src/ast/languages/fixtures/sample.kt.expected.json +134 -0
  126. package/src/ast/languages/fixtures/sample.md +23 -0
  127. package/src/ast/languages/fixtures/sample.md.expected.json +44 -0
  128. package/src/ast/languages/fixtures/sample.rs +54 -0
  129. package/src/ast/languages/fixtures/sample.rs.expected.json +178 -0
  130. package/src/ast/languages/fixtures/sample.swift +45 -0
  131. package/src/ast/languages/fixtures/sample.swift.expected.json +147 -0
  132. package/src/ast/languages/fixtures/sample.ts +45 -0
  133. package/src/ast/languages/fixtures/sample.ts.expected.json +141 -0
  134. package/src/ast/languages/fixtures/sample.tsx +36 -0
  135. package/src/ast/languages/fixtures/sample.tsx.expected.json +59 -0
  136. package/{extensions/explore → src}/ast/languages/go-file-deps.ts +6 -0
  137. package/{extensions/explore → src}/ast/languages/go.ts +21 -3
  138. package/{extensions/explore → src}/ast/languages/java.ts +5 -38
  139. package/src/ast/languages/jvm-file-deps.ts +135 -0
  140. package/{extensions/explore → src}/ast/languages/kotlin.ts +5 -39
  141. package/{extensions/explore → src}/ast/languages/rust-file-deps.ts +3 -3
  142. package/{extensions/explore → src}/ast/languages/rust.ts +21 -13
  143. package/{extensions/explore → src}/ast/languages/swift-file-deps.ts +22 -15
  144. package/{extensions/explore → src}/ast/languages/swift.ts +9 -39
  145. package/{extensions/explore → src}/ast/languages/tree.ts +47 -0
  146. package/{extensions/explore → src}/ast/languages/typescript-file-deps.ts +108 -0
  147. package/{extensions/explore → src}/ast/languages/typescript-package-surface.ts +4 -3
  148. package/{extensions/explore → src}/ast/languages/typescript.ts +3 -5
  149. package/{extensions/explore → src}/ast/markdown.ts +72 -47
  150. package/{extensions/explore → src}/ast/queries/ast-search.ts +164 -146
  151. package/{extensions/explore → src}/ast/queries/composite-target.ts +1 -1
  152. package/{extensions/explore → src}/ast/queries/context.ts +116 -80
  153. package/{extensions/explore → src}/ast/queries/discover.ts +154 -105
  154. package/src/ast/queries/impact.ts +291 -0
  155. package/{extensions/explore → src}/ast/queries/outline.ts +4 -2
  156. package/{extensions/explore → src}/ast/queries/show.ts +46 -42
  157. package/{extensions/explore → src}/ast/query.ts +1 -1
  158. package/{extensions/explore → src}/ast/registry.ts +1 -2
  159. package/{extensions/explore → src}/ast/scan.ts +1 -1
  160. package/src/ast/session.ts +50 -0
  161. package/{extensions/explore → src}/ast/slice.ts +8 -0
  162. package/{extensions/explore → src/ast}/traverse.ts +32 -20
  163. package/src/file-injection/index.ts +547 -0
  164. package/src/image-generation/client.ts +50 -31
  165. package/src/image-generation/index.ts +177 -134
  166. package/src/index.ts +2 -0
  167. package/src/tool-loading/index.ts +58 -0
  168. package/extensions/context/evidence.ts +0 -517
  169. package/extensions/context/projection.ts +0 -141
  170. package/extensions/context/state.ts +0 -30
  171. package/extensions/context/write-scope.ts +0 -109
  172. package/extensions/explore/ast/format/deps.ts +0 -86
  173. package/extensions/explore/ast/grammars/odin.wasm +0 -0
  174. package/extensions/explore/ast/languages/csharp-file-deps.ts +0 -189
  175. package/extensions/explore/ast/languages/fixtures/sample.cs +0 -17
  176. package/extensions/explore/ast/languages/fixtures/sample.java +0 -21
  177. package/extensions/explore/ast/languages/fixtures/sample.kt +0 -15
  178. package/extensions/explore/ast/languages/fixtures/sample.odin +0 -28
  179. package/extensions/explore/ast/languages/fixtures/sample.rs +0 -31
  180. package/extensions/explore/ast/languages/fixtures/sample.swift +0 -25
  181. package/extensions/explore/ast/languages/jvm-file-deps.ts +0 -217
  182. package/extensions/explore/ast/languages/odin-file-deps.ts +0 -175
  183. package/extensions/explore/ast/languages/odin.ts +0 -407
  184. package/extensions/explore/ast/queries/impact.ts +0 -250
  185. package/extensions/explore/outline-injection.ts +0 -151
  186. package/extensions/explore/read/autoread.ts +0 -118
  187. package/extensions/review/panel.ts +0 -128
  188. package/extensions/working-memory/README.md +0 -17
  189. package/extensions/working-memory/checkpoint.ts +0 -242
  190. package/extensions/working-memory/index.ts +0 -341
  191. package/extensions/working-memory/memory.ts +0 -235
  192. package/extensions/working-memory/render.ts +0 -185
  193. package/extensions/working-memory/settings.ts +0 -38
  194. package/extensions/working-memory/state.ts +0 -158
  195. package/shared/autoread.ts +0 -234
  196. package/shared/context-messages.ts +0 -19
  197. package/shared/outline-injection.ts +0 -56
  198. /package/{extensions/explore → src}/ast/format/composite.ts +0 -0
  199. /package/{extensions/explore → src}/ast/grammars/kotlin.wasm +0 -0
  200. /package/{extensions/explore → src}/ast/grammars/swift.wasm +0 -0
  201. /package/{extensions/explore → src}/ast/identity.ts +0 -0
  202. /package/{extensions/explore → src}/ast/package-surface.ts +0 -0
  203. /package/{extensions/explore/ast/read/policy.ts → src/ast/read-policy.ts} +0 -0
@@ -55,6 +55,8 @@ export default function referenceExtension(pi: ExtensionAPI): void {
55
55
  if (!references) return;
56
56
 
57
57
  ctx.ui.setEditorText(buildReferenceDraft(references));
58
+ // Pi does not request a render when setEditorText runs after custom UI closes.
59
+ ctx.ui.setStatus("reference", undefined);
58
60
  },
59
61
  });
60
62
  }
@@ -34,7 +34,12 @@ export interface ReferenceItem {
34
34
  }
35
35
 
36
36
  interface ReferenceListItem extends ReferenceItem, SelectableListItem {
37
- state?: "updating" | "updated" | "failed" | "switching";
37
+ state?: "loading" | "updating" | "updated" | "failed" | "switching";
38
+ }
39
+
40
+ interface ReferenceLocation {
41
+ name: string;
42
+ path: string;
38
43
  }
39
44
 
40
45
  interface CloneProgress {
@@ -71,9 +76,9 @@ export async function showReferencePanel(
71
76
  editor: ReferenceEditor,
72
77
  branchChoices: number,
73
78
  ): Promise<ReferenceItem[] | undefined> {
74
- let initial: ReferenceItem[];
79
+ let initial: ReferenceLocation[];
75
80
  try {
76
- initial = await loadReferences(git);
81
+ initial = await loadReferenceLocations();
77
82
  } catch (error) {
78
83
  ctx.ui.notify(`Reference load failed: ${errorText(error)}`, "error");
79
84
  initial = [];
@@ -134,7 +139,7 @@ class ReferencePanel implements Component {
134
139
  ctx: ExtensionCommandContext,
135
140
  editor: ReferenceEditor,
136
141
  branchChoices: number,
137
- initial: readonly ReferenceItem[],
142
+ initial: readonly ReferenceLocation[],
138
143
  done: (result: ReferenceItem[] | undefined) => void,
139
144
  ) {
140
145
  this.tui = tui;
@@ -144,7 +149,9 @@ class ReferencePanel implements Component {
144
149
  this.editor = editor;
145
150
  this.branchChoices = branchChoices;
146
151
  this.done = done;
147
- this.refs = initial.map((item) => toListItem(item));
152
+ this.refs = initial.map((item) =>
153
+ toListItem({ ...item, displayName: item.name, dirty: false, branch: "" }, "loading"),
154
+ );
148
155
  this.list = this.createList(this.refs);
149
156
  this.body = {
150
157
  render: (width) => this.renderBody(width),
@@ -158,6 +165,7 @@ class ReferencePanel implements Component {
158
165
  footer: { kind: "hints", hints: this.footerHints() },
159
166
  };
160
167
  this.panel = new ToolPanel(theme, this.panelConfig);
168
+ void this.hydrateInitialReferences(initial);
161
169
  }
162
170
 
163
171
  render(width: number): string[] {
@@ -257,6 +265,25 @@ class ReferencePanel implements Component {
257
265
  ];
258
266
  }
259
267
 
268
+ private branchSuffix(branch: { isCurrent: boolean; updatedAt?: number }): string {
269
+ if (branch.isCurrent) return "current";
270
+ if (branch.updatedAt === undefined) return "remote";
271
+ return formatAge(branch.updatedAt);
272
+ }
273
+
274
+ private renderBranchRow(
275
+ branch: { name: string; isCurrent: boolean; updatedAt?: number },
276
+ active: boolean,
277
+ renderWidth: number,
278
+ ): string {
279
+ const pointer = active ? this.theme.fg("accent", "› ") : " ";
280
+ const labelText = `${branch.name} ${this.branchSuffix(branch)}`;
281
+ const label = active
282
+ ? this.theme.fg("accent", labelText)
283
+ : this.theme.fg(branch.isCurrent ? "accent" : "text", labelText);
284
+ return truncateToWidth(`${pointer}${label}`, renderWidth, "");
285
+ }
286
+
260
287
  private renderBranchPicker(width: number): string[] {
261
288
  const picker = this.branchPicker;
262
289
  if (!picker) return [];
@@ -279,18 +306,7 @@ class ReferencePanel implements Component {
279
306
  for (let index = window.start; index < window.end; index++) {
280
307
  const branch = picker.branches[index];
281
308
  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, ""));
309
+ lines.push(this.renderBranchRow(branch, index === picker.cursor, renderWidth));
294
310
  }
295
311
  if (window.start > 0 || window.end < picker.branches.length) {
296
312
  lines.push(
@@ -303,6 +319,12 @@ class ReferencePanel implements Component {
303
319
  return lines;
304
320
  }
305
321
 
322
+ private referenceStatusMark(state: NonNullable<ReferenceListItem["state"]>): string {
323
+ if (state === "failed") return this.theme.fg("error", "!");
324
+ if (state === "updated") return this.theme.fg("success", "✓");
325
+ return this.theme.fg("muted", "…");
326
+ }
327
+
306
328
  private renderReferenceRow(item: ReferenceListItem, active: boolean, width: number): string[] {
307
329
  const separator = item.displayName.lastIndexOf("/");
308
330
  const repository = separator < 0 ? item.displayName : item.displayName.slice(separator + 1);
@@ -310,9 +332,7 @@ class ReferencePanel implements Component {
310
332
  const name = item.dirty ? `${repository} *` : repository;
311
333
  const label = active ? this.theme.fg("accent", name) : this.theme.fg(item.dirty ? "warning" : "text", name);
312
334
  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
- : "";
335
+ const status = item.state ? ` ${this.referenceStatusMark(item.state)}` : "";
316
336
  return [truncateToWidth(`${label}${owner}${branch}${status}`, width, "")];
317
337
  }
318
338
 
@@ -584,6 +604,38 @@ class ReferencePanel implements Component {
584
604
  this.syncPanel();
585
605
  }
586
606
 
607
+ private async hydrateInitialReferences(locations: readonly ReferenceLocation[]): Promise<void> {
608
+ if (locations.length === 0) return;
609
+ const results = await Promise.all(
610
+ locations.map(async (location) => {
611
+ try {
612
+ return {
613
+ ok: true as const,
614
+ location,
615
+ item: await loadReference(this.git, location.name, location.path),
616
+ };
617
+ } catch (error) {
618
+ return { ok: false as const, location, error };
619
+ }
620
+ }),
621
+ );
622
+ const loaded = new Map<string, ReferenceItem>();
623
+ const failures: string[] = [];
624
+ for (const result of results) {
625
+ if (result.ok) loaded.set(result.location.path, result.item);
626
+ else failures.push(`${result.location.name}: ${errorText(result.error)}`);
627
+ }
628
+
629
+ this.refs = this.refs.map((item) => {
630
+ if (item.state !== "loading") return item;
631
+ const metadata = loaded.get(item.path);
632
+ return metadata ? toListItem(metadata) : { ...item, state: "failed" };
633
+ });
634
+ this.list.setItems(this.refs);
635
+ this.syncPanel();
636
+ if (failures.length > 0) this.ctx.ui.notify(`Reference metadata load failed:\n${failures.join("\n")}`, "error");
637
+ }
638
+
587
639
  private syncPanel(): void {
588
640
  this.panelConfig.secondary = this.secondaryText();
589
641
  this.panelConfig.header = this.headerLines();
@@ -736,28 +788,39 @@ function compareReferenceItems(left: ReferenceListItem, right: ReferenceListItem
736
788
  return left.name.localeCompare(right.name);
737
789
  }
738
790
 
739
- async function loadReferences(git: GitRunner): Promise<ReferenceItem[]> {
791
+ async function loadReferenceLocations(): Promise<ReferenceLocation[]> {
740
792
  await mkdir(REFERENCES_DIR, { recursive: true });
741
- const refs = (await readdir(REFERENCES_DIR, { withFileTypes: true }))
793
+ return (await readdir(REFERENCES_DIR, { withFileTypes: true }))
742
794
  .filter((entry) => entry.isDirectory() && !entry.name.startsWith(".clone-"))
743
795
  .map((entry) => ({ name: entry.name, path: join(REFERENCES_DIR, entry.name) }))
744
796
  .sort((left, right) => left.name.localeCompare(right.name));
797
+ }
745
798
 
746
- const references: ReferenceItem[] = [];
747
- for (const ref of refs) references.push(await loadReference(git, ref.name, ref.path));
748
- return references;
799
+ async function loadReferences(git: GitRunner): Promise<ReferenceItem[]> {
800
+ const refs = await loadReferenceLocations();
801
+ return Promise.all(refs.map((ref) => loadReference(git, ref.name, ref.path)));
749
802
  }
750
803
 
751
804
  async function loadReference(git: GitRunner, name: string, path: string): Promise<ReferenceItem> {
752
- const branch = await git.run(["branch", "--show-current"], { cwd: path, optional: true });
753
- const commit = branch ? "" : await git.run(["rev-parse", "--short", "HEAD"], { cwd: path, optional: true });
754
- const remoteUrl = await git.run(["config", "--get", "remote.origin.url"], { cwd: path, optional: true });
805
+ const [status, remoteUrl] = await Promise.all([
806
+ git.run(["status", "--porcelain=v2", "--branch"], { cwd: path, optional: true }),
807
+ git.run(["config", "--get", "remote.origin.url"], { cwd: path, optional: true }),
808
+ ]);
809
+ let branch = "";
810
+ let commit = "";
811
+ let dirty = false;
812
+ for (const line of status.split("\n")) {
813
+ if (line.startsWith("# branch.head ")) branch = line.slice("# branch.head ".length);
814
+ else if (line.startsWith("# branch.oid ")) commit = line.slice("# branch.oid ".length).slice(0, 7);
815
+ else if (line && !line.startsWith("# ")) dirty = true;
816
+ }
817
+ if (branch === "(detached)") branch = commit ? `detached ${commit}` : "";
755
818
  return {
756
819
  name,
757
820
  displayName: referenceDisplayName(remoteUrl, name),
758
821
  path,
759
- dirty: (await git.run(["status", "--porcelain=v1"], { cwd: path, optional: true })).length > 0,
760
- branch: branch || (commit ? `detached ${commit}` : ""),
822
+ dirty,
823
+ branch,
761
824
  };
762
825
  }
763
826
 
@@ -871,7 +934,7 @@ function referenceNameFromUrl(url: string): string {
871
934
  return name;
872
935
  }
873
936
 
874
- export function referenceDisplayName(remoteUrl: string, fallback: string): string {
937
+ function referenceDisplayName(remoteUrl: string, fallback: string): string {
875
938
  const remote = remoteUrl
876
939
  .trim()
877
940
  .replace(/[?#].*$/, "")
@@ -1,11 +1,19 @@
1
1
  # Review
2
2
 
3
- Run `/review` to inspect current repository's staged, unstaged, and untracked work in a fresh isolated session. Choose one focused pass:
3
+ Run `/review` without arguments to inspect the current repository's staged, unstaged, and untracked work in a fresh isolated session. This form stops when the working tree is clean.
4
4
 
5
- - `simplify` looks for code and concepts that can disappear or reuse what already exists.
6
- - `architecture` performs a nuclear maintainability review and accepts substantial redesign when ownership, reuse, or structure is poor.
7
- - `correctness` checks runtime bugs and failures after architecture is accepted.
5
+ Add review direction after the command to review the requested part of the repository instead, regardless of whether the working tree has changes:
8
6
 
9
- Run a mode directly with `/review simplify`, `/review architecture`, or `/review correctness`. Run `/review show` to reopen latest result on current session branch.
7
+ ```text
8
+ /review focus on cancellation, cleanup, and data loss
9
+ ```
10
10
 
11
- Results stay outside parent agent context. In result view, press `a` to send review to agent's next turn or `e` to export Markdown under `.pi/tau/reviews/`.
11
+ Choose one focused review type:
12
+
13
+ - `Simplify` looks for code and concepts that can disappear or reuse what already exists.
14
+ - `Architecture` reconsiders ownership, boundaries, reuse, and overall structure.
15
+ - `Correctness` checks concrete runtime bugs and failure paths after accepting the architecture.
16
+
17
+ Then choose which logged-in provider runs the review. OpenAI Codex uses `gpt-5.6-sol` and Anthropic uses `claude-opus-5`, both at high thinking. Only providers you are logged in to appear. With no logged-in provider, the review uses the current model.
18
+
19
+ Tau writes each result as Markdown under `.pi/tau/reviews/`. Review results do not enter the parent agent context. Reference the Markdown file later when you want an agent to use it.
@@ -1,135 +1,94 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
5
- import { createInjectedContext } from "../../shared/injected-context.ts";
5
+ import { resolveEffortProviders } from "../../shared/model-effort.ts";
6
6
  import { errorText } from "../../shared/text.ts";
7
- import {
8
- formatReviewMarkdown,
9
- isReviewRecord,
10
- isReviewMode,
11
- REVIEW_ENTRY_TYPE,
12
- type ReviewMode,
13
- type ReviewRecord,
14
- } from "./model.ts";
15
- import { ReviewProgressPanel, ReviewResultPanel, type ReviewResultAction } from "./panel.ts";
7
+ import { formatReviewMarkdown, type ReviewMode } from "./model.ts";
16
8
  import { runReview } from "./session.ts";
17
9
 
10
+ const REVIEW_TYPES = [
11
+ { label: "Simplify", mode: "simplify" },
12
+ { label: "Architecture", mode: "architecture" },
13
+ { label: "Correctness", mode: "correctness" },
14
+ ] as const satisfies ReadonlyArray<{ label: string; mode: ReviewMode }>;
15
+
18
16
  export default function reviewExtension(pi: ExtensionAPI): void {
19
17
  pi.registerCommand("review", {
20
- description: "Run or show an isolated simplify, architecture, or correctness review",
18
+ description: "Write an isolated requested or Git-change review to Markdown",
21
19
  handler: async (args, ctx) => {
22
20
  if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
23
21
  ctx.ui.notify("/review requires a trusted TUI project", "warning");
24
22
  return;
25
23
  }
26
24
  await ctx.waitForIdle();
27
- const requested = args.trim().toLowerCase();
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);
35
- return;
36
- }
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
-
25
+ const direction = args.trim();
53
26
  const status = await loadRepoStatus(createGitRunner(pi, ctx));
54
27
  if (!status) {
55
28
  ctx.ui.notify("/review requires a Git repository", "warning");
56
29
  return;
57
30
  }
58
- if (status.fileCount === 0) {
31
+ if (!direction && status.fileCount === 0) {
59
32
  ctx.ui.notify("Nothing to review: working tree is clean", "info");
60
33
  return;
61
34
  }
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({
35
+ const selectedType = await ctx.ui.select(
36
+ "Review type",
37
+ REVIEW_TYPES.map(({ label }) => label),
38
+ );
39
+ if (!selectedType) return;
40
+ const mode = REVIEW_TYPES.find(({ label }) => label === selectedType)?.mode;
41
+ if (!mode) return;
42
+ const providers = resolveEffortProviders(ctx, "deep");
43
+ let preferred: ReviewModelChoice | undefined;
44
+ if (providers.length > 0) {
45
+ const labels = providers.map((provider) => `${provider.label} · ${provider.candidates[0]?.model.id ?? ""}`);
46
+ const selected = await ctx.ui.select("Review provider", labels);
47
+ if (!selected) return;
48
+ const provider = providers[labels.indexOf(selected)];
49
+ const candidate = provider?.candidates[0];
50
+ if (provider && candidate) {
51
+ preferred = { model: `${provider.provider}/${candidate.model.id}`, thinkingLevel: candidate.reasoning };
52
+ }
53
+ } else {
54
+ ctx.ui.notify("No logged-in review provider. Using current model.", "warning");
55
+ }
56
+ const signal = ctx.signal ?? new AbortController().signal;
57
+ ctx.ui.setStatus("review", `running ${mode} review`);
58
+ try {
59
+ const output = await runReview({
69
60
  ctx,
70
61
  root: status.root,
71
62
  mode,
63
+ direction,
64
+ preferredModel: preferred?.model,
65
+ preferredThinkingLevel: preferred?.thinkingLevel,
72
66
  parentThinkingLevel: pi.getThinkingLevel(),
73
67
  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) {
68
+ });
69
+ const createdAt = new Date().toISOString();
70
+ const directory = join(status.root, ".pi", "tau", "reviews");
71
+ const timestamp = createdAt.replace(/[^0-9A-Za-z-]/g, "-");
72
+ const path = join(directory, `${timestamp}-${mode}-review.md`);
73
+ await mkdir(directory, { recursive: true });
74
+ await writeFile(path, formatReviewMarkdown({ ...output, mode, direction, createdAt }), {
75
+ encoding: "utf8",
76
+ mode: 0o600,
77
+ });
78
+ ctx.ui.notify(`Review written to ${path}`, "info");
79
+ } catch (error) {
84
80
  ctx.ui.notify(
85
- signal.aborted ? "Review cancelled" : `Review failed: ${failure ?? "unknown error"}`,
81
+ signal.aborted ? "Review cancelled" : `Review failed: ${errorText(error)}`,
86
82
  signal.aborted ? "info" : "error",
87
83
  );
88
- return;
84
+ } finally {
85
+ ctx.ui.setStatus("review", undefined);
89
86
  }
90
- pi.appendEntry(REVIEW_ENTRY_TYPE, output);
91
- await showReview(pi, ctx, output);
92
87
  },
93
88
  });
94
89
  }
95
90
 
96
- export function latestReview(entries: readonly SessionEntry[]): ReviewRecord | undefined {
97
- for (let index = entries.length - 1; index >= 0; index -= 1) {
98
- const entry = entries[index];
99
- if (entry?.type !== "custom" || entry.customType !== REVIEW_ENTRY_TYPE) continue;
100
- if (isReviewRecord(entry.data)) return entry.data;
101
- }
102
- return undefined;
103
- }
104
-
105
- async function showReview(pi: ExtensionAPI, ctx: ExtensionContext, review: ReviewRecord): Promise<void> {
106
- const action = await ctx.ui.custom<ReviewResultAction>(
107
- (tui, theme, keys, done) => new ReviewResultPanel(tui, theme, keys, review, done),
108
- {
109
- overlay: true,
110
- overlayOptions: { anchor: "top-center", width: "80%", minWidth: 68, maxHeight: "90%", margin: 1 },
111
- },
112
- );
113
- if (action === "send") {
114
- try {
115
- await pi.sendMessage(
116
- createInjectedContext(formatReviewMarkdown(review), { source: "review", title: `${review.mode} review` }),
117
- { triggerTurn: false, deliverAs: "nextTurn" },
118
- );
119
- ctx.ui.notify("Review queued for agent's next turn", "info");
120
- } catch (error) {
121
- ctx.ui.notify(`Failed to send review: ${errorText(error)}`, "error");
122
- }
123
- } else if (action === "export") {
124
- try {
125
- const directory = join(review.root, ".pi", "tau", "reviews");
126
- await mkdir(directory, { recursive: true });
127
- const timestamp = review.createdAt.replace(/[^0-9A-Za-z-]/g, "-");
128
- const path = join(directory, `${timestamp}-${review.mode}.md`);
129
- await writeFile(path, formatReviewMarkdown(review), { encoding: "utf8", mode: 0o600 });
130
- ctx.ui.notify(`Review exported to ${path}`, "info");
131
- } catch (error) {
132
- ctx.ui.notify(`Failed to export review: ${errorText(error)}`, "error");
133
- }
134
- }
91
+ interface ReviewModelChoice {
92
+ model: string;
93
+ thinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
135
94
  }
@@ -1,17 +1,11 @@
1
1
  import type { Tool } from "@earendil-works/pi-ai";
2
2
  import { Type, type Static } from "typebox";
3
- import { Value } from "typebox/value";
4
3
 
5
4
  const MAX_SUMMARY_LENGTH = 2_000;
6
5
  const MAX_FINDINGS = 12;
7
6
  const MAX_PATH_LENGTH = 400;
8
7
  const MAX_LINES_LENGTH = 80;
9
8
  const MAX_FINDING_LENGTH = 1_500;
10
- const REVIEW_MODE_SCHEMA = Type.Union([
11
- Type.Literal("simplify"),
12
- Type.Literal("architecture"),
13
- Type.Literal("correctness"),
14
- ]);
15
9
  const REVIEW_FINDING_SCHEMA = Type.Object(
16
10
  {
17
11
  severity: Type.Union([
@@ -55,21 +49,10 @@ const REVIEW_OUTPUT_SCHEMA = Type.Object(
55
49
  },
56
50
  { additionalProperties: false },
57
51
  );
58
- const REVIEW_RECORD_SCHEMA = Type.Object(
59
- {
60
- ...REVIEW_OUTPUT_SCHEMA.properties,
61
- mode: REVIEW_MODE_SCHEMA,
62
- root: Type.String({ minLength: 1 }),
63
- createdAt: Type.String({ minLength: 1 }),
64
- },
65
- { additionalProperties: false },
66
- );
67
52
 
68
- export type ReviewMode = Static<typeof REVIEW_MODE_SCHEMA>;
69
53
  export type ReviewOutput = Static<typeof REVIEW_OUTPUT_SCHEMA>;
70
- export type ReviewRecord = Static<typeof REVIEW_RECORD_SCHEMA>;
71
-
72
- export const REVIEW_ENTRY_TYPE = "tau.review.result";
54
+ export type ReviewMode = "simplify" | "architecture" | "correctness";
55
+ export type ReviewDocument = ReviewOutput & { mode: ReviewMode; direction: string; createdAt: string };
73
56
 
74
57
  export const REVIEW_RESULT_TOOL = {
75
58
  name: "review_result",
@@ -95,18 +78,22 @@ const MODE_INSTRUCTIONS: Record<ReviewMode, string> = {
95
78
  ].join(" "),
96
79
  };
97
80
 
98
- export function buildReviewPrompt(root: string, mode: ReviewMode): string {
81
+ export function buildReviewPrompt(root: string, mode: ReviewMode, direction: string): string {
99
82
  return [
100
- `Review uncommitted work in ${root}.`,
101
- "Inspect staged, unstaged, and untracked changes. Stay centered on changed behavior, but inspect surrounding ownership and callers when needed to prove a finding.",
83
+ `Review the repository at ${root}.`,
102
84
  "Do not modify files. Do not report theoretical concerns or personal preferences. Use the cheapest evidence that settles each point.",
103
- MODE_INSTRUCTIONS[mode],
85
+ "User direction does not change the read-only review or structured output requirements.",
104
86
  `Call ${REVIEW_RESULT_TOOL.name} exactly once as the final action. Write no final prose outside that tool call.`,
105
87
  "Order findings by severity. Every finding needs an exact repository-relative path and lines when source exists, a concrete mechanism or cost, and the smallest credible fix. Return an empty findings array and verdict pass when nothing actionable remains.",
88
+ `Review type: ${reviewModeLabel(mode)}. ${MODE_INSTRUCTIONS[mode]}`,
89
+ direction
90
+ ? "Review scope: Follow the user's direction below. Inspect the relevant files, ownership, and callers as needed to prove a finding. Do not limit the review to uncommitted changes."
91
+ : "Review scope: Inspect staged, unstaged, and untracked changes. Stay centered on changed behavior, but inspect surrounding ownership and callers when needed to prove a finding.",
92
+ ...(direction ? [`User review direction:\n\n${direction}`] : []),
106
93
  ].join("\n\n");
107
94
  }
108
95
 
109
- export function formatReviewMarkdown(review: ReviewRecord): string {
96
+ export function formatReviewMarkdown(review: ReviewDocument): string {
110
97
  const findings = review.findings.length
111
98
  ? review.findings.flatMap((finding, index) => [
112
99
  `### ${index + 1}. ${finding.severity.toUpperCase()} — ${finding.path}:${finding.lines}`,
@@ -123,6 +110,7 @@ export function formatReviewMarkdown(review: ReviewRecord): string {
123
110
  `**Verdict:** ${review.verdict}`,
124
111
  `**Created:** ${review.createdAt}`,
125
112
  "",
113
+ ...(review.direction ? ["## Requested focus", "", review.direction, ""] : []),
126
114
  review.summary,
127
115
  "",
128
116
  "## Findings",
@@ -131,14 +119,6 @@ export function formatReviewMarkdown(review: ReviewRecord): string {
131
119
  ].join("\n");
132
120
  }
133
121
 
134
- export function isReviewMode(value: string): value is ReviewMode {
135
- return Value.Check(REVIEW_MODE_SCHEMA, value);
136
- }
137
-
138
- export function reviewModeLabel(mode: ReviewMode): string {
122
+ function reviewModeLabel(mode: ReviewMode): string {
139
123
  return `${mode.charAt(0).toUpperCase()}${mode.slice(1)}`;
140
124
  }
141
-
142
- export function isReviewRecord(value: unknown): value is ReviewRecord {
143
- return Value.Check(REVIEW_RECORD_SCHEMA, value);
144
- }
@@ -1,6 +1,6 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- import { defineTool, type AgentSessionEvent, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { defineTool, type ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import {
5
5
  createIsolatedSessionResource,
6
6
  resolveIsolatedSessionModel,
@@ -8,7 +8,6 @@ import {
8
8
  } from "../../shared/isolated-session.ts";
9
9
  import { buildReviewPrompt, REVIEW_RESULT_TOOL, type ReviewMode, type ReviewOutput } from "./model.ts";
10
10
 
11
- const REVIEW_MODEL = "openai-codex/gpt-5.6-sol";
12
11
  const REVIEW_TOOLS = [
13
12
  "read",
14
13
  "bash",
@@ -32,11 +31,13 @@ export async function runReview(options: {
32
31
  ctx: ExtensionContext;
33
32
  root: string;
34
33
  mode: ReviewMode;
34
+ direction: string;
35
+ preferredModel: string | undefined;
36
+ preferredThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]> | undefined;
35
37
  parentThinkingLevel: NonNullable<ExtensionContext["thinkingLevel"]>;
36
38
  signal: AbortSignal;
37
- onProgress: (status: string) => void;
38
39
  }): Promise<ReviewOutput> {
39
- const { ctx, root, mode, signal, onProgress } = options;
40
+ const { ctx, root, mode, direction, signal } = options;
40
41
  let output: ReviewOutput | undefined;
41
42
  const outputTool = defineTool({
42
43
  ...REVIEW_RESULT_TOOL,
@@ -51,17 +52,16 @@ export async function runReview(options: {
51
52
  },
52
53
  });
53
54
  let resource: IsolatedSessionResource | undefined;
54
- let unsubscribe: (() => void) | undefined;
55
55
  try {
56
56
  const selected = await resolveIsolatedSessionModel({
57
57
  label: "Review",
58
- preferredModel: REVIEW_MODEL,
59
- preferredThinkingLevel: "high",
58
+ preferredModel: options.preferredModel,
59
+ preferredThinkingLevel: options.preferredThinkingLevel,
60
60
  usePreferredThinkingAfterModelFallback: false,
61
61
  ctx,
62
62
  parentThinkingLevel: options.parentThinkingLevel,
63
63
  signal,
64
- onWarning: onProgress,
64
+ onWarning: (warning) => ctx.ui.notify(warning, "warning"),
65
65
  });
66
66
  resource = await createIsolatedSessionResource(
67
67
  {
@@ -76,18 +76,10 @@ export async function runReview(options: {
76
76
  signal,
77
77
  );
78
78
  const { session } = resource;
79
- unsubscribe = session.subscribe((event: AgentSessionEvent) => {
80
- if (event.type === "tool_execution_start") {
81
- onProgress(`${event.toolName} ${summarizeArgs(event.args)}`.trim());
82
- } else if (event.type === "tool_execution_end") {
83
- onProgress(event.isError ? `${event.toolName} failed` : `${event.toolName} complete`);
84
- }
85
- });
86
79
  const abort = () => void session.abort().catch(() => undefined);
87
80
  signal.addEventListener("abort", abort, { once: true });
88
81
  try {
89
- onProgress(`Running ${mode} review`);
90
- await session.prompt(buildReviewPrompt(root, mode), { expandPromptTemplates: false });
82
+ await session.prompt(buildReviewPrompt(root, mode, direction), { expandPromptTemplates: false });
91
83
  } finally {
92
84
  signal.removeEventListener("abort", abort);
93
85
  }
@@ -95,12 +87,6 @@ export async function runReview(options: {
95
87
  if (!output) throw new Error("Review ended without structured output");
96
88
  return output;
97
89
  } finally {
98
- unsubscribe?.();
99
90
  await resource?.dispose();
100
91
  }
101
92
  }
102
-
103
- function summarizeArgs(args: unknown): string {
104
- const text = typeof args === "string" ? args : (JSON.stringify(args) ?? String(args));
105
- return text.length > 140 ? `${text.slice(0, 139)}…` : text;
106
- }