@danypops/pi-lector 0.13.9 → 0.14.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.
@@ -4,7 +4,6 @@ import { resolve } from "node:path";
4
4
  import type {
5
5
  CachedRepositoryPage,
6
6
  ContentHash,
7
- ContextBundleResult,
8
7
  Diagnostic,
9
8
  DocumentSymbolEntry,
10
9
  EditOutcome,
@@ -31,7 +30,7 @@ import type {
31
30
  WorkspaceLocation,
32
31
  WorkspaceMapResult,
33
32
  } from "@danypops/lector";
34
- import { DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS, PACKAGE_ECOSYSTEMS } from "@danypops/lector";
33
+ import { codeActionPreviewId, DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS, PACKAGE_ECOSYSTEMS } from "@danypops/lector";
35
34
  import {
36
35
  type AgentToolResult,
37
36
  createEditToolDefinition,
@@ -56,6 +55,12 @@ import {
56
55
  type CallHierarchyToolDetails,
57
56
  formatCallHierarchyCall,
58
57
  formatCallHierarchyResult,
58
+ formatCodeActionApplyCall,
59
+ formatCodeActionApplyResult,
60
+ formatCodeActionPreviewCall,
61
+ formatCodeActionPreviewResult,
62
+ formatDiagnosticDeltaCall,
63
+ formatDiagnosticDeltaResult,
59
64
  formatDiagnosticsCall,
60
65
  formatDiagnosticsResult,
61
66
  formatDocumentSymbolsCall,
@@ -68,8 +73,12 @@ import {
68
73
  formatGoToImplementationResult,
69
74
  formatHoverCall,
70
75
  formatHoverResult,
76
+ formatImpactAnalysisCall,
77
+ formatImpactAnalysisResult,
71
78
  formatReachableFromCall,
72
79
  formatReachableFromResult,
80
+ formatTypeHierarchyCall,
81
+ formatTypeHierarchyResult,
73
82
  formatWorkspaceMapCall,
74
83
  formatWorkspaceMapResult,
75
84
  } from "./code-intelligence/rendering.ts";
@@ -111,6 +120,9 @@ import {
111
120
  PACKAGE_SOURCE_LIST_VISIBLE_ROWS,
112
121
  packageSourceListMoreLine,
113
122
  } from "./package-source/rendering.ts";
123
+ import { formatSemanticModelContent } from "./presentation/model-content.ts";
124
+ import { withLectorPresentation } from "./presentation/presentation-contract.ts";
125
+ import { presentationTitle } from "./presentation/tool-presentation.ts";
114
126
  import { createLectorReadOperations } from "./read/operations.ts";
115
127
  import { createReferenceBasedRenameOperations } from "./reference-based-rename/operations.ts";
116
128
  import { createRenameOperations } from "./rename/operations.ts";
@@ -179,7 +191,7 @@ export default function (pi: ExtensionAPI) {
179
191
  const customToolNames = new Set<string>();
180
192
  function registerLectorTool<TParams extends TSchema, TDetails = unknown, TState = unknown>(tool: ToolDefinition<TParams, TDetails, TState>): void {
181
193
  customToolNames.add(tool.name);
182
- pi.registerTool(tool);
194
+ pi.registerTool(withLectorPresentation(tool));
183
195
  }
184
196
  pi.on("tool_result", (event) => {
185
197
  if (!customToolNames.has(event.toolName)) return;
@@ -461,7 +473,11 @@ export default function (pi: ExtensionAPI) {
461
473
  return { content: [{ type: "text", text }], details: result };
462
474
  },
463
475
  renderCall(args, theme) {
464
- return new Text(theme.fg("toolTitle", `localize ${typeof args.query === "string" ? args.query : "context"}`), 0, 0);
476
+ return new Text(
477
+ `${theme.fg("toolTitle", theme.bold(presentationTitle("localize_context")))} ${theme.fg("accent", typeof args.query === "string" ? `"${args.query}"` : "")}`,
478
+ 0,
479
+ 0,
480
+ );
465
481
  },
466
482
  renderResult(result, { isPartial }, theme, context) {
467
483
  if (isPartial) return new Text(theme.fg("warning", "Localizing..."), 0, 0);
@@ -472,8 +488,11 @@ export default function (pi: ExtensionAPI) {
472
488
  .join("\n");
473
489
  return new Text(theme.fg("error", errorText || "localize_context failed"), 0, 0);
474
490
  }
475
- const details = result.details as ContextBundleResult | undefined;
476
- return new Text(details ? `${details.candidates.length} candidates · graph ${details.completeness.graph}` : "Localization complete", 0, 0);
491
+ const contentText = result.content
492
+ .filter((block) => block.type === "text")
493
+ .map((block) => block.text)
494
+ .join("\n");
495
+ return new Text(contentText || "Localization complete", 0, 0);
477
496
  },
478
497
  });
479
498
 
@@ -774,6 +793,186 @@ export default function (pi: ExtensionAPI) {
774
793
  },
775
794
  });
776
795
 
796
+ registerLectorTool({
797
+ name: "code_action_preview",
798
+ label: "Code Action Preview",
799
+ description:
800
+ "Request bounded language-server code actions for one range. Returns opaque preview ids, affected files, exact edits, provenance, and truncation status. This operation is read-only; use code_action_apply separately to mutate files.",
801
+ promptSnippet: "Preview bounded language-server fixes for one range",
802
+ promptGuidelines: ["Preview first, inspect every affected path/edit, then pass exactly one returned previewId to code_action_apply."],
803
+ parameters: Type.Object({
804
+ path: Type.String({ description: "Absolute or cwd-relative file path" }),
805
+ startLine: Type.Number({ description: "1-indexed range start line" }),
806
+ startCharacter: Type.Number({ description: "1-indexed range start character" }),
807
+ endLine: Type.Number({ description: "1-indexed range end line" }),
808
+ endCharacter: Type.Number({ description: "1-indexed range end character" }),
809
+ only: Type.Optional(Type.Array(Type.String(), { maxItems: 20, description: "Code-action kinds such as quickfix" })),
810
+ includeCommandActions: Type.Optional(Type.Boolean({ description: "Include command-only actions in preview; guarded apply still denies them" })),
811
+ maxActions: Type.Number({ description: "Maximum actions" }),
812
+ maxEdits: Type.Number({ description: "Maximum edits per action" }),
813
+ maxFiles: Type.Number({ description: "Maximum affected files per action" }),
814
+ maxBytes: Type.Number({ description: "Maximum response JSON bytes" }),
815
+ deadlineMs: Type.Number({ description: "Wall-clock deadline in milliseconds" }),
816
+ }),
817
+ async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<OperationOutputs["workspace.previewCodeActions"]>> {
818
+ const path = resolve(cwd, params.path);
819
+ const vehicleCall: LectorVehicleCall = { toolName: "code_action_preview", toolCallId, signal, context: ctx };
820
+ const result = await codeIntelligenceOperations.previewCodeActions(
821
+ path,
822
+ {
823
+ range: {
824
+ start: { line: params.startLine, character: params.startCharacter },
825
+ end: { line: params.endLine, character: params.endCharacter },
826
+ },
827
+ ...(params.only ? { only: params.only } : {}),
828
+ ...(params.includeCommandActions !== undefined ? { includeCommandActions: params.includeCommandActions } : {}),
829
+ maxActions: params.maxActions,
830
+ maxEdits: params.maxEdits,
831
+ maxFiles: params.maxFiles,
832
+ maxBytes: params.maxBytes,
833
+ deadlineMs: params.deadlineMs,
834
+ },
835
+ vehicleCall,
836
+ );
837
+ const text = result.actions
838
+ .map((action) => `${action.id} ${action.kind ?? "action"} ${action.title} -- ${action.affectedPaths.join(", ") || "command only"}`)
839
+ .join("\n");
840
+ return { content: [{ type: "text", text: text || "No code actions." }], details: result };
841
+ },
842
+ renderCall(args, theme, context) {
843
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
844
+ text.setText(formatCodeActionPreviewCall(args, theme));
845
+ return text;
846
+ },
847
+ renderResult(result, { expanded, isPartial }, theme, context) {
848
+ if (isPartial) return new Text(theme.fg("warning", "Finding code actions..."), 0, 0);
849
+ if (context.isError) {
850
+ const errorText = result.content
851
+ .filter((block) => block.type === "text")
852
+ .map((block) => block.text)
853
+ .join("\n");
854
+ return new Text(theme.fg("error", errorText || "code_action_preview failed"), 0, 0);
855
+ }
856
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
857
+ text.setText(formatCodeActionPreviewResult(result.details, expanded, theme));
858
+ return text;
859
+ },
860
+ });
861
+
862
+ registerLectorTool({
863
+ name: "code_action_apply",
864
+ label: "Apply Code Action",
865
+ description:
866
+ "Apply one previously previewed language-server WorkspaceEdit atomically with workspace containment, document-version and content-hash guards. Returns a transaction id for exact transaction-aware revert. Command-only actions are denied.",
867
+ promptSnippet: "Atomically apply one previewed language-server edit",
868
+ promptGuidelines: ["Use only after inspecting code_action_preview; retain transactionId for diagnostic_delta and revert."],
869
+ parameters: Type.Object({
870
+ path: Type.String({ description: "The same file path used to obtain the preview" }),
871
+ previewId: Type.String({ description: "Opaque id returned by code_action_preview" }),
872
+ }),
873
+ async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<OperationOutputs["workspace.applyCodeAction"]>> {
874
+ const vehicleCall: LectorVehicleCall = { toolName: "code_action_apply", toolCallId, signal, context: ctx };
875
+ const result = await codeIntelligenceOperations.applyCodeAction(resolve(cwd, params.path), codeActionPreviewId(params.previewId), vehicleCall);
876
+ const text = [
877
+ ...result.touchedPaths,
878
+ ...(result.transactionId ? [`transaction ${result.transactionId}`] : []),
879
+ ...(result.pendingCommand ? [`pending command ${result.pendingCommand.command}`] : []),
880
+ ].join("\n");
881
+ return { content: [{ type: "text", text: text || "Code action made no file changes." }], details: result };
882
+ },
883
+ renderCall(args, theme, context) {
884
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
885
+ text.setText(formatCodeActionApplyCall(args, theme));
886
+ return text;
887
+ },
888
+ renderResult(result, { isPartial }, theme, context) {
889
+ if (isPartial) return new Text(theme.fg("warning", "Applying code action..."), 0, 0);
890
+ if (context.isError) {
891
+ const errorText = result.content
892
+ .filter((block) => block.type === "text")
893
+ .map((block) => block.text)
894
+ .join("\n");
895
+ return new Text(theme.fg("error", errorText || "code_action_apply failed"), 0, 0);
896
+ }
897
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
898
+ text.setText(formatCodeActionApplyResult(result.details, theme));
899
+ return text;
900
+ },
901
+ });
902
+
903
+ registerLectorTool({
904
+ name: "diagnostic_delta",
905
+ label: "Diagnostic Delta",
906
+ description:
907
+ "Return only diagnostics introduced, resolved, or changed by one atomic mutation transaction or git-ref diff across its bounded affected-file cone, with completeness and provenance. Transaction results include the exact safe revert operation.",
908
+ promptSnippet: "Inspect diagnostic changes caused by a transaction or git diff",
909
+ promptGuidelines: [
910
+ "Use source=transaction with the id returned by an atomic mutation, or source=git with a baseline ref; unchanged diagnostics are omitted.",
911
+ ],
912
+ parameters: Type.Object({
913
+ path: Type.String({ description: "Project directory or a path inside it" }),
914
+ source: Type.String({ description: "transaction | git" }),
915
+ sourceId: Type.String({ description: "Mutation transaction id or git baseline ref" }),
916
+ maxResults: Type.Optional(Type.Number({ description: "Maximum results per delta class" })),
917
+ maxBytes: Type.Optional(Type.Number({ description: "Maximum response JSON bytes" })),
918
+ maxDepth: Type.Optional(Type.Number({ description: "Git source: maximum impact depth" })),
919
+ maxNodes: Type.Optional(Type.Number({ description: "Git source: maximum graph nodes" })),
920
+ maxEdges: Type.Optional(Type.Number({ description: "Git source: maximum graph edges" })),
921
+ deadlineMs: Type.Optional(Type.Number({ description: "Git source: wall-clock deadline" })),
922
+ maxFiles: Type.Optional(Type.Number({ description: "Git source: maximum graph files" })),
923
+ maxSymbolsPerFile: Type.Optional(Type.Number({ description: "Git source: maximum declarations per file" })),
924
+ autoPopulate: Type.Optional(Type.Boolean({ description: "Git source: populate a missing/stale graph" })),
925
+ }),
926
+ async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.diagnosticDelta"]>> {
927
+ const source =
928
+ params.source === "transaction"
929
+ ? ({ kind: "transaction", transactionId: params.sourceId } as const)
930
+ : params.source === "git"
931
+ ? ({ kind: "git", ref: params.sourceId } as const)
932
+ : undefined;
933
+ if (!source) throw new TypeError("diagnostic_delta source must be transaction or git");
934
+ const result = await codeIntelligenceOperations.diagnosticDelta(resolve(cwd, params.path), source, {
935
+ maxResults: params.maxResults,
936
+ maxBytes: params.maxBytes,
937
+ maxDepth: params.maxDepth,
938
+ maxNodes: params.maxNodes,
939
+ maxEdges: params.maxEdges,
940
+ deadlineMs: params.deadlineMs,
941
+ maxFiles: params.maxFiles,
942
+ maxSymbolsPerFile: params.maxSymbolsPerFile,
943
+ autoPopulate: params.autoPopulate,
944
+ });
945
+ const text = [
946
+ ...result.introduced.map(
947
+ (diagnostic) => `introduced ${diagnostic.severity} ${diagnostic.range.path}:${diagnostic.range.start.line} -- ${diagnostic.message}`,
948
+ ),
949
+ ...result.resolved.map(
950
+ (diagnostic) => `resolved ${diagnostic.severity} ${diagnostic.range.path}:${diagnostic.range.start.line} -- ${diagnostic.message}`,
951
+ ),
952
+ ...result.changed.map(({ before, after }) => `changed ${after.range.path}:${after.range.start.line} -- ${before.message} -> ${after.message}`),
953
+ ].join("\n");
954
+ return { content: [{ type: "text", text: text || "No diagnostic changes." }], details: result };
955
+ },
956
+ renderCall(args, theme, context) {
957
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
958
+ text.setText(formatDiagnosticDeltaCall(args, theme));
959
+ return text;
960
+ },
961
+ renderResult(result, { expanded, isPartial }, theme, context) {
962
+ if (isPartial) return new Text(theme.fg("warning", "Comparing diagnostics..."), 0, 0);
963
+ if (context.isError) {
964
+ const errorText = result.content
965
+ .filter((block) => block.type === "text")
966
+ .map((block) => block.text)
967
+ .join("\n");
968
+ return new Text(theme.fg("error", errorText || "diagnostic_delta failed"), 0, 0);
969
+ }
970
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
971
+ text.setText(formatDiagnosticDeltaResult(result.details, expanded, theme));
972
+ return text;
973
+ },
974
+ });
975
+
777
976
  registerLectorTool({
778
977
  name: "call_hierarchy",
779
978
  label: "Call Hierarchy",
@@ -840,6 +1039,137 @@ export default function (pi: ExtensionAPI) {
840
1039
  },
841
1040
  });
842
1041
 
1042
+ registerLectorTool({
1043
+ name: "type_hierarchy",
1044
+ label: "Type Hierarchy",
1045
+ description:
1046
+ "Resolve a type at an exact file position, or list its direct supertypes or subtypes. ACTIONS: prepare, supertypes, subtypes. Capability-unavailable is reported distinctly from an empty hierarchy.",
1047
+ promptSnippet: "Resolve a type hierarchy at an exact position",
1048
+ promptGuidelines: ["Use supertypes/subtypes instead of grepping for extends or implements; the language server resolves semantic relationships."],
1049
+ parameters: Type.Object({
1050
+ direction: Type.String({ description: "prepare | supertypes | subtypes" }),
1051
+ ...positionParameters,
1052
+ maxResults: Type.Optional(Type.Number({ description: "Maximum hierarchy items (default 1000)" })),
1053
+ maxBytes: Type.Optional(Type.Number({ description: "Maximum JSON bytes (default 1 MiB)" })),
1054
+ deadlineMs: Type.Optional(Type.Number({ description: "Wall-clock deadline in milliseconds (default 10000, maximum 120000)" })),
1055
+ }),
1056
+ async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.prepareTypeHierarchy"]>> {
1057
+ const path = resolve(cwd, params.path);
1058
+ const bounds = { maxResults: params.maxResults, maxBytes: params.maxBytes, deadlineMs: params.deadlineMs };
1059
+ const result =
1060
+ params.direction === "prepare"
1061
+ ? await codeIntelligenceOperations.prepareTypeHierarchy(path, params.line, params.character, bounds)
1062
+ : params.direction === "supertypes"
1063
+ ? await codeIntelligenceOperations.supertypes(path, params.line, params.character, bounds)
1064
+ : params.direction === "subtypes"
1065
+ ? await codeIntelligenceOperations.subtypes(path, params.line, params.character, bounds)
1066
+ : undefined;
1067
+ if (!result) throw new Error(`unknown type_hierarchy direction: ${String(params.direction)}`);
1068
+ const text =
1069
+ result.items.length === 0
1070
+ ? "No type-hierarchy items found."
1071
+ : result.items.map((item) => `${item.kind} ${item.name} -- ${item.location.path}:${item.location.line}:${item.location.character}`).join("\n");
1072
+ return { content: [{ type: "text", text: `${describeIntelligenceSource(result.provenance)}\n${text}` }], details: result };
1073
+ },
1074
+ renderCall(args, theme, context) {
1075
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1076
+ text.setText(formatTypeHierarchyCall(args, theme));
1077
+ return text;
1078
+ },
1079
+ renderResult(result, { expanded, isPartial }, theme, context) {
1080
+ if (isPartial) return new Text(theme.fg("warning", "Resolving type hierarchy..."), 0, 0);
1081
+ if (context.isError) {
1082
+ const errorText = result.content
1083
+ .filter((block) => block.type === "text")
1084
+ .map((block) => block.text)
1085
+ .join("\n");
1086
+ return new Text(theme.fg("error", errorText || "type_hierarchy failed"), 0, 0);
1087
+ }
1088
+ const details = result.details;
1089
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1090
+ text.setText(renderIntelligenceSource(formatTypeHierarchyResult(details, expanded, theme), details.provenance, theme));
1091
+ return text;
1092
+ },
1093
+ });
1094
+
1095
+ registerLectorTool({
1096
+ name: "impact_analysis",
1097
+ label: "Impact Analysis",
1098
+ description:
1099
+ "Map a git diff or mutation transaction to changed declarations, semantic callers/references, package boundaries, diagnostics, and related tests. Associations preserve semantic-edge versus filename-heuristic evidence.",
1100
+ promptSnippet: "Find symbols and tests affected by a change",
1101
+ promptGuidelines: [
1102
+ "Use after a mutation or against a git ref to choose targeted verification from explicit evidence rather than filename guesses alone.",
1103
+ ],
1104
+ parameters: Type.Object({
1105
+ path: Type.String({ description: "Project directory or a path inside it" }),
1106
+ source: Type.String({ description: "git | mutation" }),
1107
+ ref: Type.Optional(Type.String({ description: "Git ref for source=git" })),
1108
+ transactionId: Type.Optional(Type.String({ description: "Mutation transaction id for source=mutation" })),
1109
+ maxDepth: Type.Number({ description: "Maximum reverse-graph hops" }),
1110
+ maxNodes: Type.Number({ description: "Maximum graph nodes" }),
1111
+ maxEdges: Type.Number({ description: "Maximum graph edges" }),
1112
+ maxBytes: Type.Number({ description: "Maximum response JSON bytes" }),
1113
+ deadlineMs: Type.Number({ description: "Wall-clock deadline in milliseconds" }),
1114
+ maxFiles: Type.Number({ description: "Maximum source files for graph freshness/population" }),
1115
+ maxSymbolsPerFile: Type.Number({ description: "Maximum declarations per source file" }),
1116
+ autoPopulate: Type.Optional(Type.Boolean({ description: "Populate once when no complete graph exists" })),
1117
+ coverage: Type.Optional(
1118
+ Type.Array(
1119
+ Type.Object({
1120
+ testPath: Type.String(),
1121
+ coveredPaths: Type.Array(Type.String(), { maxItems: 1000 }),
1122
+ }),
1123
+ { maxItems: 1000, description: "Optional test-to-covered-source evidence" },
1124
+ ),
1125
+ ),
1126
+ }),
1127
+ async execute(_toolCallId, params): Promise<AgentToolResult<OperationOutputs["workspace.impactAnalysis"]>> {
1128
+ const source =
1129
+ params.source === "git"
1130
+ ? ({ kind: "git", ...(params.ref !== undefined ? { ref: params.ref } : {}) } as const)
1131
+ : params.source === "mutation" && params.transactionId
1132
+ ? ({ kind: "mutation", transactionId: params.transactionId } as const)
1133
+ : undefined;
1134
+ if (!source) throw new TypeError("impact_analysis requires source=git or source=mutation with transactionId");
1135
+ const result = await codeIntelligenceOperations.impactAnalysis(resolve(cwd, params.path), source, {
1136
+ maxDepth: params.maxDepth,
1137
+ maxNodes: params.maxNodes,
1138
+ maxEdges: params.maxEdges,
1139
+ maxBytes: params.maxBytes,
1140
+ deadlineMs: params.deadlineMs,
1141
+ maxFiles: params.maxFiles,
1142
+ maxSymbolsPerFile: params.maxSymbolsPerFile,
1143
+ ...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
1144
+ ...(params.coverage !== undefined ? { coverage: params.coverage } : {}),
1145
+ });
1146
+ const text = [
1147
+ ...result.changedSymbols.map(({ symbol, side }) => `changed ${side} ${symbol.kind} ${symbol.name} -- ${symbol.location.path}`),
1148
+ ...result.impactedSymbols.map(({ symbol, depth }) => `impact depth=${depth} ${symbol.kind} ${symbol.name} -- ${symbol.location.path}`),
1149
+ ...result.relatedTests.map(({ symbol, evidence }) => `test ${evidence.kind} -- ${symbol.location.path}`),
1150
+ ].join("\n");
1151
+ return { content: [{ type: "text", text: text || "No changed symbols resolved." }], details: result };
1152
+ },
1153
+ renderCall(args, theme, context) {
1154
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1155
+ text.setText(formatImpactAnalysisCall(args, theme));
1156
+ return text;
1157
+ },
1158
+ renderResult(result, { expanded, isPartial }, theme, context) {
1159
+ if (isPartial) return new Text(theme.fg("warning", "Analyzing impact..."), 0, 0);
1160
+ if (context.isError) {
1161
+ const errorText = result.content
1162
+ .filter((block) => block.type === "text")
1163
+ .map((block) => block.text)
1164
+ .join("\n");
1165
+ return new Text(theme.fg("error", errorText || "impact_analysis failed"), 0, 0);
1166
+ }
1167
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1168
+ text.setText(formatImpactAnalysisResult(result.details, expanded, theme));
1169
+ return text;
1170
+ },
1171
+ });
1172
+
843
1173
  registerLectorTool({
844
1174
  name: "reference_based_rename",
845
1175
  label: "Reference-Based Rename",
@@ -874,7 +1204,7 @@ export default function (pi: ExtensionAPI) {
874
1204
  const toPath = typeof args.toPath === "string" ? args.toPath : "";
875
1205
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
876
1206
  text.setText(
877
- `${theme.fg("toolTitle", theme.bold("reference_based_rename"))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
1207
+ `${theme.fg("toolTitle", theme.bold(presentationTitle("reference_based_rename")))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
878
1208
  );
879
1209
  return text;
880
1210
  },
@@ -939,7 +1269,7 @@ export default function (pi: ExtensionAPI) {
939
1269
  const action = typeof args.action === "string" ? args.action : "";
940
1270
  const path = typeof args.path === "string" ? args.path : "";
941
1271
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
942
- text.setText(`${theme.fg("toolTitle", theme.bold("rename"))} ${theme.fg("dim", action)} ${theme.fg("accent", path)}`);
1272
+ text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("rename", action)))} ${theme.fg("accent", path)}`);
943
1273
  return text;
944
1274
  },
945
1275
  renderResult(result, { isPartial }, theme, context) {
@@ -1012,6 +1342,9 @@ export default function (pi: ExtensionAPI) {
1012
1342
  childId: Type.Optional(Type.String({ description: "The contained annotation's id -- required for contain/uncontain" })),
1013
1343
  rootId: Type.Optional(Type.String({ description: "The subtree's root annotation id -- required for tree" })),
1014
1344
  maxDepth: Type.Optional(Type.Number({ description: "Maximum containment hops from rootId to include -- required for tree" })),
1345
+ autoPopulate: Type.Optional(Type.Boolean({ description: "For create/refresh: populate a genuinely not-cached graph once before resolving anchors" })),
1346
+ maxFiles: Type.Optional(Type.Number({ description: "For create/refresh autoPopulate: explicit maximum files to scan" })),
1347
+ maxSymbolsPerFile: Type.Optional(Type.Number({ description: "For create/refresh autoPopulate: explicit maximum declarations per file" })),
1015
1348
  }),
1016
1349
  async execute(toolCallId, params, signal, _onUpdate, ctx): Promise<AgentToolResult<SymbolAnnotationToolDetails>> {
1017
1350
  const path = resolve(cwd, params.path);
@@ -1034,6 +1367,11 @@ export default function (pi: ExtensionAPI) {
1034
1367
  params.body,
1035
1368
  resolveAnchorInputs(params.anchors),
1036
1369
  vehicleCall,
1370
+ {
1371
+ ...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
1372
+ ...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
1373
+ ...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
1374
+ },
1037
1375
  );
1038
1376
  details.annotation = annotation;
1039
1377
  text = formatAnnotationDetail(annotation);
@@ -1063,6 +1401,11 @@ export default function (pi: ExtensionAPI) {
1063
1401
  params.body,
1064
1402
  resolveAnchorInputs(params.anchors),
1065
1403
  vehicleCall,
1404
+ {
1405
+ ...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
1406
+ ...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
1407
+ ...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
1408
+ },
1066
1409
  );
1067
1410
  details.annotation = annotation;
1068
1411
  text = annotation ? formatAnnotationDetail(annotation) : `no annotation "${params.id}"`;
@@ -1107,10 +1450,10 @@ export default function (pi: ExtensionAPI) {
1107
1450
  ? ` ${args.rootId}`
1108
1451
  : "";
1109
1452
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1110
- text.setText(`${theme.fg("toolTitle", theme.bold("symbol_annotations"))} ${theme.fg("accent", action)}${theme.fg("dim", id)}`);
1453
+ text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("symbol_annotations", action)))}${theme.fg("accent", id)}`);
1111
1454
  return text;
