@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
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { IsObject, type TSchema } from "typebox";
2
+ import { IsObject, type TObject, type TSchema } from "typebox";
3
3
  import { Value } from "typebox/value";
4
4
  import type { JsonObject, TauExtensionSettingsSpec } from "./define.ts";
5
5
  import { asObject, readJsonStatus, writeJsonObject } from "./json.ts";
@@ -63,6 +63,47 @@ function extensionSection(root: JsonObject, key: string): JsonObject | undefined
63
63
 
64
64
  const NO_DEFAULT = Symbol("no-default");
65
65
 
66
+ function repairKnownProperties(
67
+ schema: TObject,
68
+ valueObject: JsonObject,
69
+ defaultObject: JsonObject | undefined,
70
+ ): JsonObject | typeof NO_DEFAULT {
71
+ const repaired: JsonObject = {};
72
+ const required = new Set(Array.isArray(schema.required) ? schema.required : []);
73
+ for (const [key, propertySchema] of Object.entries(schema.properties)) {
74
+ if (!Object.hasOwn(valueObject, key)) {
75
+ if (!required.has(key)) continue;
76
+ const requiredDefault = propertyDefault(propertySchema, defaultObject, key);
77
+ if (requiredDefault === NO_DEFAULT) return NO_DEFAULT;
78
+ repaired[key] = Value.Clone(requiredDefault);
79
+ continue;
80
+ }
81
+
82
+ const propertyValue = replaceInvalidProperties(
83
+ propertySchema,
84
+ valueObject[key],
85
+ propertyDefault(propertySchema, defaultObject, key),
86
+ );
87
+ if (propertyValue !== NO_DEFAULT) repaired[key] = propertyValue;
88
+ else if (required.has(key)) return NO_DEFAULT;
89
+ }
90
+ return repaired;
91
+ }
92
+
93
+ function repairAdditionalProperties(schema: TObject, valueObject: JsonObject, repaired: JsonObject): void {
94
+ const additionalProperties = asObject(schema)?.additionalProperties;
95
+ if (additionalProperties === false) return;
96
+ for (const [key, additionalValue] of Object.entries(valueObject)) {
97
+ if (Object.hasOwn(schema.properties, key)) continue;
98
+ if (additionalProperties === undefined || additionalProperties === true) {
99
+ repaired[key] = additionalValue;
100
+ continue;
101
+ }
102
+ const repairedAdditional = replaceInvalidProperties(additionalProperties as TSchema, additionalValue, NO_DEFAULT);
103
+ if (repairedAdditional !== NO_DEFAULT) repaired[key] = repairedAdditional;
104
+ }
105
+ }
106
+
66
107
  function replaceInvalidProperties(
67
108
  schema: TSchema,
68
109
  value: unknown,
@@ -73,43 +114,9 @@ function replaceInvalidProperties(
73
114
  const valueObject = asObject(value);
74
115
  if (IsObject(schema) && valueObject) {
75
116
  const defaultObject = documentedDefault === NO_DEFAULT ? undefined : asObject(documentedDefault);
76
- const repaired: JsonObject = {};
77
- const required = new Set(Array.isArray(schema.required) ? schema.required : []);
78
- for (const [key, propertySchema] of Object.entries(schema.properties)) {
79
- if (!Object.hasOwn(valueObject, key)) {
80
- if (!required.has(key)) continue;
81
- const requiredDefault = propertyDefault(propertySchema, defaultObject, key);
82
- if (requiredDefault === NO_DEFAULT) return validatedDefault(schema, documentedDefault);
83
- repaired[key] = Value.Clone(requiredDefault);
84
- continue;
85
- }
86
-
87
- const propertyValue = replaceInvalidProperties(
88
- propertySchema,
89
- valueObject[key],
90
- propertyDefault(propertySchema, defaultObject, key),
91
- );
92
- if (propertyValue !== NO_DEFAULT) repaired[key] = propertyValue;
93
- else if (required.has(key)) return validatedDefault(schema, documentedDefault);
94
- }
95
-
96
- const schemaObject = asObject(schema);
97
- const additionalProperties = schemaObject?.additionalProperties;
98
- if (additionalProperties !== false) {
99
- for (const [key, additionalValue] of Object.entries(valueObject)) {
100
- if (Object.hasOwn(schema.properties, key)) continue;
101
- if (additionalProperties === undefined || additionalProperties === true) repaired[key] = additionalValue;
102
- else {
103
- const repairedAdditional = replaceInvalidProperties(
104
- additionalProperties as TSchema,
105
- additionalValue,
106
- NO_DEFAULT,
107
- );
108
- if (repairedAdditional !== NO_DEFAULT) repaired[key] = repairedAdditional;
109
- }
110
- }
111
- }
112
-
117
+ const repaired = repairKnownProperties(schema, valueObject, defaultObject);
118
+ if (repaired === NO_DEFAULT) return validatedDefault(schema, documentedDefault);
119
+ repairAdditionalProperties(schema, valueObject, repaired);
113
120
  if (Value.Check(schema, repaired)) return repaired;
114
121
  }
115
122
 
@@ -126,6 +126,7 @@ export class TemporaryOutputStore {
126
126
  return writer;
127
127
  }
128
128
 
129
+ // fallow-ignore-next-line unused-class-member -- called via createTemporaryOutputStore() from explore session lifecycle
129
130
  async shutdown(): Promise<void> {
130
131
  if (this.shutdownPromise) return this.shutdownPromise;
131
132
  this.shutdownPromise = this.performShutdown();
@@ -177,6 +178,20 @@ export class TemporaryOutputStore {
177
178
  this.active.delete(writer);
178
179
  }
179
180
 
181
+ private async isRemovableOrphan(path: string): Promise<boolean> {
182
+ const metadata = await stat(path);
183
+ if (Date.now() - metadata.mtimeMs < this.orphanLifetimeMs) return false;
184
+ const marker = JSON.parse(await readFile(join(path, OWNER_FILE), "utf8")) as Partial<OwnerMarker>;
185
+ if (marker.kind !== "tau-tool-output" || marker.version !== 1 || typeof marker.pid !== "number") return false;
186
+ try {
187
+ process.kill(marker.pid, 0);
188
+ return false;
189
+ } catch (error) {
190
+ if (!isErrorCode(error, "ESRCH")) return false;
191
+ }
192
+ return true;
193
+ }
194
+
180
195
  private async removeStaleOrphans(): Promise<void> {
181
196
  let directory;
182
197
  try {
@@ -188,16 +203,7 @@ export class TemporaryOutputStore {
188
203
  if (!entry.isDirectory() || !entry.name.startsWith(DIRECTORY_PREFIX)) continue;
189
204
  const path = join(this.baseDirectory, entry.name);
190
205
  try {
191
- const metadata = await stat(path);
192
- if (Date.now() - metadata.mtimeMs < this.orphanLifetimeMs) continue;
193
- const marker = JSON.parse(await readFile(join(path, OWNER_FILE), "utf8")) as Partial<OwnerMarker>;
194
- if (marker.kind !== "tau-tool-output" || marker.version !== 1 || typeof marker.pid !== "number") continue;
195
- try {
196
- process.kill(marker.pid, 0);
197
- continue;
198
- } catch (error) {
199
- if (!isErrorCode(error, "ESRCH")) continue;
200
- }
206
+ if (!(await this.isRemovableOrphan(path))) continue;
201
207
  await rm(path, { recursive: true, force: true });
202
208
  } catch {
203
209
  // An untrusted or concurrently changing directory is not ours to remove.
@@ -1,5 +1,5 @@
1
1
  import type { Tree } from "web-tree-sitter";
2
- import type { Decl, ImportRef } from "./ir.ts";
2
+ import type { CallSite, Decl, ImportRef } from "./ir.ts";
3
3
  import type { PackageSurfaceResolver } from "./package-surface.ts";
4
4
 
5
5
  export type LanguageCapabilities = {
@@ -8,6 +8,8 @@ export type LanguageCapabilities = {
8
8
  fileDeps: boolean;
9
9
  callEdges: boolean;
10
10
  packageSurface: boolean;
11
+ /** Interfaces are satisfied by method set, not by naming them (Go). */
12
+ structuralImplements: boolean;
11
13
  };
12
14
 
13
15
  /**
@@ -25,11 +27,18 @@ export type AstSearchPatternContext = {
25
27
  export type ExtractResult = {
26
28
  decls: Decl[];
27
29
  imports: ImportRef[];
30
+ /** Call sites in top-level statements — outside any declaration body. */
31
+ fileCalls: CallSite[];
28
32
  };
29
33
 
30
- /** Result of adapter-owned import specifier → file resolution. */
34
+ /**
35
+ * Result of adapter-owned import specifier → file resolution.
36
+ * `package` is an in-repo directory too large to list file by file — distinct
37
+ * from `external`, which means "not in this repository".
38
+ */
31
39
  export type FileDepResolution =
32
40
  | { kind: "internal"; paths: string[] }
41
+ | { kind: "package"; id: string; dir: string; fileCount: number }
33
42
  | { kind: "external"; id: string }
34
43
  | { kind: "unresolved" };
35
44
 
@@ -13,7 +13,7 @@ import type { AdapterRegistry } from "./registry.ts";
13
13
  import { createDefaultRegistry } from "./registry.ts";
14
14
  import type { FileIr } from "./ir.ts";
15
15
  import { grammarWasmPath, loadGrammarManifest, runtimeWasmPath, type GrammarPin } from "./grammars/manifest.ts";
16
- import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "../traverse.ts";
16
+ import { formatPathForDisplay, pathResolutionError, resolveExplorePath } from "./traverse.ts";
17
17
 
18
18
  export type FileSource = {
19
19
  ir: FileIr;
@@ -95,6 +95,40 @@ function pinById(id: string): GrammarPin {
95
95
  return pin;
96
96
  }
97
97
 
98
+ function isMetaVarStart(c: string): boolean {
99
+ return (c >= "A" && c <= "Z") || c === "_";
100
+ }
101
+
102
+ function isMetaVarContinue(c: string): boolean {
103
+ return isMetaVarStart(c) || (c >= "0" && c <= "9");
104
+ }
105
+
106
+ function pushUniqueName(list: string[], name: string): void {
107
+ if (!list.includes(name)) list.push(name);
108
+ }
109
+
110
+ /** Read `$NAME` / `$$$NAME` starting at `start` (must point at `$`). */
111
+ function readMetaVarAt(pattern: string, start: number): { dollars: number; name: string; next: number } {
112
+ let i = start;
113
+ let dollars = 0;
114
+ while (i < pattern.length && pattern[i] === "$") {
115
+ dollars += 1;
116
+ i += 1;
117
+ }
118
+ let name = "";
119
+ while (i < pattern.length) {
120
+ const c = pattern[i];
121
+ if (c === undefined) break;
122
+ if ((name.length === 0 && isMetaVarStart(c)) || (name.length > 0 && isMetaVarContinue(c))) {
123
+ name += c;
124
+ i += 1;
125
+ continue;
126
+ }
127
+ break;
128
+ }
129
+ return { dollars, name, next: i };
130
+ }
131
+
98
132
  /** ast-grep metavars: `$NAME` one node, `$$$NAME` sibling sequence. Skip `$_`. */
99
133
  function metaVarNames(pattern: string): { singles: string[]; multis: string[] } {
100
134
  const singles: string[] = [];
@@ -105,30 +139,11 @@ function metaVarNames(pattern: string): { singles: string[]; multis: string[] }
105
139
  i += 1;
106
140
  continue;
107
141
  }
108
- let dollars = 0;
109
- while (i < pattern.length && pattern[i] === "$") {
110
- dollars += 1;
111
- i += 1;
112
- }
113
- let name = "";
114
- while (i < pattern.length) {
115
- const c = pattern[i];
116
- if (c === undefined) break;
117
- const isAlpha = (c >= "A" && c <= "Z") || c === "_";
118
- const isAlnum = isAlpha || (c >= "0" && c <= "9");
119
- if ((name.length === 0 && isAlpha) || (name.length > 0 && isAlnum)) {
120
- name += c;
121
- i += 1;
122
- continue;
123
- }
124
- break;
125
- }
142
+ const { dollars, name, next } = readMetaVarAt(pattern, i);
143
+ i = next;
126
144
  if (name.length === 0 || name === "_") continue;
127
- if (dollars >= 3) {
128
- if (!multis.includes(name)) multis.push(name);
129
- } else if (dollars === 1) {
130
- if (!singles.includes(name)) singles.push(name);
131
- }
145
+ if (dollars >= 3) pushUniqueName(multis, name);
146
+ else if (dollars === 1) pushUniqueName(singles, name);
132
147
  }
133
148
  return { singles, multis };
134
149
  }
@@ -228,6 +243,17 @@ export function createExploreEngine(options: ExploreEngineOptions): ExploreEngin
228
243
  }
229
244
  };
230
245
 
246
+ const requireSearchableAdapter = (languageId: string) => {
247
+ const adapter = registry.adapterForId(languageId);
248
+ if (adapter === undefined) {
249
+ throw new Error(`Unregistered language: ${languageId}`);
250
+ }
251
+ if (adapter.mode !== "grammar" || !adapter.capabilities.search) {
252
+ throw new Error(`Language does not support structural search: ${languageId}`);
253
+ }
254
+ return adapter;
255
+ };
256
+
231
257
  const ensureReady = async (): Promise<Parser> => {
232
258
  assertLive();
233
259
  if (parser !== undefined) return parser;
@@ -257,13 +283,7 @@ export function createExploreEngine(options: ExploreEngineOptions): ExploreEngin
257
283
  const ensureSearchLanguage = async (languageId: string): Promise<void> => {
258
284
  assertLive();
259
285
  if (searchLangReady.has(languageId)) return;
260
- const adapter = registry.adapterForId(languageId);
261
- if (adapter === undefined) {
262
- throw new Error(`Unregistered language: ${languageId}`);
263
- }
264
- if (adapter.mode !== "grammar" || !adapter.capabilities.search) {
265
- throw new Error(`Language does not support structural search: ${languageId}`);
266
- }
286
+ requireSearchableAdapter(languageId);
267
287
  await ensureSearchReady();
268
288
  assertLive();
269
289
  await registerDynamicLanguage({
@@ -320,6 +340,7 @@ export function createExploreEngine(options: ExploreEngineOptions): ExploreEngin
320
340
  lineCount,
321
341
  decls: extracted.decls,
322
342
  imports: extracted.imports,
343
+ fileCalls: extracted.fileCalls,
323
344
  parseDegraded: false,
324
345
  };
325
346
  }
@@ -342,6 +363,7 @@ export function createExploreEngine(options: ExploreEngineOptions): ExploreEngin
342
363
  lineCount,
343
364
  decls: extracted.decls,
344
365
  imports: extracted.imports,
366
+ fileCalls: extracted.fileCalls,
345
367
  parseDegraded: tree.rootNode.hasError,
346
368
  };
347
369
  } finally {
@@ -386,13 +408,7 @@ export function createExploreEngine(options: ExploreEngineOptions): ExploreEngin
386
408
 
387
409
  async searchInSource(languageId: string, source: string, pattern: string): Promise<AstSearchHit[]> {
388
410
  assertLive();
389
- const adapter = registry.adapterForId(languageId);
390
- if (adapter === undefined) {
391
- throw new Error(`Unregistered language: ${languageId}`);
392
- }
393
- if (adapter.mode !== "grammar" || !adapter.capabilities.search) {
394
- throw new Error(`Language does not support structural search: ${languageId}`);
395
- }
411
+ const adapter = requireSearchableAdapter(languageId);
396
412
  await ensureSearchLanguage(languageId);
397
413
  assertLive();
398
414
 
@@ -1,27 +1,36 @@
1
1
  import type { AstSearchMatch, AstSearchResult } from "../queries/ast-search.ts";
2
2
  import type { ScanOutcome } from "../scan.ts";
3
- import type { TraversalLimit } from "../../traverse.ts";
4
- import { formatPathForDisplay } from "../../traverse.ts";
3
+ import type { TraversalLimit } from "../traverse.ts";
4
+ import { formatPathForDisplay } from "../traverse.ts";
5
5
 
6
6
  function lineRange(startLine: number, endLine: number): string {
7
7
  return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
8
8
  }
9
9
 
10
+ /** Head-retained line budget per match. resultLimit caps match count, not match size. */
11
+ const MATCH_MAX_LINES = 12;
12
+
10
13
  function formatMatchRows(match: AstSearchMatch): string[] {
11
14
  const lines: string[] = [];
12
15
  const preview = match.text.length > 0 ? match.text : "(empty)";
13
16
  const previewLines = preview.split("\n");
14
17
  const first = previewLines[0] ?? "(empty)";
15
18
  lines.push(`${lineRange(match.startLine, match.endLine)} ${first}`);
16
- for (let i = 1; i < previewLines.length; i += 1) {
19
+ const shown = Math.min(previewLines.length, MATCH_MAX_LINES);
20
+ for (let i = 1; i < shown; i += 1) {
17
21
  lines.push(` ${previewLines[i] ?? ""}`);
18
22
  }
23
+ if (previewLines.length > shown) {
24
+ lines.push(` ... +${previewLines.length - shown} lines`);
25
+ }
19
26
  for (const binding of match.bindings) {
20
27
  const bindLines = binding.text.split("\n");
21
- lines.push(` $${binding.name} = ${bindLines[0] ?? ""}`);
22
- for (let i = 1; i < bindLines.length; i += 1) {
23
- lines.push(` ${bindLines[i] ?? ""}`);
28
+ // A multi-line capture is a span, not a value — report its size.
29
+ if (bindLines.length > 1) {
30
+ lines.push(` $${binding.name} = <${bindLines.length} lines>`);
31
+ continue;
24
32
  }
33
+ lines.push(` $${binding.name} = ${bindLines[0] ?? ""}`);
25
34
  }
26
35
  if (match.enclosing !== undefined) {
27
36
  lines.push(` in ${match.enclosing.kind} ${match.enclosing.name} L${match.enclosing.startLine}`);
@@ -1,7 +1,7 @@
1
- import { formatPathForDisplay } from "../../traverse.ts";
1
+ import { formatPathForDisplay } from "../traverse.ts";
2
2
  import type { ContextEntry, ContextGroup, ContextResult, ContextTarget } from "../queries/context.ts";
3
3
  import { compositeSectionBlocks, type CompositeBlock } from "./composite.ts";
4
- import { formatCandidateList } from "./relationships.ts";
4
+ import { formatTargetedResult } from "./targeted-result.ts";
5
5
 
6
6
  function formatTargetHeader(target: ContextTarget, budget: number, used: number, cwd: string): string {
7
7
  const path = formatPathForDisplay(target.path, cwd);
@@ -43,10 +43,7 @@ function formatResolved(result: Extract<ContextResult, { kind: "resolved" }>, cw
43
43
  }
44
44
 
45
45
  export function formatContextResult(result: ContextResult, cwd: string): string {
46
- if (result.kind === "error") return result.message;
47
- if (result.kind === "notFound") return "No matching declaration.";
48
- if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
49
- return formatResolved(result, cwd);
46
+ return formatTargetedResult(result, cwd, formatResolved);
50
47
  }
51
48
 
52
49
  export function contextSectionBlocks(
@@ -0,0 +1,94 @@
1
+ import type { FileDepQueryResult } from "../graph/file-graph.ts";
2
+ import { formatPathForDisplay } from "../traverse.ts";
3
+
4
+ function depthPrefix(depth: number, showDepth: boolean): string {
5
+ return showDepth ? `d${depth} ` : "";
6
+ }
7
+
8
+ function commonDirPrefixLength(split: readonly string[][]): number {
9
+ const first = split[0];
10
+ if (first === undefined || split.length <= 1) return 0;
11
+ let common = 0;
12
+ while (common < first.length - 1) {
13
+ const part = first[common];
14
+ if (part === undefined) break;
15
+ if (!split.every((parts) => parts[common] === part)) break;
16
+ common += 1;
17
+ }
18
+ return common;
19
+ }
20
+
21
+ /** Factor shared directory prefixes into a small indent tree. */
22
+ function formatInternalTree(rows: readonly { depth: number; display: string }[], showDepth: boolean): string[] {
23
+ if (rows.length === 0) return [];
24
+
25
+ const split = rows.map((row) => row.display.split("/"));
26
+ const common = commonDirPrefixLength(split);
27
+ const first = split[0];
28
+ const lines: string[] = [];
29
+ if (common > 0 && first !== undefined) {
30
+ lines.push(first.slice(0, common).join("/"));
31
+ }
32
+
33
+ for (let i = 0; i < rows.length; i += 1) {
34
+ const row = rows[i];
35
+ const parts = split[i];
36
+ if (row === undefined || parts === undefined) continue;
37
+ const leaf = common > 0 ? parts.slice(common).join("/") : row.display;
38
+ const indent = common > 0 ? " " : "";
39
+ lines.push(`${indent}${depthPrefix(row.depth, showDepth)}${leaf}`);
40
+ }
41
+ return lines;
42
+ }
43
+
44
+ export function formatDepsResult(result: FileDepQueryResult, cwd: string, emptyLabel: string): string {
45
+ const showDepth = result.depth > 1;
46
+ const inRepo: { depth: number; display: string }[] = [];
47
+ const external: { depth: number; id: string }[] = [];
48
+ for (const hit of result.hits) {
49
+ if (hit.kind === "internal") {
50
+ inRepo.push({ depth: hit.depth, display: formatPathForDisplay(hit.path, cwd) });
51
+ continue;
52
+ }
53
+ if (hit.kind === "package") {
54
+ // In-repo directory, listed as a package because file-by-file would flood.
55
+ inRepo.push({
56
+ depth: hit.depth,
57
+ display: `${formatPathForDisplay(hit.dir, cwd)}/ (${hit.fileCount} files)`,
58
+ });
59
+ continue;
60
+ }
61
+ external.push({ depth: hit.depth, id: hit.id });
62
+ }
63
+
64
+ if (inRepo.length === 0 && external.length === 0) {
65
+ return emptyLabel;
66
+ }
67
+
68
+ const lines: string[] = [];
69
+ lines.push(...formatInternalTree(inRepo, showDepth));
70
+
71
+ if (external.length > 0) {
72
+ lines.push("external:");
73
+ for (const hit of external) {
74
+ lines.push(` ${depthPrefix(hit.depth, showDepth)}${hit.id}`);
75
+ }
76
+ }
77
+ if (result.externalOmitted > 0) {
78
+ lines.push(` +${result.externalOmitted} more external`);
79
+ }
80
+
81
+ if (result.resultLimitReached) {
82
+ lines.push(`omitted (result limit ${result.resultLimit})`);
83
+ }
84
+
85
+ return lines.join("\n");
86
+ }
87
+
88
+ export function formatDepsEmpty(): string {
89
+ return "No file dependencies";
90
+ }
91
+
92
+ export function formatReverseDepsEmpty(): string {
93
+ return "No reverse file dependencies";
94
+ }
@@ -1,7 +1,7 @@
1
1
  import type { DiscoverCandidate, DiscoverResult } from "../queries/discover.ts";
2
2
  import type { ScanOutcome } from "../scan.ts";
3
- import type { TraversalLimit } from "../../traverse.ts";
4
- import { formatPathForDisplay } from "../../traverse.ts";
3
+ import type { TraversalLimit } from "../traverse.ts";
4
+ import { formatPathForDisplay } from "../traverse.ts";
5
5
 
6
6
  function lineRange(startLine: number, endLine: number): string {
7
7
  return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
@@ -1,7 +1,7 @@
1
- import { formatPathForDisplay } from "../../traverse.ts";
1
+ import { formatPathForDisplay } from "../traverse.ts";
2
2
  import type { ImpactFileRow, ImpactResult, ImpactSection, ImpactSiteRow, ImpactTarget } from "../queries/impact.ts";
3
3
  import { compositeSectionBlocks, type CompositeBlock } from "./composite.ts";
4
- import { formatCandidateList } from "./relationships.ts";
4
+ import { formatTargetedResult } from "./targeted-result.ts";
5
5
 
6
6
  function formatTargetHeader(target: ImpactTarget, cwd: string): string {
7
7
  const path = formatPathForDisplay(target.path, cwd);
@@ -17,10 +17,11 @@ function formatSiteRow(row: ImpactSiteRow): string {
17
17
 
18
18
  function formatFileRow(row: ImpactFileRow, cwd: string, showDepth: boolean): string {
19
19
  const depth = showDepth ? `d${row.depth} ` : "";
20
- if (row.path !== undefined) {
21
- return ` ${depth}${formatPathForDisplay(row.path, cwd)}`;
20
+ if (row.target.kind === "internal") return ` ${depth}${formatPathForDisplay(row.target.path, cwd)}`;
21
+ if (row.target.kind === "package") {
22
+ return ` ${depth}${formatPathForDisplay(row.target.dir, cwd)}/ (${row.target.fileCount} files)`;
22
23
  }
23
- return ` ${depth}${row.externalId ?? "?"}`;
24
+ return ` ${depth}${row.target.id}`;
24
25
  }
25
26
 
26
27
  function formatSection(section: ImpactSection, cwd: string): string[] {
@@ -41,8 +42,8 @@ function formatSection(section: ImpactSection, cwd: string): string[] {
41
42
  }
42
43
 
43
44
  if (fileRows.length > 0) {
44
- const internal = fileRows.filter((row) => row.path !== undefined);
45
- const external = fileRows.filter((row) => row.externalId !== undefined);
45
+ const internal = fileRows.filter((row) => row.target.kind !== "external");
46
+ const external = fileRows.filter((row) => row.target.kind === "external");
46
47
  for (const row of internal) {
47
48
  lines.push(formatFileRow(row, cwd, showDepth));
48
49
  }
@@ -80,10 +81,7 @@ function formatResolved(result: Extract<ImpactResult, { kind: "resolved" }>, cwd
80
81
  }
81
82
 
82
83
  export function formatImpactResult(result: ImpactResult, cwd: string): string {
83
- if (result.kind === "error") return result.message;
84
- if (result.kind === "notFound") return "No matching declaration.";
85
- if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
86
- return formatResolved(result, cwd);
84
+ return formatTargetedResult(result, cwd, formatResolved);
87
85
  }
88
86
 
89
87
  export function impactSectionBlocks(
@@ -1,7 +1,7 @@
1
1
  import type { OutlineFileView, OutlineRow } from "../queries/outline.ts";
2
2
  import type { ScanOutcome } from "../scan.ts";
3
- import type { TraversalLimit } from "../../traverse.ts";
4
- import { formatPathForDisplay } from "../../traverse.ts";
3
+ import type { TraversalLimit } from "../traverse.ts";
4
+ import { formatPathForDisplay } from "../traverse.ts";
5
5
 
6
6
  function lineRange(startLine: number, endLine: number): string {
7
7
  return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
@@ -1,13 +1,19 @@
1
1
  import type { Candidate } from "../identity.ts";
2
2
  import type { RelationshipQueryResult, RelationshipSite, RelationshipTarget } from "../graph/relationships.ts";
3
- import { formatPathForDisplay } from "../../traverse.ts";
3
+ import { compactSignature } from "../slice.ts";
4
+ import { formatPathForDisplay } from "../traverse.ts";
5
+
6
+ function candidateParts(candidate: Candidate, cwd: string): { path: string; signature: string } {
7
+ return {
8
+ path: formatPathForDisplay(candidate.path, cwd),
9
+ signature: compactSignature(candidate.signature),
10
+ };
11
+ }
4
12
 
5
13
  function formatCompetitors(competitors: readonly Candidate[], cwd: string): string[] {
6
14
  const lines: string[] = [];
7
15
  for (const c of competitors) {
8
- const path = formatPathForDisplay(c.path, cwd);
9
- const sig = c.signature.replace(/\s+/gu, " ").trim();
10
- const short = sig.length > 80 ? `${sig.slice(0, 77)}...` : sig;
16
+ const { path, signature: short } = candidateParts(c, cwd);
11
17
  lines.push(` - ${path}:${c.startLine} ${c.kind} ${c.qualifiedName || c.name} ${short}`);
12
18
  }
13
19
  return lines;
@@ -57,9 +63,7 @@ function formatResolved(
57
63
  export function formatCandidateList(candidates: readonly Candidate[], cwd: string): string {
58
64
  const lines = ["Ambiguous target — disambiguate with targetPath or line:", ""];
59
65
  for (const c of candidates) {
60
- const path = formatPathForDisplay(c.path, cwd);
61
- const sig = c.signature.replace(/\s+/gu, " ").trim();
62
- const short = sig.length > 80 ? `${sig.slice(0, 77)}...` : sig;
66
+ const { path, signature: short } = candidateParts(c, cwd);
63
67
  lines.push(`${path}:${c.startLine} ${c.kind} ${c.qualifiedName || c.name}`);
64
68
  if (short.length > 0) lines.push(` ${short}`);
65
69
  }
@@ -1,5 +1,5 @@
1
1
  import type { ShowBatch, ShowBlock } from "../queries/show.ts";
2
- import { formatPathForDisplay } from "../../traverse.ts";
2
+ import { formatPathForDisplay } from "../traverse.ts";
3
3
 
4
4
  function lineRange(startLine: number, endLine: number): string {
5
5
  return startLine === endLine ? `L${startLine}` : `L${startLine}-${endLine}`;
@@ -0,0 +1,20 @@
1
+ import type { Candidate } from "../identity.ts";
2
+ import { formatCandidateList } from "./relationships.ts";
3
+
4
+ type TargetedResult<Resolved extends { kind: "resolved" }> =
5
+ | Resolved
6
+ | { kind: "candidates"; candidates: Candidate[] }
7
+ | { kind: "notFound" }
8
+ | { kind: "error"; message: string };
9
+
10
+ /** Shared dispatch for query results that resolve one declaration target. */
11
+ export function formatTargetedResult<Resolved extends { kind: "resolved" }>(
12
+ result: TargetedResult<Resolved>,
13
+ cwd: string,
14
+ formatResolved: (resolved: Resolved, cwd: string) => string,
15
+ ): string {
16
+ if (result.kind === "error") return result.message;
17
+ if (result.kind === "notFound") return "No matching declaration.";
18
+ if (result.kind === "candidates") return formatCandidateList(result.candidates, cwd);
19
+ return formatResolved(result, cwd);
20
+ }
Binary file
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "webTreeSitter": "0.26.11",
3
3
  "vscodeTreeSitterWasm": "0.3.1",
4
- "treeSitterCli": "0.26.11",
5
- "wasiSdk": "wasi-sdk-29",
6
4
  "grammars": [
7
5
  { "id": "typescript", "source": "vscode", "file": "tree-sitter-typescript.wasm" },
8
6
  { "id": "tsx", "source": "vscode", "file": "tree-sitter-tsx.wasm" },
9
7
  { "id": "go", "source": "vscode", "file": "tree-sitter-go.wasm" },
10
8
  { "id": "rust", "source": "vscode", "file": "tree-sitter-rust.wasm" },
11
- { "id": "c_sharp", "source": "vscode", "file": "tree-sitter-c-sharp.wasm" },
9
+ {
10
+ "id": "c_sharp",
11
+ "source": "release",
12
+ "file": "c_sharp.wasm",
13
+ "repo": "https://github.com/tree-sitter/tree-sitter-c-sharp",
14
+ "tag": "v0.23.5",
15
+ "rev": "cac6d5fb595f5811a076336682d5d595ac1c9e85",
16
+ "url": "https://github.com/tree-sitter/tree-sitter-c-sharp/releases/download/v0.23.5/tree-sitter-c_sharp.wasm"
17
+ },
12
18
  { "id": "java", "source": "vscode", "file": "tree-sitter-java.wasm" },
13
19
  {
14
20
  "id": "kotlin",
@@ -27,15 +33,6 @@
27
33
  "tag": "0.7.3",
28
34
  "rev": "b8b22bffbb3441780e6471665bacfb263741c86a",
29
35
  "url": "https://github.com/alex-pinkus/tree-sitter-swift/releases/download/0.7.3/tree-sitter-swift.wasm"
30
- },
31
- {
32
- "id": "odin",
33
- "source": "built",
34
- "file": "odin.wasm",
35
- "repo": "https://github.com/tree-sitter-grammars/tree-sitter-odin",
36
- "tag": "v1.3.0",
37
- "rev": "e8adc739b78409a99f8c31313f0bb54cc538cf73",
38
- "subdir": "."
39
36
  }
40
37
  ]
41
38
  }