@mrclrchtr/supi-code-intelligence 6.4.0 → 7.0.1

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 (129) hide show
  1. package/README.md +164 -82
  2. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  3. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  4. package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  5. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  6. package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  7. package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  9. package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
  10. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  11. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  12. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  13. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  14. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  15. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  16. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  17. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
  18. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
  20. package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
  21. package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
  23. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
  24. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
  25. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
  26. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
  27. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
  28. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
  29. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
  30. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
  31. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
  32. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
  34. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
  35. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
  36. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
  42. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
  43. package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
  46. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
  47. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
  48. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
  49. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
  50. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
  51. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
  52. package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
  53. package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
  54. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
  55. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
  56. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
  57. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  58. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
  59. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
  60. package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
  61. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
  62. package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
  63. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
  64. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
  65. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
  66. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  67. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  68. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  69. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  70. package/node_modules/yaml/package.json +1 -1
  71. package/package.json +5 -5
  72. package/src/analysis/health/diagnostics.ts +1 -2
  73. package/src/analysis/health/file-scope.ts +4 -1
  74. package/src/analysis/health/server-status.ts +1 -1
  75. package/src/analysis/relations/callees.ts +17 -8
  76. package/src/analysis/relations/implementations.ts +1 -2
  77. package/src/analysis/relations/references.ts +8 -4
  78. package/src/analysis/relations/types.ts +6 -30
  79. package/src/analysis/target/anchored.ts +9 -1
  80. package/src/analysis/target/symbol.ts +29 -11
  81. package/src/analysis/target/types.ts +14 -6
  82. package/src/api.ts +2 -0
  83. package/src/session/graph/collect.ts +18 -3
  84. package/src/session/health-refresh.ts +12 -1
  85. package/src/session/health-types.ts +18 -6
  86. package/src/session/health-workflow.ts +21 -19
  87. package/src/session/orientation-types.ts +5 -6
  88. package/src/session/orientation-workflow.ts +10 -2
  89. package/src/session/target-workflow.ts +9 -6
  90. package/src/substrate/lsp/source-tracking.ts +11 -6
  91. package/src/tool/code_find/guidance.ts +2 -2
  92. package/src/tool/code_find/spec.ts +5 -4
  93. package/src/tool/code_find/tui.ts +7 -1
  94. package/src/tool/code_graph/details.ts +32 -0
  95. package/src/tool/code_graph/display.ts +54 -0
  96. package/src/tool/code_graph/format.ts +60 -0
  97. package/src/tool/code_graph/guidance.ts +1 -1
  98. package/src/tool/code_graph/markdown.ts +76 -198
  99. package/src/tool/code_graph/read-next.ts +65 -0
  100. package/src/tool/code_graph/result.ts +118 -83
  101. package/src/tool/code_graph/tui.ts +216 -54
  102. package/src/tool/code_health/guidance.ts +2 -4
  103. package/src/tool/code_health/markdown.ts +75 -58
  104. package/src/tool/code_health/refresh-outcome.ts +41 -1
  105. package/src/tool/code_health/refresh-status.ts +114 -48
  106. package/src/tool/code_health/scope.ts +78 -0
  107. package/src/tool/code_health/spec.ts +1 -1
  108. package/src/tool/code_health/tui.ts +25 -38
  109. package/src/tool/code_inspect/tui.ts +7 -1
  110. package/src/tool/code_orientation/result.ts +31 -8
  111. package/src/tool/code_orientation/tui.ts +26 -5
  112. package/src/tool/code_refactor_apply/guidance.ts +1 -1
  113. package/src/tool/code_refactor_apply/tui.ts +7 -3
  114. package/src/tool/code_refactor_plan/guidance.ts +1 -1
  115. package/src/tool/code_refactor_plan/tui.ts +7 -1
  116. package/src/tool/code_resolve/guidance.ts +2 -4
  117. package/src/tool/code_resolve/result.ts +13 -10
  118. package/src/tool/code_resolve/tui.ts +18 -1
  119. package/src/tool/infra/truncate.ts +39 -13
  120. package/src/tool/result/candidate-row.ts +27 -0
  121. package/src/tool/result/errors.ts +21 -6
  122. package/src/tool/result/types.ts +7 -3
  123. package/src/tool/schemas.ts +5 -0
  124. package/src/types/execution.ts +2 -0
  125. package/src/types/index.ts +2 -0
  126. package/src/ui/tui/candidate-legacy-body.ts +69 -0
  127. package/src/ui/tui/candidate-selection.ts +366 -0
  128. package/src/ui/tui/common.ts +26 -31
  129. package/src/ui/tui/execution-error.ts +92 -0