1112
1455
  },
1113
- renderResult(result, { isPartial }, theme, context) {
1456
+ renderResult(result, { expanded, isPartial }, theme, context) {
1114
1457
  if (isPartial) return new Text(theme.fg("warning", "Working on annotation..."), 0, 0);
1115
1458
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1116
1459
  if (context.isError) {
@@ -1123,11 +1466,11 @@ export default function (pi: ExtensionAPI) {
1123
1466
  }
1124
1467
  const details = result.details as SymbolAnnotationToolDetails | undefined;
1125
1468
  if (details?.annotations) {
1126
- text.setText(formatAnnotationListSummary(details.annotations, theme));
1469
+ text.setText(expanded ? details.annotations.map(formatAnnotationDetail).join("\n\n") : formatAnnotationListSummary(details.annotations, theme));
1127
1470
  return text;
1128
1471
  }
1129
1472
  if (details?.annotation) {
1130
- text.setText(formatAnnotationSummary(details.annotation, theme));
1473
+ text.setText(expanded ? formatAnnotationDetail(details.annotation) : formatAnnotationSummary(details.annotation, theme));
1131
1474
  return text;
1132
1475
  }
1133
1476
  if (details?.scrubbed !== undefined) {
@@ -1169,10 +1512,17 @@ export default function (pi: ExtensionAPI) {
1169
1512
  description: "Restrict to one edge kind; omit for any kind",
1170
1513
  }),
1171
1514
  ),
1515
+ autoPopulate: Type.Optional(Type.Boolean({ description: "Populate a genuinely not-cached graph once before traversing" })),
1516
+ maxFiles: Type.Optional(Type.Number({ description: "For autoPopulate: explicit maximum files to scan" })),
1517
+ maxSymbolsPerFile: Type.Optional(Type.Number({ description: "For autoPopulate: explicit maximum declarations per file" })),
1172
1518
  }),
