@shanepadgett/tau-agent 0.33.0 → 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 +130 -93
  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
@@ -1,36 +1,163 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { join } from "node:path";
2
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { getKeybindings } from "@earendil-works/pi-tui";
4
+ import { prepareFileInjection } from "@shanepadgett/tau-agent";
4
5
  import { createGitRunner, loadRepoStatus } from "../../shared/git.ts";
5
6
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
7
+ import { findProjectRoot, loadContextEntries, type ContextEntry, type ContextShowTarget } from "./definitions.ts";
6
8
  import { ContextPanel, ContextSyncStatusPanel } from "./panel.ts";
7
- import { findProjectRoot, loadContextEntries, type ContextEntry } from "./definitions.ts";
8
- import { hideContextSyncEvidenceTool, registerContextSyncEvidenceTool } from "./evidence.ts";
9
- import {
10
- buildContextProjection,
11
- contextProjectionKey,
12
- contextProjectionMessage,
13
- removeLegacyContextMessages,
14
- selectContextEntries,
15
- } from "./projection.ts";
16
9
  import contextSettings from "./settings.ts";
17
- import { CONTEXT_SELECTION_TYPE, createContextSelectionState, replayContextSelection } from "./state.ts";
18
- import { runContextSync } from "./sync.ts";
10
+ import { type ContextSyncDetails, runContextSync } from "./sync.ts";
19
11
  import { formatContextValidationFailure, validateContextCatalog } from "./validation.ts";
20
12
 
