@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,69 @@
1
+ /** Bounded raw Markdown rendering for legacy target-selection results. */
2
+ import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
3
+ import { type Component, Markdown, Text, truncateToWidth } from "@earendil-works/pi-tui";
4
+
5
+ /** Maximum source characters rendered by the legacy candidate body. */
6
+ const MAX_LEGACY_SOURCE_CHARACTERS = 4096;
7
+
8
+ /** Maximum physical rows rendered by the legacy candidate body. */
9
+ const MAX_LEGACY_BODY_ROWS = 40;
10
+
11
+ const LEGACY_TRUNCATION_NOTICE = `Legacy candidate body truncated for display (source limit: ${MAX_LEGACY_SOURCE_CHARACTERS} characters; body limit: ${MAX_LEGACY_BODY_ROWS} rows).`;
12
+
13
+ /** Read valid Markdown text blocks without trusting persisted content shape. */
14
+ export function readLegacyCandidateText(value: unknown): string {
15
+ if (!Array.isArray(value)) return "";
16
+ return value
17
+ .flatMap((block) => {
18
+ if (block === null || typeof block !== "object" || Array.isArray(block)) return [];
19
+ const record = block as Record<string, unknown>;
20
+ return record.type === "text" && typeof record.text === "string" ? [record.text] : [];
21
+ })
22
+ .join("\n");
23
+ }
24
+
25
+ /** Create a bounded legacy candidate body, or null when no valid text exists. */
26
+ export function createLegacyCandidateBody(markdownText: string, theme: Theme): Component | null {
27
+ if (!markdownText.trim()) return null;
28
+ return new BoundedLegacyCandidateBody(markdownText, theme);
29
+ }
30
+
31
+ class BoundedLegacyCandidateBody implements Component {
32
+ private readonly markdownText: string;
33
+ private readonly sourceTruncated: boolean;
34
+
35
+ constructor(
36
+ markdownText: string,
37
+ private readonly theme: Theme,
38
+ ) {
39
+ const sourceCharacters = Array.from(markdownText);
40
+ this.sourceTruncated = sourceCharacters.length > MAX_LEGACY_SOURCE_CHARACTERS;
41
+ this.markdownText = this.sourceTruncated
42
+ ? sourceCharacters.slice(0, MAX_LEGACY_SOURCE_CHARACTERS).join("")
43
+ : markdownText;
44
+ }
45
+
46
+ render(width: number): string[] {
47
+ const safeWidth = Math.max(1, width);
48
+ const markdownRows = new Markdown(this.markdownText, 0, 0, getMarkdownTheme())
49
+ .render(safeWidth)
50
+ .map((row) => truncateToWidth(row, safeWidth, ""));
51
+
52
+ if (!this.sourceTruncated && markdownRows.length <= MAX_LEGACY_BODY_ROWS) {
53
+ return markdownRows;
54
+ }
55
+
56
+ const noticeRows = new Text(this.theme.fg("warning", LEGACY_TRUNCATION_NOTICE), 0, 0).render(
57
+ safeWidth,
58
+ );
59
+ const markdownRowBudget = Math.max(0, MAX_LEGACY_BODY_ROWS - noticeRows.length);
60
+ return [...markdownRows.slice(0, markdownRowBudget), ...noticeRows].slice(
61
+ 0,
62
+ MAX_LEGACY_BODY_ROWS,
63
+ );
64
+ }
65
+
66
+ invalidate(): void {
67
+ // The Markdown and notice components are rebuilt for each render.
68
+ }
69
+ }
@@ -0,0 +1,366 @@
1
+ /** Shared human transcript renderer for target-candidate selections. */
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { type Component, Container, Spacer, Text } from "@earendil-works/pi-tui";
4
+ import { formatCandidateRow } from "../../tool/result/candidate-row.ts";
5
+ import {
6
+ MAX_TUI_DISPLAY_ITEMS,
7
+ readToolDisplaySections,
8
+ truncateDisplayText,
9
+ } from "../../tool/result/display.ts";
10
+ import type { ToolDisplaySection } from "../../tool/result/types.ts";
11
+ import { createLegacyCandidateBody, readLegacyCandidateText } from "./candidate-legacy-body.ts";
12
+ import {
13
+ type EvidenceEntry,
14
+ formatEvidenceEntry,
15
+ type ResultOptios,
16
+ readEvidenceEntries,
17
+ renderTruncationDisclosure,
18
+ type ToolResult,
19
+ } from "./common.ts";
20
+
21
+ /** Display-section keys that identify candidate-selection results. */
22
+ export type CandidateSelectionKey =
23
+ | "graph.candidates"
24
+ | "resolve.candidates"
25
+ | "orientation.candidates";
26
+
27
+ interface CandidateRow {
28
+ targetId: string;
29
+ name: string;
30
+ kind: string | null;
31
+ file: string;
32
+ line: number;
33
+ character: number;
34
+ rank: number;
35
+ container: string | null;
36
+ }
37
+
38
+ interface CandidateSelectionData {
39
+ reason: string | null;
40
+ evidence: EvidenceEntry | null;
41
+ display: ToolDisplaySection | null;
42
+ rows: string[];
43
+ guidance: string[];
44
+ legacyGuidance: string[];
45
+ legacyMarkdownText: string;
46
+ rawFallback: boolean;
47
+ }
48
+
49
+ /** Render a candidate-selection result, or return null for unrelated results. */
50
+ export function renderCandidateSelection(
51
+ result: ToolResult,
52
+ options: ResultOptios,
53
+ theme: Theme,
54
+ key: CandidateSelectionKey,
55
+ ): Component | null {
56
+ const selection = readCandidateSelection(result, key);
57
+ if (!selection) return null;
58
+ return new CandidateSelectionComponent(result, options, theme, selection);
59
+ }
60
+
61
+ function readCandidateSelection(
62
+ result: ToolResult,
63
+ key: CandidateSelectionKey,
64
+ ): CandidateSelectionData | null {
65
+ const data = asRecord(result.details?.data);
66
+ const legacyMarkdownText = readLegacyCandidateText(result.content);
67
+ const display =
68
+ readToolDisplaySections(result.details?.displaySections).find(
69
+ (section) => section.key === key,
70
+ ) ?? null;
71
+ const evidence =
72
+ readEvidenceEntries(data?.evidenceLists).find((entry) => entry.key === key) ?? null;
73
+ const structuredCandidates = readCandidateRows(data?.candidates);
74
+
75
+ // A status or message alone is not a candidate-selection marker. Require a
76
+ // candidate key or a structured candidate collection before changing views.
77
+ if (!display && !evidence && !structuredCandidates) return null;
78
+
79
+ const rows = candidateRowsFor(display, structuredCandidates, key);
80
+ const selectionEvidence = candidateEvidenceFor({
81
+ display,
82
+ evidence,
83
+ structuredCandidates,
84
+ rows,
85
+ key,
86
+ });
87
+ const reason = selectionReason(result, data);
88
+ const structuredGuidance = readStructuredNextQueries(data);
89
+ const guidance = selectionGuidance(result, data, key);
90
+
91
+ return {
92
+ reason,
93
+ evidence: selectionEvidence,
94
+ display,
95
+ rows,
96
+ guidance,
97
+ legacyGuidance: key === "orientation.candidates" ? structuredGuidance.slice(0, 2) : [],
98
+ legacyMarkdownText,
99
+ rawFallback:
100
+ rows.length === 0 ||
101
+ !hasStructuredSelectionReason(result, data) ||
102
+ !hasStructuredSelectionGuidance(data),
103
+ };
104
+ }
105
+
106
+ function candidateRowsFor(
107
+ display: ToolDisplaySection | null,
108
+ structuredCandidates: CandidateRow[] | null,
109
+ key: CandidateSelectionKey,
110
+ ): string[] {
111
+ if (display?.lines.length) return [...display.lines];
112
+ return (structuredCandidates ?? []).map((candidate, index) =>
113
+ formatCandidateRow(
114
+ candidate,
115
+ key === "graph.candidates" ? "handle-first" : "rank-first",
116
+ index + 1,
117
+ ),
118
+ );
119
+ }
120
+
121
+ function candidateEvidenceFor(input: {
122
+ display: ToolDisplaySection | null;
123
+ evidence: EvidenceEntry | null;
124
+ structuredCandidates: CandidateRow[] | null;
125
+ rows: string[];
126
+ key: CandidateSelectionKey;
127
+ }): EvidenceEntry | null {
128
+ if (input.display?.lines.length) return displayEvidence(input.display, input.key);
129
+ if (input.evidence && input.structuredCandidates?.length) {
130
+ return evidenceForRows(input.evidence, input.rows.length);
131
+ }
132
+ if (input.display) return displayEvidence(input.display, input.key);
133
+ return input.evidence;
134
+ }
135
+
136
+ function displayEvidence(display: ToolDisplaySection, key: CandidateSelectionKey): EvidenceEntry {
137
+ const shownCount = display.lines.length;
138
+ return {
139
+ key,
140
+ shownCount,
141
+ totalCount: display.totalCount,
142
+ omittedCount:
143
+ display.totalCount === null
144
+ ? unknownOmittedCount(display.omittedCount, Math.max(0, display.shownCount - shownCount))
145
+ : Math.max(0, display.totalCount - shownCount),
146
+ partialReason: display.partialReason,
147
+ };
148
+ }
149
+
150
+ function evidenceForRows(evidence: EvidenceEntry, shownCount: number): EvidenceEntry {
151
+ return {
152
+ ...evidence,
153
+ shownCount,
154
+ omittedCount:
155
+ evidence.totalCount === null
156
+ ? unknownOmittedCount(evidence.omittedCount, Math.max(0, evidence.shownCount - shownCount))
157
+ : Math.max(0, evidence.totalCount - shownCount),
158
+ };
159
+ }
160
+
161
+ function readCandidateRows(value: unknown): CandidateRow[] | null {
162
+ if (!Array.isArray(value)) return null;
163
+ const rows = value.slice(0, MAX_TUI_DISPLAY_ITEMS).map(readCandidateRow);
164
+ return rows.every((row): row is CandidateRow => row !== null) ? rows : null;
165
+ }
166
+
167
+ function readCandidateRow(value: unknown): CandidateRow | null {
168
+ const record = asRecord(value);
169
+ if (
170
+ !record ||
171
+ typeof record.targetId !== "string" ||
172
+ record.targetId.length === 0 ||
173
+ typeof record.name !== "string" ||
174
+ typeof record.file !== "string" ||
175
+ !isPosition(record.line) ||
176
+ !isPosition(record.character) ||
177
+ (record.rank !== undefined && !isCount(record.rank)) ||
178
+ (record.kind !== undefined && !isNullableString(record.kind)) ||
179
+ (record.container !== undefined && !isNullableString(record.container))
180
+ ) {
181
+ return null;
182
+ }
183
+ return {
184
+ targetId: record.targetId,
185
+ name: record.name,
186
+ kind: typeof record.kind === "string" ? record.kind : null,
187
+ file: record.file,
188
+ line: record.line,
189
+ character: record.character,
190
+ rank: isCount(record.rank) ? record.rank : 0,
191
+ container: typeof record.container === "string" ? record.container : null,
192
+ };
193
+ }
194
+
195
+ function selectionReason(result: ToolResult, data: Record<string, unknown> | null): string | null {
196
+ const message = typeof result.details?.message === "string" ? result.details.message.trim() : "";
197
+ if (message) return truncateDisplayText(message, 160);
198
+
199
+ const resultKind = data?.resultKind;
200
+ if (resultKind === "kind-mismatch" && typeof data?.requestedKind === "string") {
201
+ return `No target matched provider kind ${data.requestedKind}.`;
202
+ }
203
+ if (resultKind === "disambiguation") return "Multiple target matches require one candidate.";
204
+ return null;
205
+ }
206
+
207
+ function hasStructuredSelectionReason(
208
+ result: ToolResult,
209
+ data: Record<string, unknown> | null,
210
+ ): boolean {
211
+ const message = typeof result.details?.message === "string" && result.details.message.trim();
212
+ if (message) return true;
213
+ return (
214
+ data?.resultKind === "disambiguation" ||
215
+ (data?.resultKind === "kind-mismatch" && typeof data.requestedKind === "string")
216
+ );
217
+ }
218
+
219
+ function hasStructuredSelectionGuidance(data: Record<string, unknown> | null): boolean {
220
+ return readStructuredNextQueries(data).length > 0;
221
+ }
222
+
223
+ function readStructuredNextQueries(data: Record<string, unknown> | null): string[] {
224
+ return Array.isArray(data?.nextQueries)
225
+ ? data.nextQueries.filter(
226
+ (query): query is string => typeof query === "string" && query.length > 0,
227
+ )
228
+ : [];
229
+ }
230
+
231
+ function selectionGuidance(
232
+ result: ToolResult,
233
+ data: Record<string, unknown> | null,
234
+ key: CandidateSelectionKey,
235
+ ): string[] {
236
+ const nextQueries = readStructuredNextQueries(data);
237
+ if (nextQueries.length > 0) return nextQueries.slice(0, 2);
238
+
239
+ const mismatch =
240
+ data?.resultKind === "kind-mismatch" || result.details?.status === "invalid-input";
241
+ if (key === "orientation.candidates") {
242
+ return ["Use one candidate handle as focus.target.handle"];
243
+ }
244
+ if (mismatch) {
245
+ return [
246
+ "Retry without symbolKind, use an observed provider kind, or choose a candidate handle",
247
+ ];
248
+ }
249
+ return ["Choose one candidate handle, or narrow the symbol selector with scope or symbolKind"];
250
+ }
251
+
252
+ class CandidateSelectionComponent implements Component {
253
+ constructor(
254
+ private readonly result: ToolResult,
255
+ private readonly options: ResultOptios,
256
+ private readonly theme: Theme,
257
+ private readonly selection: CandidateSelectionData,
258
+ ) {}
259
+
260
+ render(width: number): string[] {
261
+ const container = new Container();
262
+ const reason = this.selection.reason ? `: ${this.selection.reason}` : "";
263
+ container.addChild(new Text(this.theme.fg("warning", `Choose a target${reason}`), 0, 0));
264
+
265
+ if (!this.options.expanded) return this.renderCollapsed(container, width);
266
+ if (this.appendLegacyFallback(container)) return this.renderContainer(container, width);
267
+
268
+ this.appendStructuredSelection(container);
269
+ this.appendTruncation(container);
270
+ return this.renderContainer(container, width);
271
+ }
272
+
273
+ invalidate(): void {}
274
+
275
+ private renderCollapsed(container: Container, width: number): string[] {
276
+ const summary = this.selection.evidence ? formatEvidenceEntry(this.selection.evidence) : null;
277
+ if (summary) container.addChild(new Text(this.theme.fg("muted", summary), 0, 0));
278
+ this.appendTruncation(container);
279
+ return this.renderContainer(container, width);
280
+ }
281
+
282
+ private appendLegacyFallback(container: Container): boolean {
283
+ if (!this.selection.rawFallback || !this.selection.legacyMarkdownText.trim()) return false;
284
+ const legacyBody = createLegacyCandidateBody(this.selection.legacyMarkdownText, this.theme);
285
+ if (!legacyBody) return false;
286
+
287
+ const divider = `${this.theme.fg("border", "────")} ${this.theme.fg("dim", "raw markdown")} ${this.theme.fg("border", "────")}`;
288
+ container.addChild(new Spacer(1));
289
+ container.addChild(new Text(divider, 0, 0));
290
+ container.addChild(legacyBody);
291
+ this.appendLegacyGuidance(container);
292
+ this.appendTruncation(container);
293
+ return true;
294
+ }
295
+
296
+ private appendLegacyGuidance(container: Container): void {
297
+ if (this.selection.legacyGuidance.length === 0) return;
298
+ container.addChild(new Spacer(1));
299
+ container.addChild(
300
+ new Text(this.theme.fg("muted", `next: ${this.selection.legacyGuidance.join(" · ")}`), 0, 0),
301
+ );
302
+ }
303
+
304
+ private appendStructuredSelection(container: Container): void {
305
+ container.addChild(new Spacer(1));
306
+ const title = this.selection.display?.title ?? "Candidates";
307
+ const bounds = this.selection.evidence ? formatSectionBounds(this.selection.evidence) : "";
308
+ container.addChild(
309
+ new Text(this.theme.fg("dim", `${title}${bounds ? ` (${bounds})` : ""}`), 0, 0),
310
+ );
311
+ for (const row of this.selection.rows) {
312
+ container.addChild(new Text(this.theme.fg("muted", row), 0, 0));
313
+ }
314
+
315
+ if (this.selection.guidance.length === 0) return;
316
+ container.addChild(new Spacer(1));
317
+ container.addChild(
318
+ new Text(this.theme.fg("muted", `next: ${this.selection.guidance.join(" · ")}`), 0, 0),
319
+ );
320
+ }
321
+
322
+ private renderContainer(container: Container, width: number): string[] {
323
+ return container.render(Math.max(1, width));
324
+ }
325
+
326
+ private appendTruncation(container: Container): void {
327
+ const truncation = renderTruncationDisclosure(this.result, this.theme);
328
+ if (!truncation) return;
329
+ container.addChild(new Spacer(1));
330
+ container.addChild(truncation);
331
+ }
332
+ }
333
+
334
+ function formatSectionBounds(entry: EvidenceEntry): string {
335
+ if (entry.totalCount === null) {
336
+ const omitted = entry.omittedCount ? `; ${entry.omittedCount} collected omitted` : "";
337
+ const reason = entry.partialReason ? `; more may exist — ${entry.partialReason}` : "; partial";
338
+ return `${entry.shownCount}${omitted}${reason}`;
339
+ }
340
+ if (entry.omittedCount)
341
+ return `${entry.shownCount} of ${entry.totalCount}; ${entry.omittedCount} omitted`;
342
+ return `${entry.totalCount}`;
343
+ }
344
+
345
+ function unknownOmittedCount(base: number | null, additional: number): number | null {
346
+ const omitted = (base ?? 0) + additional;
347
+ return omitted > 0 ? omitted : null;
348
+ }
349
+
350
+ function asRecord(value: unknown): Record<string, unknown> | null {
351
+ return value !== null && typeof value === "object" && !Array.isArray(value)
352
+ ? (value as Record<string, unknown>)
353
+ : null;
354
+ }
355
+
356
+ function isPosition(value: unknown): value is number {
357
+ return isCount(value);
358
+ }
359
+
360
+ function isCount(value: unknown): value is number {
361
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
362
+ }
363
+
364
+ function isNullableString(value: unknown): value is string | null {
365
+ return value === null || typeof value === "string";
366
+ }
@@ -1,9 +1,6 @@
1
1
  /**
2
- * Shared TUI rendering utilities for code-intelligence tool results.
3
- *
4
- * Extracted from the per-tool renderers to eliminate duplication:
5
- * evidence section rendering, markdown detail, count badges,
6
- * partial/error guards, and the shared simple-result pattern.
2
+ * Shared transcript helpers for structured Code Intelligence results.
3
+ * Tool renderers own presentation; these helpers read assembled details.
7
4
  */