1173
1519
  async execute(_toolCallId, params) {
1174
1520
  const path = resolve(cwd, params.path);
1175
- const symbols = await codeIntelligenceOperations.reachableFrom(path, params.line, params.character, params.maxDepth, params.kind);
1521
+ const symbols = await codeIntelligenceOperations.reachableFrom(path, params.line, params.character, params.maxDepth, params.kind, {
1522
+ ...(params.autoPopulate !== undefined ? { autoPopulate: params.autoPopulate } : {}),
1523
+ ...(params.maxFiles !== undefined ? { maxFiles: params.maxFiles } : {}),
1524
+ ...(params.maxSymbolsPerFile !== undefined ? { maxSymbolsPerFile: params.maxSymbolsPerFile } : {}),
1525
+ });
1176
1526
  const text =
1177
1527
  symbols.length === 0
1178
1528
  ? "Nothing reachable at this position."
@@ -1292,7 +1642,10 @@ export default function (pi: ExtensionAPI) {
1292
1642
  if (params.action === "job_status") {
1293
1643
  if (!params.jobId) throw new Error("workspace_cache action=job_status requires jobId");
1294
1644
  const job = await cacheOperations.jobStatus(params.jobId);
1295
- return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "job_status", job } };
1645
+ return {
1646
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "job_status"), job) }],
1647
+ details: { action: "job_status", job },
1648
+ };
1296
1649
  }
