@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.
- package/README.md +164 -82
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/README.md +35 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/index.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/legacy.js +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/ignore/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +6 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +43 -35
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +83 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +108 -92
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +44 -82
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +3 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +9 -68
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +45 -58
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +230 -331
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +201 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +129 -70
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-typescript.ts +395 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +8 -12
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +685 -183
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +60 -60
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-request-enrollment.ts +75 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-barrier.ts +443 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-errors.ts +30 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-semantic-input-file.ts +15 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +290 -37
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +72 -16
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +1 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +30 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +10 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-process-crash-report.ts +50 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +11 -7
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +608 -45
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +3 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +37 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +19 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +8 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +10 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/llm.ts +141 -16
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callee-display-name.ts +165 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +67 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +2 -0
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +1 -2
- package/src/analysis/health/file-scope.ts +4 -1
- package/src/analysis/health/server-status.ts +1 -1
- package/src/analysis/relations/callees.ts +17 -8
- package/src/analysis/relations/implementations.ts +1 -2
- package/src/analysis/relations/references.ts +8 -4
- package/src/analysis/relations/types.ts +6 -30
- package/src/analysis/target/anchored.ts +9 -1
- package/src/analysis/target/symbol.ts +29 -11
- package/src/analysis/target/types.ts +14 -6
- package/src/api.ts +2 -0
- package/src/session/graph/collect.ts +18 -3
- package/src/session/health-refresh.ts +12 -1
- package/src/session/health-types.ts +18 -6
- package/src/session/health-workflow.ts +21 -19
- package/src/session/orientation-types.ts +5 -6
- package/src/session/orientation-workflow.ts +10 -2
- package/src/session/target-workflow.ts +9 -6
- package/src/substrate/lsp/source-tracking.ts +11 -6
- package/src/tool/code_find/guidance.ts +2 -2
- package/src/tool/code_find/spec.ts +5 -4
- package/src/tool/code_find/tui.ts +7 -1
- package/src/tool/code_graph/details.ts +32 -0
- package/src/tool/code_graph/display.ts +54 -0
- package/src/tool/code_graph/format.ts +60 -0
- package/src/tool/code_graph/guidance.ts +1 -1
- package/src/tool/code_graph/markdown.ts +76 -198
- package/src/tool/code_graph/read-next.ts +65 -0
- package/src/tool/code_graph/result.ts +118 -83
- package/src/tool/code_graph/tui.ts +216 -54
- package/src/tool/code_health/guidance.ts +2 -4
- package/src/tool/code_health/markdown.ts +75 -58
- package/src/tool/code_health/refresh-outcome.ts +41 -1
- package/src/tool/code_health/refresh-status.ts +114 -48
- package/src/tool/code_health/scope.ts +78 -0
- package/src/tool/code_health/spec.ts +1 -1
- package/src/tool/code_health/tui.ts +25 -38
- package/src/tool/code_inspect/tui.ts +7 -1
- package/src/tool/code_orientation/result.ts +31 -8
- package/src/tool/code_orientation/tui.ts +26 -5
- package/src/tool/code_refactor_apply/guidance.ts +1 -1
- package/src/tool/code_refactor_apply/tui.ts +7 -3
- package/src/tool/code_refactor_plan/guidance.ts +1 -1
- package/src/tool/code_refactor_plan/tui.ts +7 -1
- package/src/tool/code_resolve/guidance.ts +2 -4
- package/src/tool/code_resolve/result.ts +13 -10
- package/src/tool/code_resolve/tui.ts +18 -1
- package/src/tool/infra/truncate.ts +39 -13
- package/src/tool/result/candidate-row.ts +27 -0
- package/src/tool/result/errors.ts +21 -6
- package/src/tool/result/types.ts +7 -3
- package/src/tool/schemas.ts +5 -0
- package/src/types/execution.ts +2 -0
- package/src/types/index.ts +2 -0
- package/src/ui/tui/candidate-legacy-body.ts +69 -0
- package/src/ui/tui/candidate-selection.ts +366 -0
- package/src/ui/tui/common.ts +26 -31
- 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
|
+
}
|
package/src/ui/tui/common.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
labels: { progress: string; failure: string },
|
|
258
249
|
context?: ToolRendererContext,
|
|
259
|
-
):
|
|
260
|
-
if (options.isPartial) return renderPartial(
|
|
250
|
+
): Component {
|
|
251
|
+
if (options.isPartial) return renderPartial(labels.progress, theme);
|
|
261
252
|
|
|
262
|
-
const executionError = renderExecutionError(
|
|
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
|
+
}
|