@@ -0,0 +1,65 @@
1
+ import { type ReadNextItem, readNextRange } from "../../analysis/read-next.ts";
2
+ import type { GraphSection } from "../../session/graph-types.ts";
3
+
4
+ /** Prefer the known enclosing scope, then alternate relation sites before the display cap. */
5
+ export function graphReadNext(sections: readonly GraphSection[]): ReadNextItem[] {
6
+ const calls = sections.find((section) => section.kind === "ok" && section.rel === "callees");
7
+ const scopeReads =
8
+ calls?.kind === "ok"
9
+ ? calls.readNext.map((item) => ({ ...item, reason: "inspect the enclosing scope" }))
10
+ : [];
11
+ const queues = sections.flatMap((section) => {
12
+ if (section.kind !== "ok" || section.rel === "callees") return [];
13
+ // Reference collection puts the approximate target range first. The known scope replaces it.
14
+ return [
15
+ section.rel === "references" && scopeReads.length > 0
16
+ ? section.readNext.slice(1)
17
+ : section.readNext,
18
+ ];
19
+ });
20
+ const candidates = [...scopeReads];
21
+ const maxLength = Math.max(0, ...queues.map((queue) => queue.length));
22
+ for (let index = 0; index < maxLength; index++) {
23
+ for (const queue of queues) {
24
+ const item = queue[index];
25
+ if (item) candidates.push(item);
26
+ }
27
+ }
28
+ return mergeRanges(candidates)
29
+ .slice(0, 3)
30
+ .map((item) => readNextRange(item));
31
+ }
32
+
33
+ /** Merge connected ranges before bounding them, including ranges that bridge two earlier reads. */
34
+ function mergeRanges(items: readonly ReadNextItem[]): ReadNextItem[] {
35
+ const result: ReadNextItem[] = [];
36
+ for (const item of items) {
37
+ const merged = { ...item };
38
+ let firstIndex = result.length;
39
+ let index = 0;
40
+ while (index < result.length) {
41
+ const other = result[index];
42
+ if (
43
+ other.file === merged.file &&
44
+ other.startLine <= merged.endLine &&
45
+ merged.startLine <= other.endLine
46
+ ) {
47
+ firstIndex = Math.min(firstIndex, index);
48
+ if (
49
+ merged.reason !== other.reason ||
50
+ merged.startLine !== other.startLine ||
51
+ merged.endLine !== other.endLine
52
+ )
53
+ merged.reason = "inspect related source ranges";
54
+ merged.startLine = Math.min(merged.startLine, other.startLine);
55
+ merged.endLine = Math.max(merged.endLine, other.endLine);
56
+ result.splice(index, 1);
57
+ index = 0;
58
+ } else {
59
+ index++;
60
+ }
61
+ }
62
+ result.splice(firstIndex, 0, merged);
63
+ }
64
+ return result;
65
+ }
@@ -1,5 +1,10 @@
1
1
  import type { ConfidenceMode } from "@mrclrchtr/supi-code-runtime/api";
2
- import { createEvidenceList, type EvidenceList } from "../../analysis/evidence.ts";
2
+ import type { EvidenceListMetadata } from "../../analysis/evidence.ts";
3
+ import {
4
+ createEvidenceList,
5
+ createPartialEvidenceList,
6
+ type EvidenceList,
7
+ } from "../../analysis/evidence.ts";
3
8
  import type { RelationLocationPartialReason } from "../../analysis/relations/provider-locations.ts";