1297
1650
  if (params.action === "wait") {
1298
1651
  if (!params.jobId) throw new Error("workspace_cache action=wait requires jobId");
@@ -1314,7 +1667,10 @@ export default function (pi: ExtensionAPI) {
1314
1667
  );
1315
1668
  }
1316
1669
  const job = outcome.kind === "terminal" ? outcome.job : await cacheOperations.jobStatus(params.jobId);
1317
- return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "wait", job } };
1670
+ return {
1671
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "wait"), job) }],
1672
+ details: { action: "wait", job },
1673
+ };
1318
1674
  }
1319
1675
  if (!params.directory) throw new Error(`workspace_cache action=${params.action} requires directory`);
1320
1676
  const directory = resolve(cwd, params.directory);
@@ -1322,10 +1678,16 @@ export default function (pi: ExtensionAPI) {
1322
1678
  const maxSymbolsPerFile = params.maxSymbolsPerFile ?? 100;
1323
1679
  if (params.action === "status") {
1324
1680
  const status = await cacheOperations.status(directory, maxFiles, maxSymbolsPerFile);
1325
- return { content: [{ type: "text", text: JSON.stringify(status) }], details: { action: "status", status } };
1681
+ return {
1682
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "status"), status) }],
1683
+ details: { action: "status", status },
1684
+ };
1326
1685
  }