13
+ const CONTEXT_BRIEF_TYPE = "tau.context.brief";
14
+
15
+ const SHOW_RANK = {
16
+ signature: 0,
17
+ signatureWithDocs: 1,
18
+ declaration: 2,
19
+ declarationWithImports: 3,
20
+ } as const;
21
+
22
+ function collectInjectionPlan(selected: readonly ContextEntry[]): {
23
+ read: string[];
24
+ shows: ContextShowTarget[];
25
+ outline: string[];
26
+ references: string[];
27
+ } {
28
+ const read = new Set(selected.flatMap((entry) => entry.read));
29
+ const showByKey = new Map<string, ContextShowTarget>();
30
+ for (const entry of selected) {
31
+ for (const target of entry.show) {
32
+ if (read.has(target.path)) continue;
33
+ const key = `${target.path}\0${target.name}`;
34
+ const existing = showByKey.get(key);
35
+ if (!existing || SHOW_RANK[target.view] > SHOW_RANK[existing.view]) showByKey.set(key, target);
36
+ }
37
+ }
38
+ const shows = [...showByKey.values()].sort(
39
+ (left, right) => left.path.localeCompare(right.path) || left.name.localeCompare(right.name),
40
+ );
41
+ const outline = new Set(selected.flatMap((entry) => entry.outline).filter((path) => !read.has(path)));
42
+ const references = [
43
+ ...new Set(selected.flatMap((entry) => entry.references).filter((path) => !read.has(path) && !outline.has(path))),
44
+ ].sort((left, right) => left.localeCompare(right));
45
+ return {
46
+ read: [...read].sort((left, right) => left.localeCompare(right)),
47
+ shows,
48
+ outline: [...outline].sort((left, right) => left.localeCompare(right)),
49
+ references,
50
+ };
51
+ }
52
+
53
+ function contextBriefContent(selected: readonly ContextEntry[], references: readonly string[], failed: number): string {
54
+ const authority =
55
+ failed === 0
56
+ ? "The complete files, show targets, and outlines that follow are current. Treat them as authoritative and do not read them again. Show rows are declaration slices only — do not assume the rest of the file is loaded. Use a ranged read for bodies an outline omits, and re-read a file only after you change it."
57
+ : "Successful complete-file, show, and outline rows that follow are current; failed rows contain no source context. Treat successful rows as authoritative and do not read them again. Show rows are declaration slices only — do not assume the rest of the file is loaded. Use a ranged read for bodies an outline omits, and re-read a file only after you change it.";
58
+ return [
59
+ "Active repository context, injected once from the current catalog:",
60
+ ...selected.map((entry) => `- ${entry.id}: ${entry.description}`),
61
+ "",
62
+ "Unloaded references:",
63
+ ...(references.length ? references.map((path) => `- ${path}`) : ["(none)"]),
64
+ "",
65
+ authority,
66
+ ].join("\n");
67
+ }
68
+
69
+ async function injectSelectedContext(
70
+ pi: ExtensionAPI,
71
+ ctx: ExtensionContext,
72
+ root: string,
73
+ selected: readonly ContextEntry[],
74
+ ): Promise<void> {
75
+ const plan = collectInjectionPlan(selected);
76
+ const prepared = await prepareFileInjection(pi, {
77
+ cwd: root,
78
+ source: "context",
79
+ batchId: randomUUID(),
80
+ files: [
81
+ ...plan.read.map((path) => ({ path, mode: "full" as const })),
82
+ ...plan.shows.map((target) => ({
83
+ path: target.path,
84
+ mode: "show" as const,
85
+ name: target.name,
86
+ view: target.view,
87
+ })),
88
+ ...plan.outline.map((path) => ({ path, mode: "outline" as const })),
89
+ ],
90
+ });
91
+ const failed = prepared.filter((message) => message.details.status === "failed").length;
92
+ pi.sendMessage({
93
+ customType: CONTEXT_BRIEF_TYPE,
94
+ content: contextBriefContent(selected, plan.references, failed),
95
+ display: false,
96
+ });
97
+ for (const message of prepared) pi.sendMessage(message);
98
+ if (failed > 0) {
99
+ ctx.ui.notify(
100
+ `Injected ${prepared.length - failed} of ${prepared.length} files from ${selected.length} context entries; ${failed} failed`,
101
+ "warning",
102
+ );
103
+ }
104
+ }
105
+
106
+ function hasAbortedAssistant(messages: readonly unknown[]): boolean {
107
+ return messages.some(
108
+ (message) =>
109
+ typeof message === "object" &&
110
+ message !== null &&
111
+ "role" in message &&
112
+ message.role === "assistant" &&
113
+ "stopReason" in message &&
114
+ message.stopReason === "aborted",
115
+ );
116
+ }
117
+
118
+ async function handleAgentEndValidation(
119
+ pi: ExtensionAPI,
120
+ ctx: ExtensionContext,
121
+ settings: typeof contextSettings.defaults,
122
+ lastValidationFailure: string | undefined,
123
+ ): Promise<string | undefined> {
124
+ if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) return undefined;
125
+ const root = await findProjectRoot(ctx.cwd);
126
+ const git = createGitRunner(pi, ctx);
127
+ if (!(await loadRepoStatus(git))) return lastValidationFailure;
128
+ const failure = formatContextValidationFailure(
129
+ await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
130
+ );
131
+ if (!failure) return undefined;
132
+ if (failure === lastValidationFailure) return lastValidationFailure;
133
+ ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
134
+ const result = ctx.mode === "tui" ? await runContextSyncWithEditor(pi, ctx, {}) : await runContextSync(pi, ctx, {});
135
+ if (!result || result.outcome === "cancelled") {
136
+ ctx.ui.notify("Context sync cancelled", "info");
137
+ return failure;
138
+ }
139
+ const afterFailure = formatContextValidationFailure(
140
+ await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
141
+ );
142
+ if (result.outcome === "failed" || afterFailure) {
143
+ ctx.ui.notify(
144
+ result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
145
+ "error",
146
+ );
147
+ return afterFailure ?? `${failure}\n${result.reason}`;
148
+ }
149
+ ctx.ui.notify(result.summary, "info");
150
+ return undefined;
151
+ }
152
+
21
153
  export default function contextExtension(pi: ExtensionAPI): void {
22
154
  let settings = contextSettings.defaults;
23
155
  let lastValidationFailure: string | undefined;
24
156
  let syncCommandRegistered = false;
25
- let lifecycleGeneration = 0;
26
- let projectionCache: { key: string; content: string } | undefined;
27
-
28
- registerContextSyncEvidenceTool(pi);
29
157
 
30
158
  const refreshSettings = async (ctx: { cwd: string; isProjectTrusted(): boolean }) => {
31
159
  settings = await loadTauExtensionSettings(ctx, contextSettings);
32
160
  if (settings.sync.enabled) registerContextSyncCommand();
33
- hideContextSyncEvidenceTool(pi);
34
161
  };
35
162
 
36
163
  const registerContextSyncCommand = () => {
@@ -48,45 +175,21 @@ export default function contextExtension(pi: ExtensionAPI): void {
48
175
  return;
49
176
  }
50
177
  await ctx.waitForIdle();
51
- const controller = new AbortController();
52
- const widget = showContextSyncWidget(ctx);
53
- const stopListening = ctx.ui.onTerminalInput((data) => {
54
- if (!getKeybindings().matches(data, "tui.select.cancel")) return;
55
- if (!controller.signal.aborted) widget.update("Cancelling context sync");
56
- controller.abort();
57
- return { consume: true };
178
+ const result = await runContextSyncWithEditor(pi, ctx, {
179
+ nudge: args.trim() || undefined,
58
180
  });
59
- try {
60
- const result = await runContextSync(pi, ctx, {
61
- nudge: args.trim() || undefined,
62
- signal: controller.signal,
63
- onStatus: (status) => {
64
- widget.update(status);
65
- },
66
- });
67
- if (controller.signal.aborted) {
68
- ctx.ui.notify("Context sync cancelled", "info");
69
- return;
70
- }
71
- const level = result.outcome === "failed" ? "error" : "info";
72
- ctx.ui.notify(result.summary, level);
73
- } catch (error) {
74
- ctx.ui.notify(
75
- controller.signal.aborted
76
- ? "Context sync cancelled"
77
- : `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
78
- controller.signal.aborted ? "info" : "error",
79
- );
80
- } finally {
81
- stopListening();
82
- widget.clear();
181
+ if (!result) return;
182
+ if (result.outcome === "cancelled") {
183
+ ctx.ui.notify(result.summary, "info");
184
+ return;
83
185
  }
186
+ ctx.ui.notify(result.summary, result.outcome === "failed" ? "error" : "info");
84
187
  },
85
188
  });
86
189
  };
87
190
 
88
191
  pi.registerCommand("context", {
89
- description: "Select active repository context entries",
192
+ description: "Inject repository context entries into the conversation",
90
193
  handler: async (_args, ctx) => {
91
194
  if (ctx.mode !== "tui" || !ctx.isProjectTrusted()) {
92
195
  ctx.ui.notify("/context requires a trusted TUI project", "warning");
@@ -99,21 +202,15 @@ export default function contextExtension(pi: ExtensionAPI): void {
99
202
  ctx.ui.notify(`No context entries found in ${join(root, ".pi", "contexts")}`, "warning");
100
203
  return;
101
204
  }
102
- const active = replayContextSelection(ctx.sessionManager.getBranch()).entryIds;
103
205
  const selected = await ctx.ui.custom<ContextEntry[] | undefined>(
104
- (tui, theme, _keys, done) => new ContextPanel(tui, theme, entries, active, done),
206
+ (tui, theme, _keys, done) => new ContextPanel(tui, theme, entries, done),
105
207
  {
106
208
  overlay: true,
107
209
  overlayOptions: { anchor: "top-center", width: "70%", minWidth: 64, maxHeight: "80%", margin: 2 },
108
210
  },
109
211
  );
110
- if (selected === undefined) return;
111
- pi.appendEntry(CONTEXT_SELECTION_TYPE, createContextSelectionState(selected.map((entry) => entry.id)));
112
- projectionCache = undefined;
113
- ctx.ui.notify(
114
- selected.length ? `${selected.length} context entries active` : "Active context cleared",
115
- "info",
116
- );
212
+ if (selected === undefined || selected.length === 0) return;
213
+ await injectSelectedContext(pi, ctx, root, selected);
117
214
  },
118
215
  });
119
216
 
@@ -121,124 +218,59 @@ export default function contextExtension(pi: ExtensionAPI): void {
121
218
  if (settings.sync.enabled) registerContextSyncCommand();
122
219
 
123
220
  pi.on("session_start", async (_event, ctx) => {
124
- lifecycleGeneration += 1;
125
- projectionCache = undefined;
126
221
  await refreshSettings(ctx);
127
222
  });
128
- pi.on("context", async (event, ctx) => {
129
- const messages = removeLegacyContextMessages(event.messages);
130
- if (!ctx.isProjectTrusted()) return { messages };
131
- const entryIds = replayContextSelection(ctx.sessionManager.getBranch()).entryIds;
132
- if (entryIds.length === 0) return { messages };
133
- const generation = lifecycleGeneration;
134
- const root = await findProjectRoot(ctx.cwd);
135
- const selection = selectContextEntries(await loadContextEntries(root), entryIds);
136
- const key = await contextProjectionKey(root, selection, ctx.signal, () => generation === lifecycleGeneration);
137
- if (generation !== lifecycleGeneration) return { messages };
138
- if (projectionCache?.key !== key) {
139
- projectionCache = {
140
- key,
141
- content: await buildContextProjection(
142
- pi,
143
- root,
144
- selection,
145
- ctx.signal,
146
- () => generation === lifecycleGeneration,
147
- ),
148
- };
149
- }
150
- return { messages: [...messages, contextProjectionMessage(projectionCache.content)] };
151
- });
152
223
  pi.on("agent_start", async (_event, ctx) => {
153
224
  await refreshSettings(ctx);
154
225
  });
155
226
  pi.on("agent_end", async (event, ctx) => {
156
- if (
157
- event.messages.some(
158
- (message) =>
159
- typeof message === "object" &&
160
- message !== null &&
161
- "role" in message &&
162
- message.role === "assistant" &&
163
- "stopReason" in message &&
164
- message.stopReason === "aborted",
165
- )
166
- )
167
- return;
227
+ if (hasAbortedAssistant(event.messages)) return;
168
228
  settings = await loadTauExtensionSettings(ctx, contextSettings);
169
- if (!settings.sync.enabled || !settings.validation.enabled || !ctx.isProjectTrusted()) {
170
- lastValidationFailure = undefined;
171
- return;
172
- }
173
229
  try {
174
- const root = await findProjectRoot(ctx.cwd);
175
- const git = createGitRunner(pi, ctx);
176
- if (!(await loadRepoStatus(git))) return;
177
- const failure = formatContextValidationFailure(
178
- await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
179
- );
180
- if (!failure) {
181
- lastValidationFailure = undefined;
182
- return;
183
- }
184
- // Same unresolved failure fingerprint: do not re-spawn until the dirty/catalog signal changes.
185
- if (failure === lastValidationFailure) return;
186
- lastValidationFailure = failure;
187
- ctx.ui.notify("Context catalog validation failed; running context-sync", "error");
188
- const widget = showContextSyncWidget(ctx);
230
+ lastValidationFailure = await handleAgentEndValidation(pi, ctx, settings, lastValidationFailure);
231
+ } catch (error) {
232
+ ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
233
+ }
234
+ });
235
+ }
236
+
237
+ async function runContextSyncWithEditor(
238
+ pi: ExtensionAPI,
239
+ ctx: ExtensionContext,
240
+ options: { nudge?: string },
241
+ ): Promise<ContextSyncDetails | undefined> {
242
+ return ctx.ui.custom<ContextSyncDetails | undefined>((tui, theme, _keys, done) => {
243
+ const panel = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
244
+ void (async () => {
189
245
  try {
190
246
  const result = await runContextSync(pi, ctx, {
247
+ nudge: options.nudge,
248
+ signal: panel.signal,
191
249
  onStatus: (status) => {
192
- widget.update(status);
250
+ panel.update(status);
193
251
  },
194
252
  });
195
- const afterFailure = formatContextValidationFailure(
196
- await validateContextCatalog(git, root, settings.validation.ignoreGlobs),
197
- );
198
- if (result.outcome === "failed" || afterFailure) {
199
- lastValidationFailure = afterFailure ?? `${failure}\n${result.reason}`;
200
- ctx.ui.notify(
201
- result.outcome === "failed" ? result.summary : "Context catalog still invalid after context-sync",
202
- "error",
203
- );
253
+ done(result);
254
+ } catch (error) {
255
+ if (panel.signal.aborted) {
256
+ done({
257
+ outcome: "cancelled",
258
+ summary: "Context sync cancelled",
259
+ reason: "Cancelled by user.",
260
+ changedContextFiles: [],
261
+ });
204
262
  return;
205
263
  }
206
- lastValidationFailure = undefined;
207
- ctx.ui.notify(result.summary, "info");
264
+ done({
265
+ outcome: "failed",
266
+ summary: `Context sync failed: ${error instanceof Error ? error.message : String(error)}`,
267
+ reason: error instanceof Error ? error.message : String(error),
268
+ changedContextFiles: [],
269
+ });
208
270
  } finally {
209
- widget.clear();
271
+ panel.dispose();
210
272
  }
211
- } catch (error) {
212
- ctx.ui.notify(`Context validation failed: ${error instanceof Error ? error.message : String(error)}`, "error");
213
- }
214
- });
215
- pi.on("session_tree", () => {
216
- lifecycleGeneration += 1;
217
- projectionCache = undefined;
218
- });
219
- pi.on("session_compact", () => {
220
- lifecycleGeneration += 1;
221
- projectionCache = undefined;
222
- });
223
- pi.on("session_shutdown", () => {
224
- lifecycleGeneration += 1;
225
- projectionCache = undefined;
226
- });
227
- }
228
-
229
- function showContextSyncWidget(ctx: ExtensionContext): { update(status: string): void; clear(): void } {
230
- let widget: ContextSyncStatusPanel | undefined;
231
- ctx.ui.setWidget("context-sync", (tui, theme) => {
232
- widget = new ContextSyncStatusPanel(tui, theme, "Synchronizing repository context");
233
- return widget;
273
+ })();
274
+ return panel;
234
275
  });
235
- return {
236
- update(status) {
237
- widget?.update(status);
238
- },
239
- clear() {
240
- ctx.ui.setWidget("context-sync", undefined);
241
- widget = undefined;
242
- },
243
- };
244
276
  }
@@ -21,7 +21,6 @@ export class ContextPanel implements Component {
21
21
  tui: TUI,
22
22
  theme: Theme,
23
23
  entries: readonly ContextEntry[],
24
- activeEntryIds: readonly string[],
25
24
  done: (result: ContextEntry[] | undefined) => void,
26
25
  ) {
27
26
  const firstEntry = entries[0];
@@ -30,7 +29,8 @@ export class ContextPanel implements Component {
30
29
  this.theme = theme;
31
30
  this.done = done;
32
31
  this.current = firstEntry;
33
- const tabNames = [...new Set(entries.map((entry) => entry.tab))].sort();
32
+ // loadContextEntries already emits tabs in NN_ folder order; keep first-seen order.
33
+ const tabNames = [...new Set(entries.map((entry) => entry.tab))];
34
34
  this.tabs = new Tabs(
35
35
  theme,
36
36
  tabNames.map((tab) => {
@@ -68,42 +68,43 @@ export class ContextPanel implements Component {
68
68
  border: "box",
69
69
  };
70
70
  this.panel = new ToolPanel(theme, this.config);
71
- for (const [tab, list] of this.lists)
72
- list.setSelectedIds(activeEntryIds.filter((id) => id.startsWith(`${tab}/`)));
71
+ }
72
+
73
+ private handlePanelChromeKey(data: string): boolean {
74
+ const keys = getKeybindings();
75
+ const paths = this.currentPaths();
76
+ const pageSize = this.pathPageSize();
77
+ if (paths.length > pageSize && matchesKey(data, Key.alt("up"))) {
78
+ this.fileOffset = Math.max(0, this.fileOffset - pageSize);
79
+ return true;
80
+ }
81
+ if (paths.length > pageSize && matchesKey(data, Key.alt("down"))) {
82
+ this.fileOffset = Math.min(paths.length - pageSize, this.fileOffset + pageSize);
83
+ return true;
84
+ }
85
+ if (matchesKey(data, Key.ctrl("c"))) {
86
+ for (const [tab, tabList] of this.lists) {
87
+ tabList.setSelectedIds([]);
88
+ this.selected.set(tab, []);
89
+ }
90
+ return true;
91
+ }
92
+ if (keys.matches(data, "tui.select.confirm")) {
93
+ this.done([...this.selected.values()].flatMap((items) => [...items]));
94
+ return true;
95
+ }
96
+ if (keys.matches(data, "tui.select.cancel")) {
97
+ this.done(undefined);
98
+ return true;
99
+ }
100
+ return false;
73
101
  }
74
102
 
75
103
  handleInput(data: string): void {
76
104
  const list = this.activeList();
77
- if (!list?.isFilterFocused()) {
78
- const keys = getKeybindings();
79
- const paths = this.currentPaths();
80
- const pageSize = this.pathPageSize();
81
- if (paths.length > pageSize && matchesKey(data, Key.alt("up"))) {
82
- this.fileOffset = Math.max(0, this.fileOffset - pageSize);
83
- this.sync();
84
- return;
85
- }
86
- if (paths.length > pageSize && matchesKey(data, Key.alt("down"))) {
87
- this.fileOffset = Math.min(paths.length - pageSize, this.fileOffset + pageSize);
88
- this.sync();
89
- return;
90
- }
91
- if (matchesKey(data, Key.ctrl("c"))) {
92
- for (const [tab, tabList] of this.lists) {
93
- tabList.setSelectedIds([]);
94
- this.selected.set(tab, []);
95
- }
96
- this.sync();
97
- return;
98
- }
99
- if (keys.matches(data, "tui.select.confirm")) {
100
- this.done([...this.selected.values()].flatMap((items) => [...items]));
101
- return;
102
- }
103
- if (keys.matches(data, "tui.select.cancel")) {
104
- this.done(undefined);
105
- return;
106
- }
105
+ if (!list?.isFilterFocused() && this.handlePanelChromeKey(data)) {
106
+ this.sync();
107
+ return;
107
108
  }
108
109
  this.tabs.handleInput(data);
109
110
  const current = this.activeList()?.getCurrentItem() ?? this.current;
@@ -154,9 +155,16 @@ export class ContextPanel implements Component {
154
155
  invalidate: () => this.tabs.invalidate(),
155
156
  };
156
157
  }
157
- private currentPaths(): Array<{ kind: "read" | "outline" | "reference"; path: string }> {
158
+ private currentPaths(): Array<{ kind: "read" | "show" | "outline" | "reference"; path: string }> {
158
159
  return [
159
160
  ...this.current.read.map((path) => ({ kind: "read" as const, path })),
161
+ ...this.current.show.map((target) => ({
162
+ kind: "show" as const,
163
+ path:
164
+ target.view === "declaration"
165
+ ? `${target.name} · ${target.path}`
166
+ : `${target.name} [${target.view}] · ${target.path}`,
167
+ })),
160
168
  ...this.current.outline.map((path) => ({ kind: "outline" as const, path })),
161
169
  ...this.current.references.map((path) => ({ kind: "reference" as const, path })),
162
170
  ];
@@ -168,7 +176,7 @@ export class ContextPanel implements Component {
168
176
  return this.currentPaths().length > pageSize ? Math.max(1, pageSize - 1) : pageSize;
169
177
  }
170
178
  private secondary(): string {
171
- return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected · ${this.current.read.length} read · ${this.current.outline.length} outline · ${this.current.references.length} references`;
179
+ return `${[...this.selected.values()].reduce((sum, items) => sum + items.length, 0)} selected · ${this.current.read.length} read · ${this.current.show.length} show · ${this.current.outline.length} outline · ${this.current.references.length} references`;
172
180
  }
173
181
  private hints() {
174
182
  const list = this.activeList();
@@ -193,6 +201,7 @@ export class ContextSyncStatusPanel implements Component {
193
201
  private readonly tui: TUI;
194
202
  private readonly panel: ToolPanel;
195
203
  private readonly config: ToolPanelConfig;
204
+ private readonly controller = new AbortController();
196
205
  private readonly startedAt = Date.now();
197
206
  private readonly timer: ReturnType<typeof setInterval>;
198
207
  private lines: string[] = [];
@@ -214,7 +223,7 @@ export class ContextSyncStatusPanel implements Component {
214
223
  },
215
224
  invalidate: () => {},
216
225
  },
217
- footer: { kind: "hints", hints: [] },
226
+ footer: { kind: "hints", hints: [bindingHint("tui.select.cancel", "cancel")] },
218
227
  border: "horizontal",
219
228
  };
220
229
  this.panel = new ToolPanel(theme, this.config);
@@ -223,6 +232,17 @@ export class ContextSyncStatusPanel implements Component {
223
232
  this.timer.unref();
224
233
  }
225
234
 
235
+ get signal(): AbortSignal {
236
+ return this.controller.signal;
237
+ }
238
+
239
+ handleInput(data: string): void {
240
+ if (!getKeybindings().matches(data, "tui.select.cancel")) return;
241
+ if (this.controller.signal.aborted) return;
242
+ this.update("Cancelling context sync");
243
+ this.controller.abort();
244
+ }
245
+
226
246
  update(status: string): void {
227
247
  const added = status
228
248
  .split(/\r?\n/)