8
5
 
9
6
  import {
@@ -11,12 +8,14 @@ import {
11
8
  type Theme,
12
9
  type ToolRenderResultOptions,
13
10
  } from "@earendil-works/pi-coding-agent";
14
- import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
11
+ import { type Component, Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
15
12
  import { formatEvidenceBadge } from "@mrclrchtr/supi-core/evidence-badge";
16
13
  import { truncateDisplayText } from "../../tool/result/display.ts";
17
14
  import { renderToolDisplaySections } from "./display.ts";
15
+ import { renderExecutionError } from "./execution-error.ts";
18
16
 
19
17
  export { formatCallPath, formatCallValue, renderToolDisplaySections } from "./display.ts";
18
+ export { renderExecutionError } from "./execution-error.ts";
20
19
 
21
20
  import type {
22
21
  ToolDisplaySection,
@@ -58,6 +57,8 @@ export const EVIDENCE_KEY_LABELS: Record<string, string> = {
58
57
  "imports.modules": "imports",
59
58
  "inspect.definitions": "definitions",
60
59
  "inspect.diagnostics": "nearby diagnostics",
60
+ "graph.candidates": "candidates",
61
+ "orientation.candidates": "candidates",
61
62
  "references.locations": "references",
62
63
  "refactor.edits": "edits",
63
64
  "resolve.candidates": "candidates",
@@ -135,12 +136,14 @@ function isOptionalCount(value: unknown): value is number | undefined {
135
136
 
136
137
  /** Format one assembled evidence list without recomputing its bounds. */
137
138
  export function formatEvidenceEntry(entry: EvidenceEntry): string {
139
+ const label = evidenceLabel(entry.key);
140
+ const singleCandidate = label === "candidates" && (entry.totalCount ?? entry.shownCount) === 1;
138
141
  const badge = formatEvidenceBadge({
139
142
  shownCount: entry.shownCount,
140
143
  totalCount: entry.totalCount,
141
144
  omittedCount: entry.omittedCount,
142
145
  partialReason: entry.partialReason,
143
- label: evidenceLabel(entry.key),
146
+ label: singleCandidate ? "candidate" : label,
144
147
  });
145
148
  if (!entry.invalidLocationCount) return badge;
146
149
  const noun = entry.invalidLocationCount === 1 ? "location" : "locations";
@@ -181,19 +184,6 @@ export function renderPartial(label: string, theme: Theme): Text {
181
184
  return new Text(theme.fg("warning", label), 0, 0);
182
185
  }
183
186
 
184
- export function renderError(label: string, theme: Theme): Text {
185
- return new Text(theme.fg("error", label), 0, 0);
186
- }
187
-
188
- /** Render an execution failure from PI's renderer context. */
189
- export function renderExecutionError(
190
- context: ToolRendererContext | undefined,
191
- label: string,
192
- theme: Theme,
193
- ): Text | null {
194
- return context?.isError ? renderError(label, theme) : null;
195
- }
196
-
197
187
  /** Render a domain error encoded in structured result details. */
198
188
  export function renderDomainError(result: ToolResult, theme: Theme): Text | null {
199
189
  const status = result.details?.status;
@@ -210,7 +200,7 @@ export function renderDomainError(result: ToolResult, theme: Theme): Text | null
210
200
  const message = result.details?.message
211
201
  ? `: ${truncateDisplayText(result.details.message, 160)}`
212
202
  : "";
213
- return renderError(`${label}${message}`, theme);
203
+ return new Text(theme.fg("error", `${label}${message}`), 0, 0);
214
204
  }
215
205
 
216
206
  /** Render an invalid-input or unavailable result with its structured body. */
@@ -231,16 +221,17 @@ export function renderDomainResult(
231
221
  /** Render the structured output-truncation disclosure. */
232
222
  export function renderTruncationDisclosure(result: ToolResult, theme: Theme): Text | null {
233
223
  const truncation = result.details?.truncation;
234
- if (!truncation?.truncated) return null;
224
+ if (!truncation?.truncated && !truncation?.displayTruncated) return null;
235
225
 
226
+ const label = truncation.truncated ? "Output truncated" : "Display limited";
236
227
  const path = truncation.fullOutputPath
237
- ? `; full output: ${truncateDisplayText(truncation.fullOutputPath, 160)}`
238
- : "";
239
- return new Text(theme.fg("warning", `Output truncated${path}`), 0, 0);
228
+ ? `; ${truncation.truncated ? "full output" : "full returned evidence"}: ${truncateDisplayText(truncation.fullOutputPath, 160)}`
229
+ : truncation.displayTruncated
230
+ ? "; full returned evidence file unavailable"
231
+ : "";
232
+ return new Text(theme.fg("warning", `${label}${path}`), 0, 0);
240
233
  }
241
234
 
242
- // ── Result options ───────────────────────────────────────────────
243
-
244
235
  // ── Shared simple-result renderer ─────────────────────────────────
245
236
 
246
237
  /**
@@ -254,12 +245,16 @@ export function renderSimpleResult(
254
245
  result: ToolResult,
255
246
  options: ResultOptios,
256
247
  theme: Theme,
257
- partialLabel: string,
248
+ labels: { progress: string; failure: string },
258
249
  context?: ToolRendererContext,
259
- ): Container | Text {
260
- if (options.isPartial) return renderPartial(partialLabel, theme);
250
+ ): Component {
251
+ if (options.isPartial) return renderPartial(labels.progress, theme);
261
252
 
262
- const executionError = renderExecutionError(context, "Tool failed", theme);
253
+ const executionError = renderExecutionError(
254
+ result,
255
+ { isError: context?.isError, expanded: options.expanded, label: labels.failure },
256
+ theme,
257
+ );
263
258
  if (executionError) return executionError;
264
259
 
265
260
  const domainError = renderDomainError(result, theme);
@@ -0,0 +1,92 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { type Component, Text } from "@earendil-works/pi-tui";
4
+ import type { ToolResult } from "./common.ts";
5
+
6
+ const MAX_FAILURE_TEXT_CHARACTERS = 4096;
7
+ const MAX_COLLAPSED_FAILURE_ROWS = 2;
8
+ const MAX_EXPANDED_FAILURE_ROWS = 20;
9
+
10
+ /** PI execution state and presentation for a failed tool call. */
11
+ interface ExecutionErrorOptions {
12
+ isError?: boolean;
13
+ expanded: boolean;
14
+ label: string;
15
+ }
16
+
17
+ interface FailureReason {
18
+ text: string;
19
+ limited: boolean;
20
+ }
21
+
22
+ /**
23
+ * Show a failure reason only when PI marks the execution as failed.
24
+ * PI throws can have plain content without details. This text is a failure
25
+ * body, not Markdown from which the renderer infers tool evidence.
26
+ */
27
+ export function renderExecutionError(
28
+ result: ToolResult,
29
+ options: ExecutionErrorOptions,
30
+ theme: Theme,
31
+ ): Component | null {
32
+ if (!options.isError) return null;
33
+ return new ExecutionError(readFailureReason(result), options, theme);
34
+ }
35
+
36
+ function readFailureReason(result: ToolResult): FailureReason {
37
+ const message = plainFailureText(result.details?.message);
38
+ if (message) return boundFailureReason(message);
39
+ let text = "";
40
+ for (const block of Array.isArray(result.content) ? result.content : []) {
41
+ if (block?.type !== "text") continue;
42
+ const part = plainFailureText(block.text);
43
+ if (!part) continue;
44
+ text += `${text ? "\n" : ""}${part}`;
45
+ if (text.length > MAX_FAILURE_TEXT_CHARACTERS) break;
46
+ }
47
+ return boundFailureReason(text);
48
+ }
49
+
50
+ function boundFailureReason(text: string): FailureReason {
51
+ return {
52
+ text: text.slice(0, MAX_FAILURE_TEXT_CHARACTERS),
53
+ limited: text.length > MAX_FAILURE_TEXT_CHARACTERS,
54
+ };
55
+ }
56
+
57
+ function plainFailureText(value: unknown): string {
58
+ if (typeof value !== "string") return "";
59
+ return stripVTControlCharacters(value)
60
+ .replace(/\r\n?/g, "\n")
61
+ .replace(/\t/g, " ")
62
+ .replace(/[\p{Cc}\p{Bidi_Control}]/gu, (character) => (character === "\n" ? character : ""))
63
+ .trim();
64
+ }
65
+
66
+ /** Bound physical rows after wrapping, so narrow terminals keep the same row limit. */
67
+ class ExecutionError implements Component {
68
+ constructor(
69
+ private readonly reason: FailureReason,
70
+ private readonly options: ExecutionErrorOptions,
71
+ private readonly theme: Theme,
72
+ ) {}
73
+
74
+ render(width: number): string[] {
75
+ const { expanded, label } = this.options;
76
+ const body = expanded ? this.reason.text : this.reason.text.replace(/\s+/g, " ");
77
+ const text = `${label}${body ? `\n${body}` : ""}`;
78
+ const rows = new Text(this.theme.fg("error", text), 0, 0).render(width);
79
+ const maxRows = expanded ? MAX_EXPANDED_FAILURE_ROWS : MAX_COLLAPSED_FAILURE_ROWS;
80
+ if (!this.reason.limited && rows.length <= maxRows) return rows;
81
+ const disclosure = expanded
82
+ ? "Failure text limited."
83
+ : "Failure text limited; expand for more.";
84
+ return [
85
+ ...rows.slice(0, maxRows),
86
+ ...new Text(this.theme.fg("dim", disclosure), 0, 0).render(width),
87
+ ];
88
+ }
89
+
90
+ /** Text and theme formatting are rebuilt on every render; there is no cache. */
91
+ invalidate(): void {}
92
+ }