1327
1686
  const job = await cacheOperations.submit(directory, maxFiles, maxSymbolsPerFile, params.waitMs ?? 3_000);
1328
- return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "populate", job } };
1687
+ return {
1688
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "populate"), job) }],
1689
+ details: { action: "populate", job },
1690
+ };
1329
1691
  },
1330
1692
  renderCall(args, theme, context) {
1331
1693
  const action = args.action === "populate" || args.action === "wait" || args.action === "job_status" ? args.action : "status";
@@ -1354,18 +1716,20 @@ export default function (pi: ExtensionAPI) {
1354
1716
  name: "git",
1355
1717
  label: "Git",
1356
1718
  description:
1357
- "Working tree status, recent commit log, unified diff, one symbol's own declaration diff across two versions, ref-scoped blob/text/ancestry queries with no checkout, and a real disposable checkout at another ref, for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes), compare-symbol (a named symbol's own declaration text diffed between fromRef and toRef, or fromRef and the current working tree when toRef is omitted -- tree-sitter syntactic tier only, TypeScript/JavaScript files only, no project-aware cross-reference resolution), show (a path's exact blob content at `ref`, no checkout), grep-ref (text search across `ref`'s own tree, no checkout -- the ref-scoped equivalent of search_code), ls-ref (every file path in `ref`'s own tree, no checkout), is-ancestor (is `ancestorRef` a real ancestor of, or the same commit as, `ref` -- the backport/reachability check \"was this fix ported to this branch\" actually needs), worktree-add (materializes `ref` as a real, read-only project via a detached git worktree and returns its own `directory` -- pass that straight to find_symbols/search_code/this tool itself for full semantic queries against another branch/commit, not just text), worktree-remove (releases and deletes a worktree-add-created checkout -- `directory` is that checkout's own returned directory, not the source repo's).",
1719
+ "Working tree status, recent commit log, unified diff, one symbol's own declaration diff across two versions, ref-scoped blob/text/ancestry queries with no checkout, and a real disposable checkout at another ref, for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes), compare-symbol (a named symbol's own declaration text diffed between fromRef and toRef, or fromRef and the current working tree when toRef is omitted -- tree-sitter syntactic tier only, TypeScript/JavaScript files only, no project-aware cross-reference resolution), show (a path's exact blob content at `ref`, no checkout), grep-ref (text search across `ref`'s own tree, no checkout -- the ref-scoped equivalent of search_code), grep-history (bounded extended-regex search across commit trees reachable from all refs, with topological paging and commit provenance), ls-ref (every file path in `ref`'s own tree, no checkout), is-ancestor (is `ancestorRef` a real ancestor of, or the same commit as, `ref` -- the backport/reachability check \"was this fix ported to this branch\" actually needs), worktree-add (materializes `ref` as a real, read-only project via a detached git worktree and returns its own `directory` -- pass that straight to find_symbols/search_code/this tool itself for full semantic queries against another branch/commit, not just text), worktree-remove (releases and deletes a worktree-add-created checkout -- `directory` is that checkout's own returned directory, not the source repo's).",
1358
1720
  promptSnippet:
1359
1721
  "Show a repository's status, log, diff, one symbol's diff across versions, a ref-scoped blob/text/ancestry query, or a real checkout at another ref",
1360
1722
  promptGuidelines: [
1361
- "maxCount is required for action=log; maxBytes is required for action=diff/compare-symbol/grep-ref -- every bounded query needs its bound stated explicitly, never defaulted silently.",
1723
+ "maxCount is required for action=log; maxBytes is required for action=diff/compare-symbol/grep-ref/grep-history -- every bounded query needs its bound stated explicitly, never defaulted silently.",
1362
1724
  "path, symbol, and fromRef are required for action=compare-symbol; toRef is optional and means 'the current working tree' when omitted.",
1363
1725
  "ref is required for action=worktree-add. A repeated worktree-add for the same (directory, ref) reuses the existing checkout unless forceRefresh is set -- use that when ref is a branch that may have moved.",
1364
1726
  "action=worktree-remove's directory is worktree-add's own returned directory, never the source repo's -- always call it once done with a worktree to reclaim disk.",
1365
- "ref and path are required for action=show; ref and pattern (maxMatches, maxBytes) are required for action=grep-ref; ref (maxResults) is required for action=ls-ref; ancestorRef and ref are required for action=is-ancestor. None of the four checks anything out -- prefer them over worktree-add/find_symbols for a quick existence/text/ancestry answer.",
1727
+ "ref and path are required for action=show; ref and pattern (maxMatches, maxBytes) are required for action=grep-ref; pattern, commitOffset, maxCommits, maxMatches, maxBytes, and deadlineMs are required for action=grep-history; ref (maxResults) is required for action=ls-ref; ancestorRef and ref are required for action=is-ancestor. These actions inspect Git objects directly with no checkout.",
1366
1728
  ],
1367
1729
  parameters: Type.Object({
1368
- action: Type.String({ description: "status | log | diff | compare-symbol | show | grep-ref | ls-ref | is-ancestor | worktree-add | worktree-remove" }),
1730
+ action: Type.String({
1731
+ description: "status | log | diff | compare-symbol | show | grep-ref | grep-history | ls-ref | is-ancestor | worktree-add | worktree-remove",
1732
+ }),
1369
1733
  directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
1370
1734
  maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
1371
1735
  ref: Type.Optional(
@@ -1376,7 +1740,9 @@ export default function (pi: ExtensionAPI) {
1376
1740
  ),
1377
1741
  maxBytes: Type.Optional(
1378
1742
  Type.Number({
1379
- description: "Maximum diff/comparison/grep output size in bytes before truncating -- required for action=diff/compare-symbol/grep-ref",
1743
+ minimum: 1,
1744
+ maximum: 8 * 1024 * 1024,
1745
+ description: "Maximum diff/comparison/grep output size in bytes before truncating -- required for action=diff/compare-symbol/grep-ref/grep-history",
1380
1746
  }),
1381
1747
  ),
1382
1748
  path: Type.Optional(
@@ -1392,14 +1758,24 @@ export default function (pi: ExtensionAPI) {
1392
1758
  description: "action=worktree-add only: recreate an already-reused worktree at ref's current tip instead of returning the existing one",
1393
1759
  }),
1394
1760
  ),
1395
- pattern: Type.Optional(Type.String({ description: "Text pattern to search for -- required for action=grep-ref" })),
1761
+ pattern: Type.Optional(
1762
+ Type.String({ maxLength: 4096, description: "Pattern to search for -- required for grep-ref; an extended regular expression for grep-history" }),
1763
+ ),
1396
1764
  pathspecs: Type.Optional(
1397
- Type.Array(Type.String(), {
1765
+ Type.Array(Type.String({ minLength: 1, maxLength: 1024 }), {
1766
+ maxItems: 64,
1398
1767
  description:
1399
- 'Narrows action=grep-ref (glob-based, e.g. "*.go") or action=ls-ref (prefix-based, e.g. "pkg/dpll"); omitted searches/lists the whole tree',
1768
+ 'Narrows action=grep-ref/grep-history (glob-based, e.g. "*.go") or action=ls-ref (prefix-based, e.g. "pkg/dpll"); omitted searches/lists the whole tree',
1400
1769
  }),
1401
1770
  ),
1402
- maxMatches: Type.Optional(Type.Number({ description: "Maximum grep matches to return -- required for action=grep-ref" })),
1771
+ maxMatches: Type.Optional(
1772
+ Type.Number({ minimum: 1, maximum: 10_000, description: "Maximum grep matches to return -- required for action=grep-ref/grep-history" }),
1773
+ ),
1774
+ commitOffset: Type.Optional(
1775
+ Type.Number({ minimum: 0, maximum: 1_000_000, description: "Number of topologically ordered commits to skip -- required for action=grep-history" }),
1776
+ ),
1777
+ maxCommits: Type.Optional(Type.Number({ minimum: 1, maximum: 512, description: "Maximum commit trees to search -- required for action=grep-history" })),
1778
+ deadlineMs: Type.Optional(Type.Number({ minimum: 1, maximum: 120_000, description: "Wall-clock budget -- required for action=grep-history" })),
1403
1779
  maxResults: Type.Optional(Type.Number({ description: "Maximum file paths to return -- required for action=ls-ref" })),
1404
1780
  ancestorRef: Type.Optional(Type.String({ description: "The candidate ancestor ref -- required for action=is-ancestor" })),
1405
1781
  }),
@@ -1409,7 +1785,7 @@ export default function (pi: ExtensionAPI) {
1409
1785
  if (params.action === "status") {
1410
1786
  const summary = await gitOperations.status(directory, vehicleCall);
1411
1787
  const details: GitToolDetails = { action: "status", summary };
1412
- return { content: [{ type: "text", text: JSON.stringify(summary) }], details };
1788
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "status"), summary) }], details };
1413
1789
  }