4
9
  import type {
5
10
  CallEntry,
@@ -9,13 +14,18 @@ import type {
9
14
  import { toDisplayPath } from "../../analysis/search/paths.ts";
10
15
  import type { GraphSection } from "../../session/graph-types.ts";
11
16
  import {
12
- assembledNextQueries,
17
+ assembledReadNext,
13
18
  assembleToolResult,
14
19
  type ResultProvenance,
15
20
  type ToolResultAssembly,
16
21
  } from "../result/assembly.ts";
22
+ import { formatCandidateRow } from "../result/candidate-row.ts";
17
23
  import { createToolDisplaySection } from "../result/display.ts";
18
- import type { SearchDetails, ToolDisplaySection } from "../result/types.ts";
24
+ import type { ToolDisplaySection } from "../result/types.ts";
25
+ import type { GraphDetails, GraphFileGroup, GraphSectionDetails } from "./details.ts";
26
+ import { graphDisplaySection } from "./display.ts";
27
+ import { formatGraphEvidence } from "./format.ts";
28
+ import { graphReadNext } from "./read-next.ts";
19
29
 
20
30
  export type { GraphRelationKind, GraphSection } from "../../session/graph-types.ts";
21
31
 
@@ -40,7 +50,7 @@ export interface GraphResultAssembly {
40
50
  resolvedDisplayFile: string;
41
51
  cwd: string;
42
52
  assembled: ToolResultAssembly<{ readonly sections: readonly AssembledGraphSection[] }>;
43
- details: SearchDetails;
53
+ details: GraphDetails;
44
54
  displaySections: readonly ToolDisplaySection[];
45
55
  }
46
56
 
@@ -74,13 +84,14 @@ export function assembleGraphResult(input: {
74
84
  })),
75
85
  evidenceLists,
76
86
  nextQueries: [],
77
- readNext: sections.flatMap((section) => (section.kind === "ok" ? section.readNext : [])),
87
+ readNext: graphReadNext(sections),
78
88
  candidateCount,
79
89
  confidence,
80
90
  provenance,
81
91
  });
82
92
 
83
- const displaySections = sections.map((section) => graphDisplaySection(section, input.cwd));
93
+ const displays = sections.map((section) => graphDisplaySection(section, input.cwd));
94
+ const displaySections = displays.map(({ display }) => display);
84
95
 