1414
1790
  if (params.action === "log") {
1415
1791
  if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
@@ -1430,18 +1806,18 @@ export default function (pi: ExtensionAPI) {
1430
1806
  if (params.maxBytes === undefined) throw new Error("git action=compare-symbol requires maxBytes");
1431
1807
  const comparison = await gitOperations.compareSymbol(directory, params.path, params.symbol, params.fromRef, params.toRef, params.maxBytes);
1432
1808
  const details: GitToolDetails = { action: "compare-symbol", comparison };
1433
- return { content: [{ type: "text", text: JSON.stringify(comparison) }], details };
1809
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "compare-symbol"), comparison) }], details };
1434
1810
  }
1435
1811
  if (params.action === "worktree-add") {
1436
1812
  if (!params.ref) throw new Error("git action=worktree-add requires ref");
1437
1813
  const worktreeAdd = await gitOperations.worktreeAdd(directory, params.ref, params.forceRefresh, vehicleCall);
1438
1814
  const details: GitToolDetails = { action: "worktree-add", worktreeAdd };
1439
- return { content: [{ type: "text", text: JSON.stringify(worktreeAdd) }], details };
1815
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-add"), worktreeAdd) }], details };
1440
1816
  }
1441
1817
  if (params.action === "worktree-remove") {
1442
1818
  const worktreeRemove = await gitOperations.worktreeRemove(directory, vehicleCall);
1443
1819
  const details: GitToolDetails = { action: "worktree-remove", worktreeRemove };
1444
- return { content: [{ type: "text", text: JSON.stringify(worktreeRemove) }], details };
1820
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-remove"), worktreeRemove) }], details };
1445
1821
  }
1446
1822
  if (params.action === "show") {
1447
1823
  if (!params.ref || !params.path) throw new Error("git action=show requires ref and path");
@@ -1454,20 +1830,46 @@ export default function (pi: ExtensionAPI) {
1454
1830
  if (params.maxMatches === undefined || params.maxBytes === undefined) throw new Error("git action=grep-ref requires maxMatches and maxBytes");
1455
1831
  const grep = await gitOperations.grep(directory, params.ref, params.pattern, params.pathspecs, params.maxMatches, params.maxBytes, vehicleCall);
1456
1832
  const details: GitToolDetails = { action: "grep-ref", grep };
1457
- return { content: [{ type: "text", text: JSON.stringify(grep) }], details };
1833
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-ref"), grep) }], details };
1834
+ }
1835
+ if (params.action === "grep-history") {
1836
+ if (!params.pattern) throw new Error("git action=grep-history requires pattern");
1837
+ if (
1838
+ params.commitOffset === undefined ||
1839
+ params.maxCommits === undefined ||
1840
+ params.maxMatches === undefined ||
1841
+ params.maxBytes === undefined ||
1842
+ params.deadlineMs === undefined
1843
+ )
1844
+ throw new Error("git action=grep-history requires commitOffset, maxCommits, maxMatches, maxBytes, and deadlineMs");
1845
+ const historyGrep = await gitOperations.grepHistory(
1846
+ directory,
1847
+ params.pattern,
1848
+ params.pathspecs,
1849
+ {
1850
+ commitOffset: params.commitOffset,
1851
+ maxCommits: params.maxCommits,
1852
+ maxMatches: params.maxMatches,
1853
+ maxBytes: params.maxBytes,
1854
+ deadlineMs: params.deadlineMs,
1855
+ },
1856
+ vehicleCall,
1857
+ );
1858
+ const details: GitToolDetails = { action: "grep-history", historyGrep };
1859
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-history"), historyGrep) }], details };
1458
1860
  }
1459
1861
  if (params.action === "ls-ref") {
1460
1862
  if (!params.ref) throw new Error("git action=ls-ref requires ref");
1461
1863
  if (params.maxResults === undefined) throw new Error("git action=ls-ref requires maxResults");
1462
1864
  const listFiles = await gitOperations.listFiles(directory, params.ref, params.pathspecs, params.maxResults, vehicleCall);
1463
1865
  const details: GitToolDetails = { action: "ls-ref", listFiles };
1464
- return { content: [{ type: "text", text: JSON.stringify(listFiles) }], details };
1866
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "ls-ref"), listFiles) }], details };
1465
1867
  }
1466
1868
  if (params.action === "is-ancestor") {
1467
1869
  if (!params.ancestorRef || !params.ref) throw new Error("git action=is-ancestor requires ancestorRef and ref");
1468
1870
  const result = await gitOperations.isAncestor(directory, params.ancestorRef, params.ref, vehicleCall);
1469
1871
  const details: GitToolDetails = { action: "is-ancestor", isAncestor: { ancestorRef: params.ancestorRef, ref: params.ref, result } };
1470
- return { content: [{ type: "text", text: JSON.stringify({ isAncestor: result }) }], details };
1872
+ return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "is-ancestor"), { isAncestor: result }) }], details };
1471
1873
  }
1472
1874
  throw new Error(`unknown git action: ${String(params.action)}`);
1473
1875
  },