85
96
  return {
86
97
  displayName: input.displayName,
@@ -90,51 +101,36 @@ export function assembleGraphResult(input: {
90
101
  assembled,
91
102
  displaySections,
92
103
  details: {
93
- confidence,
94
- scope: null,
95
- candidateCount,
96
- omittedCount: assembled.totals.omittedCount,
104
+ targetName: input.displayName,
105
+ targetFile: input.resolvedDisplayFile,
106
+ sections: sections.map((section, index) =>
107
+ graphSectionDetails(section, input.cwd, displays[index].fileGroups),
108
+ ),
109
+ readNext: assembledReadNext(assembled),
97
110
  evidenceLists: [...assembled.evidenceLists],
98
- nextQueries: assembledNextQueries(assembled),
99
111
  },
100
112
  };
101
113
  }
102
114
 
103
- function graphDisplaySection(section: AssembledGraphSection, cwd: string): ToolDisplaySection {
104
- if (section.kind === "unavailable") {
105
- return createToolDisplaySection({
106
- key: `graph.${section.rel}`,
107
- title: section.rel,
108
- items: [section.message],
109
- format: (message) => `Unavailable — ${message}`,
110
- totalCount: 1,
111
- });
112
- }
113
-
114
- return createToolDisplaySection({
115
- key: `graph.${section.rel}`,
116
- title: section.rel,
117
- items: section.evidence.items as readonly unknown[],
118
- totalCount: section.evidence.metadata.totalCount,
119
- omittedCount: section.evidence.metadata.omittedCount,
120
- partialReason: section.evidence.metadata.partialReason,
121
- format: (item) => formatGraphDisplayItem(section.rel, item, cwd),
122
- });
123
- }
124
-
125
- function formatGraphDisplayItem(
126
- relation: "references" | "callees" | "implements",
127
- item: unknown,
115
+ function graphSectionDetails(
116
+ section: AssembledGraphSection,
128
117
  cwd: string,
129
- ): string {
130
- if (relation === "callees") {
131
- const call = item as { name: string; file: string; line: number };
132
- return `${call.name} — ${toDisplayPath(cwd, call.file)}:L${call.line}`;
133
- }
134
-
135
- const location = item as { name: string | null; file: string; line: number; character: number };
136
- const name = location.name ? `${location.name} — ` : "";
137
- return `${name}${toDisplayPath(cwd, location.file)}:L${location.line}:${location.character}`;
118
+ fileGroups: GraphFileGroup[],
119
+ ): GraphSectionDetails {
120
+ const calls = section.kind === "ok" && section.rel === "callees" ? section.data : null;
121
+ return {
122
+ rel: section.rel,
123
+ source: section.rel === "callees" ? "structural" : "semantic",
124
+ status: section.kind === "ok" ? evidenceStatus(section.evidence) : "unavailable",
125
+ message: section.kind === "unavailable" ? section.message : null,
126
+ externalCount:
127
+ section.kind === "ok" && section.rel !== "callees" ? section.data.externalCount : 0,
128
+ enclosingScope: calls
129
+ ? { ...calls.enclosingScope, file: toDisplayPath(cwd, calls.enclosingScope.file) }
130
+ : null,
131
+ depth: calls?.depth ?? null,
132
+ fileGroups,
133
+ };
138
134
  }
139
135
 
140
136
  function assembleGraphSection(section: GraphSection, maxResults: number): AssembledGraphSection {
@@ -181,11 +177,10 @@ function createRelationEvidenceList<T>(params: {
181
177
  invalidLocationCount: number;
182
178
  partialReason: RelationLocationPartialReason | null;
183
179
  }): EvidenceList<T> {
184
- const evidence = createEvidenceList({
185
- key: params.key,
186
- items: params.items,
187
- maxResults: params.maxResults,
188
- });
180
+ const evidence =
181
+ params.partialReason === "provider-limited"
182
+ ? createPartialEvidenceList({ ...params, partialReason: params.partialReason })
183
+ : createEvidenceList(params);
189
184
  if (params.partialReason === null) return evidence;
190
185
  return {
191
186
  ...evidence,
@@ -247,38 +242,7 @@ export function finishGraphResult(outcome: GraphOutcome, cwd: string): CodeIntel
247
242
  return searchErrorResult(`**Error:** ${outcome.message}`, { message: outcome.message });
248
243
  }
249
244
  if (outcome.kind === "disambiguation" || outcome.kind === "kind-mismatch") {
250
- const lines = [
251
- outcome.kind === "kind-mismatch"
252
- ? `**No target matched provider kind \`${outcome.requestedKind}\`. Near matches:**`
253
- : "**Target is ambiguous. Choose one candidate handle:**",
254
- "",
255
- ];
256
- for (const candidate of outcome.candidates) {
257
- lines.push(
258
- `- \`${candidate.targetId}\` — ${candidate.name} (\`${candidate.kind ?? "unknown"}\`) at ${candidate.file}:${candidate.line}:${candidate.character}`,
259
- );
260
- }
261
- if (outcome.kind === "kind-mismatch") {
262
- lines.push(
263
- "",
264
- "Retry without `symbolKind`, use an observed provider kind, or choose a handle.",
265
- );
266
- }
267
- return searchErrorResult(lines.join("\n"), {
268
- message:
269
- outcome.kind === "kind-mismatch"
270
- ? `No target matched provider kind ${outcome.requestedKind}.`
271
- : "The target is ambiguous.",
272
- displaySections: [
273
- createToolDisplaySection({
274
- key: "graph.candidates",
275
- title: "Candidates",
276
- items: outcome.candidates,
277
- format: (candidate) =>
278
- `${candidate.targetId} — ${candidate.name} (${candidate.kind ?? "unknown"}) at ${candidate.file}:${candidate.line}:${candidate.character}`,
279
- }),
280
- ],
281
- });
245
+ return finishGraphCandidates(outcome);
282
246
  }
283
247
 
284
248
  const assembly = assembleGraphResult({
@@ -288,13 +252,84 @@ export function finishGraphResult(outcome: GraphOutcome, cwd: string): CodeIntel
288
252
  maxResults: outcome.maxResults,
289
253
  cwd,
290
254
  });
255
+ const displayTruncated = hasHiddenDisplayEvidence(assembly);
291
256
  return {
292
257
  content: renderGraphResult(assembly),
293
258
  details: {
294
- type: "search",
259
+ type: "graph",
295
260
  data: assembly.details,
296
261
  status: "completed",
297
262
  displaySections: assembly.displaySections,
263
+ ...(displayTruncated ? { truncation: { truncated: false, displayTruncated: true } } : {}),
298
264
  },
299
265
  };
300
266
  }
267
+
268
+ function finishGraphCandidates(
269
+ outcome: Extract<GraphOutcome, { kind: "disambiguation" | "kind-mismatch" }>,
270
+ ): CodeIntelResult {
271
+ const lines = [
272
+ outcome.kind === "kind-mismatch"
273
+ ? `**No target matched provider kind \`${outcome.requestedKind}\`. Near matches:**`
274
+ : "**Target is ambiguous. Choose one candidate handle:**",
275
+ "",
276
+ ];
277
+ for (const candidate of outcome.candidates) {
278
+ lines.push(
279
+ `- \`${candidate.targetId}\` — ${candidate.name} (\`${candidate.kind ?? "unknown"}\`) at ${candidate.file}:${candidate.line}:${candidate.character}`,
280
+ );
281
+ }
282
+ const evidence = graphCandidateEvidence(outcome);
283
+ lines.push("", formatGraphEvidence(evidence, "candidates"));
284
+ if (outcome.kind === "kind-mismatch") {
285
+ lines.push(
286
+ "",
287
+ "Retry without `symbolKind`, use an observed provider kind, or choose a handle.",
288
+ );
289
+ }
290
+ const displaySections = [
291
+ createToolDisplaySection({
292
+ key: "graph.candidates",
293
+ title: "Candidates",
294
+ items: outcome.candidates,
295
+ totalCount: evidence.totalCount,
296
+ omittedCount: evidence.omittedCount,
297
+ partialReason: evidence.partialReason,
298
+ format: (candidate) => formatCandidateRow(candidate, "handle-first"),
299
+ }),
300
+ ];
301
+ return searchErrorResult(lines.join("\n"), {
302
+ confidence: "semantic",
303
+ evidenceLists: [evidence],
304
+ nextQueries:
305
+ outcome.kind === "kind-mismatch"
306
+ ? ["Retry without symbolKind, use an observed provider kind, or choose a near-match handle"]
307
+ : ["Choose one candidate handle, or narrow the symbol selector with scope or symbolKind"],
308
+ status: outcome.kind === "disambiguation" ? "disambiguation" : "invalid-input",
309
+ message:
310
+ outcome.kind === "kind-mismatch"
311
+ ? `No target matched provider kind ${outcome.requestedKind}.`
312
+ : "The target is ambiguous.",
313
+ displaySections,
314
+ });
315
+ }
316
+
317
+ function graphCandidateEvidence(
318
+ outcome: Extract<GraphOutcome, { kind: "disambiguation" | "kind-mismatch" }>,
319
+ ): EvidenceListMetadata {
320
+ return {
321
+ key: "graph.candidates",
322
+ totalCount: outcome.totalCount,
323
+ shownCount: outcome.candidates.length,
324
+ omittedCount: outcome.omittedCount,
325
+ partialReason: outcome.partialReason,
326
+ };
327
+ }
328
+
329
+ function hasHiddenDisplayEvidence(assembly: GraphResultAssembly): boolean {
330
+ return assembly.sections.some(
331
+ (section, index) =>
332
+ section.kind === "ok" &&
333
+ assembly.displaySections[index].shownCount < section.evidence.items.length,
334
+ );
335
+ }
@@ -1,28 +1,34 @@
1
- /** TUI renderer for code_graph. */
1
+ /** Human transcript renderer for code_graph. */
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
- import { Container, Spacer, Text } from "@earendil-works/pi-tui";
3
+ import { type Component, Container, Spacer, Text } from "@earendil-works/pi-tui";
4
+ import { renderCandidateSelection } from "../../ui/tui/candidate-selection.ts";
4
5
  import {
5
6
  buildSimpleCompact,
6
- buildSimpleHeader,
7
7
  type EvidenceEntry,
8
8
  formatCallPath,
9
9
  formatCallValue,
10
10
  type ResultOptios,
11
+ readEvidenceEntries,
11
12
  renderDomainError,
12
- renderDomainResult,
13
- renderEvidenceLines,
14
13
  renderExecutionError,
15
14
  renderMarkdownDetail,
16
15
  renderPartial,
17
- renderStructuredDetailBody,
18
16
  renderToolDisplaySections,
19
17
  renderTruncationDisclosure,
20
18
  type ToolRendererContext,
21
19
  type ToolResult,
22
20
  } from "../../ui/tui/common.ts";
21
+ import {
22
+ MAX_TUI_DISPLAY_ITEMS,
23
+ readToolDisplaySections,
24
+ truncateDisplayText,
25
+ } from "../result/display.ts";
26
+ import type { ToolDisplaySection } from "../result/types.ts";
27
+ import type { GraphFileGroup } from "./details.ts";
23
28
  import type { CodeGraphToolParams, GraphRelation } from "./execute.ts";
29
+ import { compactGraphLabel, formatGraphEvidence, formatGraphRange } from "./format.ts";
24
30
 
25
- /** Render the compact code_graph call header. */
31
+ /** Render the compact call header from the requested selector. */
26
32
  export function renderGraphCall(
27
33
  args: unknown,
28
34
  theme: Theme,
@@ -30,84 +36,240 @@ export function renderGraphCall(
30
36
  ): Text {
31
37
  const params = (args ?? {}) as Partial<CodeGraphToolParams>;
32
38
  const relations: GraphRelation[] = Array.isArray(params.relations)
33
- ? (params.relations as GraphRelation[])
39
+ ? params.relations
34
40
  : ["references"];
35
- const relationLabel = formatRelations(relations);
36
-
37
- let content = theme.fg("toolTitle", "code_graph");
38
- if (relationLabel) content += ` ${theme.fg("accent", relationLabel)}`;
39
-
41
+ const label = formatRelations(relations);
40
42
  const target = formatTarget(params.target);
41
- if (target) content += ` ${theme.fg("muted", target)}`;
42
-
43
- return new Text(content, 0, 0);
43
+ return new Text(
44
+ theme.fg("toolTitle", "code_graph") +
45
+ (label ? ` ${theme.fg("accent", label)}` : "") +
46
+ (target ? ` ${theme.fg("muted", target)}` : ""),
47
+ 0,
48
+ 0,
49
+ );
44
50
  }
45
51
 
46
- /** Render code_graph progress, status, and structured result details. */
52
+ /** Render one structured body, with no duplicate agent Markdown. */
47
53
  export function renderGraphResult(
48
54
  result: ToolResult,
49
55
  options: ResultOptios,
50
56
  theme: Theme,
51
57
  context: ToolRendererContext | undefined,
52
- ): Container | Text {
58
+ ): Component {
53
59
  if (options.isPartial) return renderPartial("Collecting relations…", theme);
54
-
55
- const details =
56
- result.details?.type === "search" ? (result.details.data as Record<string, unknown>) : null;
57
- const executionError = renderExecutionError(context, "code_graph failed", theme);
60
+ const executionError = renderExecutionError(
61
+ result,
62
+ { isError: context?.isError, expanded: options.expanded, label: "code_graph failed" },
63
+ theme,
64
+ );
58
65
  if (executionError) return executionError;
66
+ const candidateSelection = renderCandidateSelection(result, options, theme, "graph.candidates");
67
+ if (candidateSelection) return candidateSelection;
59
68
  const domainError = renderDomainError(result, theme);
60
- if (domainError) return renderDomainResult(result, options, theme, domainError);
69
+ if (domainError) return renderGraphDomainError(result, options.expanded, domainError, theme);
61
70
 
62
- if (!options.expanded) {
63
- const compact = buildSimpleCompact(details ?? undefined, theme);
64
- const truncation = renderTruncationDisclosure(result, theme);
65
- if (!truncation) return compact;
66
- const container = new Container();
67
- container.addChild(compact);
71
+ const data = asRecord(result.details?.data);
72
+ const container = new Container();
73
+ if (result.details?.type === "graph" && data) {
74
+ renderGraphBody(data, result, { container, expanded: options.expanded, theme });
75
+ } else {
76
+ // Older persisted results have only generic search details.
77
+ container.addChild(buildSimpleCompact(data ?? undefined, theme));
78
+ if (options.expanded)
79
+ renderToolDisplaySections(container, result.details?.displaySections, theme);
80
+ }
81
+ const truncation = renderTruncationDisclosure(result, theme);
82
+ if (truncation) {
68
83
  container.addChild(new Spacer(1));
69
84
  container.addChild(truncation);
70
- return container;
71
85
  }
86
+ return container;
87
+ }
72
88
 
73
- const container = new Container();
74
- const header = buildSimpleHeader(details ?? undefined, theme);
75
- if (header) container.addChild(header);
89
+ interface SectionView {
90
+ rel: string;
91
+ label: string;
92
+ unavailable: boolean;
93
+ message: string | null;
94
+ externalCount: number;
95
+ scope: Record<string, unknown> | null;
96
+ fileGroups: GraphFileGroup[];
97
+ }
76
98
 
77
- const evidenceLists = details?.evidenceLists as EvidenceEntry[] | undefined;
78
- if (evidenceLists && evidenceLists.length > 0) {
99
+ function renderGraphBody(
100
+ data: Record<string, unknown>,
101
+ result: ToolResult,
102
+ options: { container: Container; expanded: boolean; theme: Theme },
103
+ ): void {
104
+ const { container, expanded, theme } = options;
105
+ const target =
106
+ typeof data.targetName === "string" ? compactGraphLabel(data.targetName) : "Target";
107
+ const file = typeof data.targetFile === "string" ? truncateDisplayText(data.targetFile) : "";
108
+ container.addChild(new Text(theme.fg("accent", `${target}${file ? ` — ${file}` : ""}`), 0, 0));
109
+ const evidence = readEvidenceEntries(data.evidenceLists);
110
+ const displays = readToolDisplaySections(result.details?.displaySections);
111
+ const sections = readSections(data.sections);
112
+ const summaries: string[] = [];
113
+ for (const section of sections) {
114
+ const display = displays.find((entry) => entry.key === `graph.${section.rel}`);
115
+ const metadata = evidence.find((entry) => entry.key.startsWith(`${section.rel}.`));
116
+ const summary = sectionSummary(section, metadata, display);
117
+ if (!expanded) {
118
+ summaries.push(summary);
119
+ continue;
120
+ }
79
121
  container.addChild(new Spacer(1));
80
- renderEvidenceLines(container, evidenceLists, theme);
122
+ container.addChild(
123
+ new Text(theme.fg(section.unavailable ? "warning" : "accent", summary), 0, 0),
124
+ );
125
+ renderRelationBody(container, { section, display, targetName: data.targetName }, theme);
81
126
  }
127
+ if (!expanded)
128
+ container.addChild(
129
+ new Text(theme.fg("muted", summaries.join(" · ") || "No graph sections"), 0, 0),
130
+ );
131
+ }
82
132
 
83
- renderToolDisplaySections(container, result.details?.displaySections, theme);
84
- renderStructuredDetailBody(container, details ?? undefined, theme);
85
- const truncation = renderTruncationDisclosure(result, theme);
86
- if (truncation) {
87
- container.addChild(new Spacer(1));
88
- container.addChild(truncation);
133
+ function renderRelationBody(
134
+ container: Container,
135
+ input: { section: SectionView; display: ToolDisplaySection | undefined; targetName: unknown },
136
+ theme: Theme,
137
+ ): void {
138
+ const { section, display, targetName } = input;
139
+ if (section.unavailable) {
140
+ if (section.message)
141
+ container.addChild(new Text(theme.fg("warning", truncateDisplayText(section.message)), 0, 0));
142
+ return;
143
+ }
144
+ const scope = scopeLabel(section.scope, targetName);
145
+ if (scope) container.addChild(new Text(theme.fg("dim", scope), 0, 0));
146
+ const rows = display?.lines ?? [];
147
+ let index = 0;
148
+ const groups =
149
+ section.fileGroups.reduce((sum, group) => sum + group.count, 0) === rows.length
150
+ ? section.fileGroups
151
+ : [];
152
+ for (const group of groups) {
153
+ if (index >= rows.length) break;
154
+ container.addChild(new Text(theme.fg("dim", group.file), 0, 0));
155
+ for (const row of rows.slice(index, index + group.count))
156
+ container.addChild(new Text(theme.fg("muted", ` ${row}`), 0, 0));
157
+ index += group.count;
89
158
  }
90
- renderMarkdownDetail(container, result, theme);
159
+ // Older results contain full paths in each row and have no file groups.
160
+ for (const row of rows.slice(index)) container.addChild(new Text(theme.fg("muted", row), 0, 0));
161
+ }
162
+
163
+ function renderGraphDomainError(
164
+ result: ToolResult,
165
+ expanded: boolean,
166
+ domainError: Text,
167
+ theme: Theme,
168
+ ): Container | Text {
169
+ if (!expanded) return domainError;
170
+ const container = new Container();
171
+ container.addChild(domainError);
172
+ const displays = readToolDisplaySections(result.details?.displaySections);
173
+ renderToolDisplaySections(container, displays, theme);
174
+ if (displays.length === 0) renderMarkdownDetail(container, result, theme);
91
175
  return container;
92
176
  }
93
177
 
178
+ function sectionSummary(
179
+ section: SectionView,
180
+ metadata: EvidenceEntry | undefined,
181
+ display: ToolDisplaySection | undefined,
182
+ ): string {
183
+ if (section.unavailable) return `${section.label}: unavailable`;
184
+ if (!metadata) return `${section.label}: evidence unavailable`;
185
+ // The display has its own bounded row count; the underlying evidence total is unchanged.
186
+ const shown = display
187
+ ? { ...metadata, shownCount: display.shownCount, omittedCount: display.omittedCount }
188
+ : metadata;
189
+ return `${section.label}: ${formatGraphEvidence(shown, section.rel === "callees" ? "call sites" : "locations", section.externalCount)}`;
190
+ }
191
+
192
+ function readSections(value: unknown): SectionView[] {
193
+ if (!Array.isArray(value)) return [];
194
+ return value.slice(0, 3).flatMap((item) => {
195
+ const section = asRecord(item);
196
+ if (
197
+ !section ||
198
+ typeof section.rel !== "string" ||
199
+ !["references", "callees", "implements"].includes(section.rel) ||
200
+ (section.source !== "structural" && section.source !== "semantic")
201
+ )
202
+ return [];
203
+ const rel = section.rel;
204
+ const source = section.source;
205
+ const depth =
206
+ rel === "callees" && (section.depth === "direct" || section.depth === "deep")
207
+ ? `, ${section.depth}`
208
+ : "";
209
+ return [
210
+ {
211
+ rel,
212
+ label: `${rel} (${source}${depth})`,
213
+ unavailable: section.status === "unavailable",
214
+ message: typeof section.message === "string" ? section.message : null,
215
+ externalCount: typeof section.externalCount === "number" ? section.externalCount : 0,
216
+ scope: asRecord(section.enclosingScope),
217
+ fileGroups: readFileGroups(section.fileGroups),
218
+ },
219
+ ];
220
+ });
221
+ }
222
+
223
+ function readFileGroups(value: unknown): GraphFileGroup[] {
224
+ if (!Array.isArray(value)) return [];
225
+ const groups: GraphFileGroup[] = [];
226
+ for (const entry of value.slice(0, MAX_TUI_DISPLAY_ITEMS)) {
227
+ const group = asRecord(entry);
228
+ if (
229
+ !group ||
230
+ typeof group.file !== "string" ||
231
+ typeof group.count !== "number" ||
232
+ !Number.isInteger(group.count) ||
233
+ group.count < 1 ||
234
+ group.count > MAX_TUI_DISPLAY_ITEMS
235
+ )
236
+ return [];
237
+ groups.push({ file: truncateDisplayText(group.file), count: group.count });
238
+ }
239
+ return groups;
240
+ }
241
+
242
+ function scopeLabel(scope: Record<string, unknown> | null, targetName: unknown): string | null {
243
+ if (!scope || typeof scope.startLine !== "number" || typeof scope.endLine !== "number")
244
+ return null;
245
+ const name =
246
+ typeof scope.name === "string" && scope.name !== targetName
247
+ ? `${compactGraphLabel(scope.name)} — `
248
+ : "";
249
+ const range = formatGraphRange({ startLine: scope.startLine, endLine: scope.endLine });
250
+ return `Scope: ${name}${range}; source expressions, not resolved symbols`;
251
+ }
252
+
253
+ function asRecord(value: unknown): Record<string, unknown> | null {
254
+ return value !== null && typeof value === "object" && !Array.isArray(value)
255
+ ? (value as Record<string, unknown>)
256
+ : null;
257
+ }
258
+
94
259
  function formatRelations(relations: readonly GraphRelation[]): string {
95
- if (relations.length === 0) return "";
96
- if (relations.length === 1) return `→ ${relations[0]}`;
97
- if (relations.length <= 3) return `→ ${relations.join(", ")}`;
98
- return `→ ${relations.slice(0, 2).join(", ")} +${relations.length - 2}`;
260
+ return relations.length ? `→ ${relations.slice(0, 3).join(", ")}` : "";
99
261
  }
100
262
 
101
263
  function formatTarget(target: unknown): string {
102
- if (!target || typeof target !== "object") return "";
103
- const record = target as Record<string, unknown>;
104
-
105
- if (record.symbol && typeof record.symbol === "object") {
106
- const query = formatCallValue((record.symbol as Record<string, unknown>).query);
264
+ const record = asRecord(target);
265
+ if (!record) return "";
266
+ const symbol = asRecord(record.symbol);
267
+ if (symbol) {
268
+ const query = formatCallValue(symbol.query);
107
269
  return query ? `of ${query}` : "";
108
270
  }
109
- if (record.anchor && typeof record.anchor === "object") {
110
- const anchor = record.anchor as Record<string, unknown>;
271
+ const anchor = asRecord(record.anchor);
272
+ if (anchor) {
111
273
  const file = formatCallPath(anchor.file);
112
274
  const line = typeof anchor.line === "number" ? `:${anchor.line}` : "";
113
275
  return file ? `at ${file}${line}` : "";