@@ -1497,7 +1899,7 @@ export default function (pi: ExtensionAPI) {
1497
1899
  name: "search_code",
1498
1900
  label: "Search Code",
1499
1901
  description:
1500
- "Multi-file text/regex search scoped to a real project directory, backed by ripgrep -- respects .gitignore, skips node_modules/.git/build output. Bounded by maxMatches and maxBytes; results are cached.",
1902
+ "Multi-file text/regex search scoped to a real project directory. Uses a bounded resident FFF index when ready and explicit ripgrep loading/stale/degraded fallback otherwise; respects ignore rules and skips generated dependency output. Bounded by maxMatches and maxBytes; results include lexical provenance.",
1501
1903
  promptSnippet: "Search a project's files for a pattern",
1502
1904
  parameters: Type.Object({
1503
1905
  directory: Type.String({ description: "Directory inside the project to search, absolute or relative to the current working directory" }),
@@ -1508,8 +1910,10 @@ export default function (pi: ExtensionAPI) {
1508
1910
  async execute(_toolCallId, params) {
1509
1911
  const directory = resolve(cwd, params.directory);
1510
1912
  const result = await searchOperations.search(params.query, directory, params.maxMatches, params.maxBytes);
1511
- const text =
1913
+ const matches =
1512
1914
  result.matches.length === 0 ? "No matches found." : result.matches.map((m) => `${m.path}:${m.lineNumber}: ${m.line.replace(/\n$/, "")}`).join("\n");
1915
+ const source = result.provenance ? `lexical via ${result.provenance.backend} (${result.provenance.indexState})\n` : "";
1916
+ const text = `${source}${matches}`;
1513
1917
  return { content: [{ type: "text", text }], details: { result } };
1514
1918
  },
1515
1919
  renderCall(args, theme, context) {
@@ -1751,7 +2155,7 @@ export default function (pi: ExtensionAPI) {
1751
2155
  const action = typeof args.action === "string" ? args.action : "";
1752
2156
  const path = typeof args.path === "string" ? args.path : "";
1753
2157
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
1754
- text.setText(`${theme.fg("toolTitle", theme.bold("mutation_history"))} ${theme.fg("accent", action)} ${theme.fg("dim", path)}`);
2158
+ text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("mutation_history", action)))} ${theme.fg("accent", path)}`);
1755
2159
  return text;
1756
2160
  },
1757
2161
  renderResult(result, { isPartial }, theme, context) {
@@ -1827,7 +2231,10 @@ export default function (pi: ExtensionAPI) {
1827
2231
  maxResults: params.maxResults,
1828
2232
  cursor: params.cursor,
1829
2233
  });
1830
- return { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
2234
+ return {
2235
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "list"), page) }],
2236
+ details: { action: "list", page },
2237
+ };
1831
2238
  }
1832
2239
  if (params.action === "remove") {
1833
2240
  if (!params.name || !params.resolvedVersion) throw new Error("package_source action=remove requires ecosystem, name, and resolvedVersion");
@@ -1837,11 +2244,17 @@ export default function (pi: ExtensionAPI) {
1837
2244
  params.name,
1838
2245
  params.resolvedVersion,
1839
2246
  );
1840
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "remove", result } };
2247
+ return {
2248
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "remove"), result) }],
2249
+ details: { action: "remove", result },
2250
+ };
1841
2251
  }
1842
2252
  if (params.action === "clean") {
1843
2253
  const result = await packageSourceOperations.clean(optionalPackageEcosystem(params.ecosystem));
1844
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "clean", result } };
2254
+ return {
2255
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "clean"), result) }],
2256
+ details: { action: "clean", result },
2257
+ };
1845
2258
  }
1846
2259
  if (!params.directory || !params.name) throw new Error("package_source action=resolve requires directory and name");
1847
2260
  const directory = resolve(cwd, params.directory);
@@ -1852,7 +2265,10 @@ export default function (pi: ExtensionAPI) {
1852
2265
  params.registry ?? null,
1853
2266
  optionalPackageEcosystem(params.ecosystem),
1854
2267
  );
1855
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "resolve", result } };
2268
+ return {
2269
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "resolve"), result) }],
2270
+ details: { action: "resolve", result },
2271
+ };
1856
2272
  },
1857
2273
  renderCall(args, theme, context) {
1858
2274
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -1949,12 +2365,18 @@ export default function (pi: ExtensionAPI) {
1949
2365
  params.forceRefresh,
1950
2366
  vehicleCall,
1951
2367
  );
1952
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "fetch", result } };
2368
+ return {
2369
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "fetch"), result) }],
2370
+ details: { action: "fetch", result },
2371
+ };
1953
2372
  }
1954
2373
  if (params.action === "evict") {
1955
2374
  if (!params.owner || !params.repo) throw new Error("repo_cache action=evict requires owner and repo");
1956
2375
  const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null, vehicleCall);
1957
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "evict", result } };
2376
+ return {
2377
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "evict"), result) }],
2378
+ details: { action: "evict", result },
2379
+ };
1958
2380
  }
1959
2381
  if (params.maxResults === undefined) throw new Error("repo_cache action=list requires maxResults");
1960
2382
  const page = await repoCacheListOperations.list(
@@ -1963,7 +2385,10 @@ export default function (pi: ExtensionAPI) {
1963
2385
  params.cursor,
1964
2386
  vehicleCall,
1965
2387
  );
1966
- return { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
2388
+ return {
2389
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "list"), page) }],
2390
+ details: { action: "list", page },
2391
+ };
1967
2392
  },
1968
2393
  renderCall(args, theme, context) {
1969
2394
  const action = args.action === "list" || args.action === "evict" ? args.action : "fetch";
@@ -2036,14 +2461,23 @@ export default function (pi: ExtensionAPI) {
2036
2461
  };
2037
2462
  if (params.action === "github_repos") {
2038
2463
  const result = await externalSearchOperations.githubRepos(params.query, maxResults, vehicleCall);
2039
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "github_repos", result } };
2464
+ return {
2465
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "github_repos"), result) }],
2466
+ details: { action: "github_repos", result },
2467
+ };
2040
2468
  }
2041
2469
  if (params.action === "npm_packages") {
2042
2470
  const result = await externalSearchOperations.npmPackages(params.query, maxResults, vehicleCall);
2043
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "npm_packages", result } };
2471
+ return {
2472
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "npm_packages"), result) }],
2473
+ details: { action: "npm_packages", result },
2474
+ };
2044
2475
  }
2045
2476
  const result = await externalSearchOperations.sourcegraphCode(params.query, maxResults, vehicleCall);
2046
- return { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "sourcegraph_code", result } };
2477
+ return {
2478
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "sourcegraph_code"), result) }],
2479
+ details: { action: "sourcegraph_code", result },
2480
+ };
2047
2481
  },
2048
2482
  renderCall(args, theme, context) {
2049
2483
  const action = args.action === "npm_packages" || args.action === "sourcegraph_code" ? args.action : "github_repos";
@@ -2051,7 +2485,7 @@ export default function (pi: ExtensionAPI) {
2051
2485
  text.setText(formatExternalSearchCall(action, args, theme));
2052
2486
  return text;
2053
2487
  },
2054
- renderResult(result, { isPartial }, theme, context) {
2488
+ renderResult(result, { expanded, isPartial }, theme, context) {
2055
2489
  if (isPartial) return new Text(theme.fg("warning", "Searching..."), 0, 0);
2056
2490
  if (context.isError) {
2057
2491
  const errorText = result.content
@@ -2062,9 +2496,9 @@ export default function (pi: ExtensionAPI) {
2062
2496
  }
2063
2497
  const details = result.details as ExternalSearchToolDetails | undefined;
2064
2498
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
2065
- if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, theme));
2066
- else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, theme));
2067
- else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, theme));
2499
+ if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, expanded, theme));
2500
+ else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, expanded, theme));
2501
+ else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, expanded, theme));
2068
2502
  return text;
2069
2503
  },
2070
2504
  });
@@ -2087,11 +2521,14 @@ export default function (pi: ExtensionAPI) {
2087
2521
  async execute(_toolCallId, params) {
2088
2522
  const directories = params.directories.map((directory) => resolve(cwd, directory));
2089
2523
  const results = await crossWorkspaceSearchOperations.findSymbols(params.query, directories, params.timeoutMs, params.maxResults);
2090
- return { content: [{ type: "text", text: JSON.stringify(results) }], details: { results } };
2524
+ return {
2525
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("find_symbols_across_projects"), results) }],
2526
+ details: { results },
2527
+ };
2091
2528
  },
2092
2529
  renderCall(args, theme, context) {
2093
2530
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
2094
- text.setText(formatCrossWorkspaceCall(args, theme));
2531
+ text.setText(formatCrossWorkspaceCall("find_symbols_across_projects", args, theme));
2095
2532
  return text;
2096
2533
  },
2097
2534
  renderResult(result, { expanded, isPartial }, theme, context) {
@@ -2114,7 +2551,7 @@ export default function (pi: ExtensionAPI) {
2114
2551
  name: "search_code_across_projects",
2115
2552
  label: "Search Code Across Projects",
2116
2553
  description:
2117
- "Fans out a ripgrep-backed text/regex search across several explicitly-named project directories at once and reports one outcome per project. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions. Each directory resolves to its OWN nearest project root (package.json/tsconfig.json/go.mod/Cargo.toml/...), not the outer repo's git root -- sibling packages under one monorepo stay distinct scopes rather than collapsing into one. A result's collapsedWith lists any other requested directories that genuinely did resolve to the same workspace; empty means it got its own.",
2554
+ "Fans out bounded indexed lexical text/regex search with explicit ripgrep fallback provenance across several explicitly-named project directories at once and reports one outcome per project. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions. Each directory resolves to its OWN nearest project root (package.json/tsconfig.json/go.mod/Cargo.toml/...), not the outer repo's git root -- sibling packages under one monorepo stay distinct scopes rather than collapsing into one. A result's collapsedWith lists any other requested directories that genuinely did resolve to the same workspace; empty means it got its own.",
2118
2555
  promptSnippet: "Search for a pattern across several projects at once",
2119
2556
  parameters: Type.Object({
2120
2557
  directories: Type.Array(Type.String(), { description: "Project directories to search, each absolute or relative to the current working directory" }),
@@ -2126,11 +2563,14 @@ export default function (pi: ExtensionAPI) {
2126
2563
  async execute(_toolCallId, params) {
2127
2564
  const directories = params.directories.map((directory) => resolve(cwd, directory));
2128
2565
  const results = await crossWorkspaceSearchOperations.searchText(params.query, directories, params.maxMatches, params.maxBytes, params.timeoutMs);
2129
- return { content: [{ type: "text", text: JSON.stringify(results) }], details: { results } };
2566
+ return {
2567
+ content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("search_code_across_projects"), results) }],
2568
+ details: { results },
2569
+ };
2130
2570
  },
2131
2571
  renderCall(args, theme, context) {
2132
2572
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
2133
- text.setText(formatCrossWorkspaceCall(args, theme));
2573
+ text.setText(formatCrossWorkspaceCall("search_code_across_projects", args, theme));
2134
2574
  return text;
2135
2575
  },
2136
2576
  renderResult(result, { expanded, isPartial }, theme, context) {