@mrclrchtr/supi-code-intelligence 4.1.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +5 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +2 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/operation-support.ts +19 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-c-family.ts +230 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-html-sql.ts +186 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-jvm.ts +311 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-polyglot.ts +218 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline-scripting.ts +307 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/outline.ts +9 -5
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +6 -2
- package/package.json +5 -5
- package/src/analysis/search/pattern.ts +2 -2
- package/src/analysis/target/symbol.ts +5 -2
- package/src/extension.ts +5 -1
- package/src/session/orientation/collect.ts +134 -180
- package/src/session/orientation/context-sections.ts +14 -48
- package/src/session/orientation-types.ts +14 -4
- package/src/session/orientation-workflow.ts +20 -48
- package/src/substrate/lsp/lifecycle.ts +55 -3
- package/src/substrate/lsp/state.ts +6 -0
- package/src/tool/graph/execute.ts +1 -1
- package/src/tool/graph/markdown.ts +222 -0
- package/src/tool/orientation/markdown.ts +66 -11
- package/src/tool/register.ts +10 -0
- package/src/ui/footer.ts +33 -8
- package/src/ui/markdown/overview-data.ts +3 -15
- package/src/ui/markdown/overview.ts +4 -10
- package/src/ui/markdown/types.ts +0 -1
- package/src/tool/graph/calls-md.ts +0 -41
- package/src/tool/graph/implementations-md.ts +0 -57
- package/src/tool/graph/markdown-base.ts +0 -88
- package/src/tool/graph/provider-location-md.ts +0 -9
- package/src/tool/graph/references-md.ts +0 -41
|
@@ -16,11 +16,16 @@ export interface OrientationWorkflowInput {
|
|
|
16
16
|
readonly maxResults?: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Presentation-neutral content atom a collector emits for one Orientation
|
|
21
|
+
* section. Collectors never build headings, section notes, or document
|
|
22
|
+
* framing — the markdown adapter owns all of that. `subheading` renders as an
|
|
23
|
+
* h3 within a section (only instruction-file paths use it).
|
|
24
|
+
*/
|
|
25
|
+
export type OrientationItem =
|
|
22
26
|
| { readonly kind: "paragraph"; readonly text: string }
|
|
23
27
|
| { readonly kind: "list-item"; readonly text: string }
|
|
28
|
+
| { readonly kind: "subheading"; readonly text: string }
|
|
24
29
|
| {
|
|
25
30
|
readonly kind: "code";
|
|
26
31
|
readonly language: string | null;
|
|
@@ -51,6 +56,8 @@ export interface OrientationSectionData {
|
|
|
51
56
|
readonly confidence: ConfidenceMode;
|
|
52
57
|
readonly provenance: readonly OrientationProvenance[];
|
|
53
58
|
readonly evidenceLists: readonly EvidenceListMetadata[];
|
|
59
|
+
/** Content atoms for this section; the adapter frames them under the title + note. */
|
|
60
|
+
readonly items: readonly OrientationItem[];
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
export interface OrientationCandidate {
|
|
@@ -67,7 +74,10 @@ export interface OrientationCandidate {
|
|
|
67
74
|
|
|
68
75
|
/** Immutable facts returned by the session before markdown or TUI rendering. */
|
|
69
76
|
export interface OrientationResultData {
|
|
70
|
-
|
|
77
|
+
/** Document title rendered as the leading h1 (e.g. "Workspace Orientation", "Code Orientation"). */
|
|
78
|
+
readonly title: string;
|
|
79
|
+
/** Leading paragraphs rendered before the title (target summary / resolution notes). */
|
|
80
|
+
readonly notes: readonly string[];
|
|
71
81
|
readonly sections: readonly OrientationSectionData[];
|
|
72
82
|
readonly confidence: ConfidenceMode;
|
|
73
83
|
readonly focusTarget: string | null;
|
|
@@ -14,10 +14,9 @@ import { loadCodeIntelligenceConfig } from "../config.ts";
|
|
|
14
14
|
import type { CapabilityAdapter } from "./capability-adapter.ts";
|
|
15
15
|
import { parseOrientationWorkflowInput } from "./input/workflows.ts";
|
|
16
16
|
import { executeOrientation } from "./orientation/collect.ts";
|
|
17
|
-
import { formatSectionNote } from "./orientation/context-sections.ts";
|
|
18
17
|
import type {
|
|
19
|
-
OrientationBlock,
|
|
20
18
|
OrientationFocusInput,
|
|
19
|
+
OrientationItem,
|
|
21
20
|
OrientationResultData,
|
|
22
21
|
OrientationSectionData,
|
|
23
22
|
OrientationWorkflowInput,
|
|
@@ -217,6 +216,20 @@ function addInstructionFiles(
|
|
|
217
216
|
const collected = collectInstructionFiles(matches);
|
|
218
217
|
if (!collected) return result;
|
|
219
218
|
deps.markInstructionDirsSurfaced(collected.metadata.files.map((file) => file.directory));
|
|
219
|
+
const items: OrientationItem[] = [];
|
|
220
|
+
for (const file of collected.files) {
|
|
221
|
+
items.push(
|
|
222
|
+
{ kind: "subheading", text: file.path },
|
|
223
|
+
{ kind: "code", language: null, lines: file.content.split("\n") },
|
|
224
|
+
);
|
|
225
|
+
if (file.truncated) {
|
|
226
|
+
items.push({
|
|
227
|
+
kind: "paragraph",
|
|
228
|
+
text: `Instruction file truncated to ${file.shownLines} of ${file.totalLines} lines. Read ${file.path} for the full file.`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
items.push({ kind: "blank" });
|
|
232
|
+
}
|
|
220
233
|
const section: OrientationSectionData = {
|
|
221
234
|
key: "instructions",
|
|
222
235
|
title: "Instructions",
|
|
@@ -233,10 +246,10 @@ function addInstructionFiles(
|
|
|
233
246
|
partialReason: null,
|
|
234
247
|
},
|
|
235
248
|
],
|
|
249
|
+
items,
|
|
236
250
|
};
|
|
237
251
|
return {
|
|
238
252
|
...result,
|
|
239
|
-
blocks: insertInstructionBlocks(result.blocks, collected.files, section),
|
|
240
253
|
sections: [section, ...result.sections],
|
|
241
254
|
renderedSections: ["instructions", ...result.renderedSections],
|
|
242
255
|
instructions: collected.metadata,
|
|
@@ -258,50 +271,9 @@ function addTargetSummary(
|
|
|
258
271
|
cwd: string,
|
|
259
272
|
): OrientationResultData {
|
|
260
273
|
const name = entry.name ? ` ${entry.name}` : "";
|
|
261
|
-
const
|
|
262
|
-
...notes.map((note)
|
|
263
|
-
{
|
|
264
|
-
kind: "paragraph",
|
|
265
|
-
text: `Resolved target${name}: ${relative(cwd, entry.file) || entry.file}:${entry.displayLine}:${entry.displayCharacter} — Target ID: ${entry.targetId}`,
|
|
266
|
-
},
|
|
267
|
-
{ kind: "blank" },
|
|
274
|
+
const summaryNotes = [
|
|
275
|
+
...notes.map((note) => `Note: ${note}`),
|
|
276
|
+
`Resolved target${name}: ${relative(cwd, entry.file) || entry.file}:${entry.displayLine}:${entry.displayCharacter} — Target ID: ${entry.targetId}`,
|
|
268
277
|
];
|
|
269
|
-
return { ...result,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function insertInstructionBlocks(
|
|
273
|
-
existing: readonly OrientationBlock[],
|
|
274
|
-
files: readonly {
|
|
275
|
-
path: string;
|
|
276
|
-
content: string;
|
|
277
|
-
truncated: boolean;
|
|
278
|
-
shownLines: number;
|
|
279
|
-
totalLines: number;
|
|
280
|
-
}[],
|
|
281
|
-
section: OrientationSectionData,
|
|
282
|
-
): OrientationBlock[] {
|
|
283
|
-
const blocks: OrientationBlock[] = [
|
|
284
|
-
{ kind: "heading", level: 2, text: "Instructions" },
|
|
285
|
-
{ kind: "paragraph", text: formatSectionNote(section) },
|
|
286
|
-
{ kind: "blank" },
|
|
287
|
-
];
|
|
288
|
-
for (const file of files) {
|
|
289
|
-
blocks.push(
|
|
290
|
-
{ kind: "heading", level: 3, text: file.path },
|
|
291
|
-
{ kind: "code", language: null, lines: file.content.split("\n") },
|
|
292
|
-
);
|
|
293
|
-
if (file.truncated) {
|
|
294
|
-
blocks.push({
|
|
295
|
-
kind: "paragraph",
|
|
296
|
-
text: `Instruction file truncated to ${file.shownLines} of ${file.totalLines} lines. Read ${file.path} for the full file.`,
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
blocks.push({ kind: "blank" });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const insertion = existing.findIndex(
|
|
303
|
-
(block, index) => index > 0 && block.kind === "heading" && block.level === 2,
|
|
304
|
-
);
|
|
305
|
-
if (insertion < 0) return [...existing, ...blocks];
|
|
306
|
-
return [...existing.slice(0, insertion), ...blocks, ...existing.slice(insertion)];
|
|
278
|
+
return { ...result, notes: [...summaryNotes, ...result.notes], target: { ...entry } };
|
|
307
279
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// LSP lifecycle backed by the process-shared Workspace provider host.
|
|
2
2
|
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { unregisterLspFooterContribution } from "../../ui/footer.ts";
|
|
5
4
|
import { acquireWorkspaceProviderHost } from "../workspace-provider-host.ts";
|
|
6
|
-
import type
|
|
5
|
+
import { LSP_STATE_CHANGE_EVENT, type LspAdapterState } from "./state.ts";
|
|
6
|
+
|
|
7
|
+
const STATE_CHANGE_EVENT = new Event(LSP_STATE_CHANGE_EVENT);
|
|
8
|
+
|
|
9
|
+
/** Poll interval (ms) for detecting server readiness transitions in the footer. */
|
|
10
|
+
const READINESS_POLL_MS = 2_000;
|
|
7
11
|
|
|
8
12
|
/** Acquire providers once per workspace and release them after the final Pi session ends. */
|
|
9
13
|
export function registerLspSessionLifecycle(
|
|
@@ -11,12 +15,55 @@ export function registerLspSessionLifecycle(
|
|
|
11
15
|
state: LspAdapterState,
|
|
12
16
|
onStarted?: (ctx: ExtensionContext) => void | Promise<void>,
|
|
13
17
|
): void {
|
|
18
|
+
let readinessTimer: ReturnType<typeof setInterval> | null = null;
|
|
19
|
+
let lastFingerprint = "";
|
|
20
|
+
|
|
21
|
+
function fingerprint(): string {
|
|
22
|
+
const servers = state.controller?.workspaceRuntime?.getProjectServers();
|
|
23
|
+
if (!servers) return "";
|
|
24
|
+
let ready = 0;
|
|
25
|
+
let starting = 0;
|
|
26
|
+
let error = 0;
|
|
27
|
+
let unavailable = 0;
|
|
28
|
+
let openFiles = 0;
|
|
29
|
+
for (const s of servers) {
|
|
30
|
+
openFiles += s.openFiles.length;
|
|
31
|
+
if (s.status === "running" && s.ready) ready++;
|
|
32
|
+
else if (s.status === "running") starting++;
|
|
33
|
+
else if (s.status === "error") error++;
|
|
34
|
+
else unavailable++;
|
|
35
|
+
}
|
|
36
|
+
return `${ready}:${starting}:${error}:${unavailable}:${openFiles}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function startReadinessPolling(): void {
|
|
40
|
+
stopReadinessPolling();
|
|
41
|
+
lastFingerprint = fingerprint();
|
|
42
|
+
readinessTimer = setInterval(() => {
|
|
43
|
+
if (state.controller?.kind !== "ready") return;
|
|
44
|
+
const current = fingerprint();
|
|
45
|
+
if (current === lastFingerprint) return;
|
|
46
|
+
lastFingerprint = current;
|
|
47
|
+
state.stateChanges.dispatchEvent(STATE_CHANGE_EVENT);
|
|
48
|
+
}, READINESS_POLL_MS);
|
|
49
|
+
readinessTimer.unref?.();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stopReadinessPolling(): void {
|
|
53
|
+
if (readinessTimer) {
|
|
54
|
+
clearInterval(readinessTimer);
|
|
55
|
+
readinessTimer = null;
|
|
56
|
+
}
|
|
57
|
+
lastFingerprint = "";
|
|
58
|
+
}
|
|
59
|
+
|
|
14
60
|
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
15
61
|
await state.providerLease?.release();
|
|
16
62
|
state.providerLease = null;
|
|
17
63
|
state.controller = null;
|
|
18
64
|
state.lspActive = false;
|
|
19
65
|
state.sentinelSnapshot = new Map();
|
|
66
|
+
stopReadinessPolling();
|
|
20
67
|
|
|
21
68
|
const lease = await acquireWorkspaceProviderHost(ctx.cwd, {
|
|
22
69
|
projectTrusted: ctx.isProjectTrusted(),
|
|
@@ -26,14 +73,19 @@ export function registerLspSessionLifecycle(
|
|
|
26
73
|
state.lspActive = lease.lspController?.kind === "ready";
|
|
27
74
|
state.sentinelSnapshot = lease.sentinelSnapshot;
|
|
28
75
|
await onStarted?.(ctx);
|
|
76
|
+
|
|
77
|
+
// Dispatch initial server list so the footer shows starting servers immediately.
|
|
78
|
+
state.stateChanges.dispatchEvent(STATE_CHANGE_EVENT);
|
|
79
|
+
// Poll for readiness transitions (running → ready, errors, etc.).
|
|
80
|
+
startReadinessPolling();
|
|
29
81
|
});
|
|
30
82
|
|
|
31
83
|
pi.on("session_shutdown", async () => {
|
|
84
|
+
stopReadinessPolling();
|
|
32
85
|
await state.providerLease?.release();
|
|
33
86
|
state.providerLease = null;
|
|
34
87
|
state.controller = null;
|
|
35
88
|
state.lspActive = false;
|
|
36
89
|
state.sentinelSnapshot = new Map();
|
|
37
|
-
unregisterLspFooterContribution();
|
|
38
90
|
});
|
|
39
91
|
}
|
|
@@ -25,6 +25,8 @@ export interface LspAdapterState {
|
|
|
25
25
|
lspActive: boolean;
|
|
26
26
|
/** Snapshot of workspace sentinel files (package.json, tsconfig, lockfiles) for change detection. */
|
|
27
27
|
sentinelSnapshot: Map<string, number>;
|
|
28
|
+
/** Fired when server statuses or readiness change — footer listens for re-renders. */
|
|
29
|
+
stateChanges: EventTarget;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export function createLspAdapterState(): LspAdapterState {
|
|
@@ -34,5 +36,9 @@ export function createLspAdapterState(): LspAdapterState {
|
|
|
34
36
|
inspector: { handle: null, close: null },
|
|
35
37
|
lspActive: false,
|
|
36
38
|
sentinelSnapshot: new Map(),
|
|
39
|
+
stateChanges: new EventTarget(),
|
|
37
40
|
};
|
|
38
41
|
}
|
|
42
|
+
|
|
43
|
+
/** Event type dispatched on {@link LspAdapterState.stateChanges} when server statuses change. */
|
|
44
|
+
export const LSP_STATE_CHANGE_EVENT = "server-status-changed" as const;
|
|
@@ -6,7 +6,7 @@ import type { CodeIntelResult, CodeIntelToolExecCtx } from "../../types/index.ts
|
|
|
6
6
|
import { searchErrorResult } from "../infra/error-results.ts";
|
|
7
7
|
import { toWorkflowControl } from "../infra/workflow-control.ts";
|
|
8
8
|
import { assembleGraphResult } from "../result/graph.ts";
|
|
9
|
-
import { renderGraphResult } from "./markdown
|
|
9
|
+
import { renderGraphResult } from "./markdown.ts";
|
|
10
10
|
|
|
11
11
|
export type GraphRelation = RequestedGraphRelation;
|
|
12
12
|
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/** Markdown adapter for assembled code_graph evidence. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type EvidenceList,
|
|
5
|
+
type EvidenceListMetadata,
|
|
6
|
+
renderEvidenceListMetadataDisclosure,
|
|
7
|
+
} from "../../analysis/evidence.ts";
|
|
8
|
+
import { renderReadNextSection } from "../../analysis/read-next.ts";
|
|
9
|
+
import {
|
|
10
|
+
compactLineRanges,
|
|
11
|
+
formatAssembledReferenceList,
|
|
12
|
+
} from "../../analysis/references/semantic-refs.ts";
|
|
13
|
+
import type {
|
|
14
|
+
CallEntry,
|
|
15
|
+
CalleeScope,
|
|
16
|
+
ImplementationEntry,
|
|
17
|
+
ReferenceEntry,
|
|
18
|
+
} from "../../analysis/relations/types.ts";
|
|
19
|
+
import { toDisplayPath } from "../../analysis/search/paths.ts";
|
|
20
|
+
import type { GraphResultAssembly } from "../result/graph.ts";
|
|
21
|
+
|
|
22
|
+
/** Render an assembled graph result. */
|
|
23
|
+
export function renderGraphResult(assembly: GraphResultAssembly): string {
|
|
24
|
+
const lines = [
|
|
25
|
+
`# Graph of \`${assembly.displayName}\``,
|
|
26
|
+
"",
|
|
27
|
+
`_File: \`${assembly.resolvedDisplayFile}\`_`,
|
|
28
|
+
"",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const available = assembly.sections.filter((section) => section.kind === "ok");
|
|
32
|
+
if (available.length > 0) {
|
|
33
|
+
lines.push(
|
|
34
|
+
available
|
|
35
|
+
.map(
|
|
36
|
+
(section) =>
|
|
37
|
+
`**${section.rel}**: ${sectionCount(section)} result${sectionCount(section) === 1 ? "" : "s"}`,
|
|
38
|
+
)
|
|
39
|
+
.join(" | "),
|
|
40
|
+
"",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const unavailable = assembly.sections.filter((section) => section.kind === "unavailable");
|
|
45
|
+
if (unavailable.length > 0) {
|
|
46
|
+
lines.push(
|
|
47
|
+
`_Unavailable: ${unavailable.map((section) => `\`${section.rel}\``).join(", ")}_`,
|
|
48
|
+
"",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const section of assembly.sections) {
|
|
53
|
+
if (section.kind === "unavailable") {
|
|
54
|
+
lines.push(`**${section.rel}**: ${section.message}`, "");
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
switch (section.rel) {
|
|
59
|
+
case "references": {
|
|
60
|
+
const rendered = renderReferencesResult({
|
|
61
|
+
symbolName: assembly.displayName,
|
|
62
|
+
references: section.evidence,
|
|
63
|
+
externalCount: section.data.externalCount,
|
|
64
|
+
confidence: section.data.confidence,
|
|
65
|
+
cwd: assembly.cwd,
|
|
66
|
+
});
|
|
67
|
+
lines.push(rendered.content, "");
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "callees": {
|
|
71
|
+
const rendered = renderCallsResult(
|
|
72
|
+
section.data.enclosingScope,
|
|
73
|
+
section.evidence,
|
|
74
|
+
assembly.resolvedDisplayFile,
|
|
75
|
+
section.data.depth,
|
|
76
|
+
);
|
|
77
|
+
lines.push(rendered.content, "");
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "implements": {
|
|
81
|
+
const rendered = renderImplementationsResult({
|
|
82
|
+
implementations: section.evidence,
|
|
83
|
+
externalCount: section.data.externalCount,
|
|
84
|
+
cwd: assembly.cwd,
|
|
85
|
+
targetName: assembly.displayName,
|
|
86
|
+
});
|
|
87
|
+
lines.push(rendered.content, "");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push(...renderReadNextSection(available.flatMap((section) => section.readNext)));
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function sectionCount(
|
|
98
|
+
section: Extract<GraphResultAssembly["sections"][number], { kind: "ok" }>,
|
|
99
|
+
): number {
|
|
100
|
+
return section.evidence.metadata.totalCount ?? section.evidence.metadata.shownCount;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function renderReferencesResult(options: {
|
|
104
|
+
symbolName: string;
|
|
105
|
+
references: EvidenceList<ReferenceEntry>;
|
|
106
|
+
externalCount: number;
|
|
107
|
+
confidence: string;
|
|
108
|
+
cwd: string;
|
|
109
|
+
}): { content: string; evidenceList: EvidenceListMetadata | null } {
|
|
110
|
+
const total = options.references.metadata.totalCount ?? options.references.metadata.shownCount;
|
|
111
|
+
const lines = [
|
|
112
|
+
`# References of \`${options.symbolName}\``,
|
|
113
|
+
"",
|
|
114
|
+
`**${total} reference${total === 1 ? "" : "s"}** (${options.confidence})`,
|
|
115
|
+
];
|
|
116
|
+
if (options.externalCount > 0) {
|
|
117
|
+
lines.push(
|
|
118
|
+
`_+${options.externalCount} external reference${options.externalCount === 1 ? "" : "s"}_`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
const invalidDisclosure = renderInvalidProviderLocations(options.references.metadata);
|
|
122
|
+
if (invalidDisclosure) lines.push(invalidDisclosure);
|
|
123
|
+
lines.push("");
|
|
124
|
+
|
|
125
|
+
const displayEvidence = {
|
|
126
|
+
key: options.references.key,
|
|
127
|
+
items: options.references.items.map((reference) => ({
|
|
128
|
+
file: toDisplayPath(options.cwd, reference.file),
|
|
129
|
+
line: reference.line,
|
|
130
|
+
})),
|
|
131
|
+
metadata: options.references.metadata,
|
|
132
|
+
};
|
|
133
|
+
const evidenceList = formatAssembledReferenceList(lines, displayEvidence);
|
|
134
|
+
return { content: lines.join("\n"), evidenceList };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function renderCallsResult(
|
|
138
|
+
enclosingScope: CalleeScope,
|
|
139
|
+
calls: EvidenceList<CallEntry>,
|
|
140
|
+
relPath: string,
|
|
141
|
+
depth: "direct" | "deep" = "direct",
|
|
142
|
+
): { content: string; evidenceList: EvidenceListMetadata } {
|
|
143
|
+
const lines: string[] = [];
|
|
144
|
+
const depthLabel = depth === "deep" ? "Deep structural calls" : "Direct structural calls";
|
|
145
|
+
const depthNote =
|
|
146
|
+
depth === "deep"
|
|
147
|
+
? "_Deep: includes calls from nested function/method/callback scopes within the enclosing scope._"
|
|
148
|
+
: "_Structural only: call expressions are reported by source shape, not symbol identity; calls inside nested function/method/callback scopes are excluded from this enclosing scope._";
|
|
149
|
+
const total = calls.metadata.totalCount ?? calls.metadata.shownCount;
|
|
150
|
+
|
|
151
|
+
lines.push(`# ${depthLabel} from \`${enclosingScope.name}\``, "");
|
|
152
|
+
lines.push(
|
|
153
|
+
`**${total} ${depth === "deep" ? "" : "direct "}structural call${total === 1 ? "" : "s"}** from enclosing scope \`${enclosingScope.name}\` (${formatScopeRange(enclosingScope)}) in \`${relPath}\``,
|
|
154
|
+
"",
|
|
155
|
+
depthNote,
|
|
156
|
+
"",
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
for (const call of calls.items) lines.push(`- \`${call.name}\` (L${call.line})`);
|
|
160
|
+
const disclosure = renderEvidenceListMetadataDisclosure(calls.metadata);
|
|
161
|
+
if (disclosure) lines.push(disclosure);
|
|
162
|
+
return { content: lines.join("\n"), evidenceList: calls.metadata };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function renderImplementationsResult(options: {
|
|
166
|
+
implementations: EvidenceList<ImplementationEntry>;
|
|
167
|
+
externalCount: number;
|
|
168
|
+
cwd: string;
|
|
169
|
+
targetName?: string;
|
|
170
|
+
}): { content: string; evidenceList: EvidenceListMetadata } {
|
|
171
|
+
const lines: string[] = [
|
|
172
|
+
options.targetName
|
|
173
|
+
? `# Implementations of \`${options.targetName}\` (semantic)`
|
|
174
|
+
: "# Implementations (semantic)",
|
|
175
|
+
"",
|
|
176
|
+
];
|
|
177
|
+
const total =
|
|
178
|
+
options.implementations.metadata.totalCount ?? options.implementations.metadata.shownCount;
|
|
179
|
+
if (total > 0) {
|
|
180
|
+
lines.push(`**${total} implementation${total === 1 ? "" : "s"}** in the project`, "");
|
|
181
|
+
for (const [file, locations] of groupByFile(options.implementations.items, options.cwd)) {
|
|
182
|
+
lines.push(`### ${file}`, `- ${compactLineRanges(locations)}`, "");
|
|
183
|
+
}
|
|
184
|
+
const disclosure = renderEvidenceListMetadataDisclosure(options.implementations.metadata);
|
|
185
|
+
if (disclosure) lines.push(disclosure);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (options.externalCount > 0) {
|
|
189
|
+
lines.push(
|
|
190
|
+
`_+${options.externalCount} external location${options.externalCount === 1 ? "" : "s"} (outside this project)_`,
|
|
191
|
+
"",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const invalidDisclosure = renderInvalidProviderLocations(options.implementations.metadata);
|
|
195
|
+
if (invalidDisclosure) lines.push(invalidDisclosure, "");
|
|
196
|
+
|
|
197
|
+
return { content: lines.join("\n"), evidenceList: options.implementations.metadata };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatScopeRange(scope: CalleeScope): string {
|
|
201
|
+
if (scope.startLine === scope.endLine) return `L${scope.startLine}`;
|
|
202
|
+
return `L${scope.startLine}–L${scope.endLine}`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function groupByFile(impls: readonly ImplementationEntry[], cwd: string): Map<string, number[]> {
|
|
206
|
+
const byFile = new Map<string, number[]>();
|
|
207
|
+
for (const impl of impls) {
|
|
208
|
+
const displayPath = toDisplayPath(cwd, impl.file);
|
|
209
|
+
const group = byFile.get(displayPath) ?? [];
|
|
210
|
+
group.push(impl.line);
|
|
211
|
+
byFile.set(displayPath, group);
|
|
212
|
+
}
|
|
213
|
+
return byFile;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Render an explicit disclosure for unusable semantic-provider locations. */
|
|
217
|
+
function renderInvalidProviderLocations(metadata: EvidenceListMetadata): string | null {
|
|
218
|
+
const count = metadata.invalidLocationCount ?? 0;
|
|
219
|
+
if (count <= 0 || metadata.partialReason === null) return null;
|
|
220
|
+
const noun = count === 1 ? "location" : "locations";
|
|
221
|
+
return `_${count} invalid provider ${noun} omitted (${metadata.partialReason})_`;
|
|
222
|
+
}
|
|
@@ -1,30 +1,85 @@
|
|
|
1
|
+
import type { EvidenceListMetadata } from "../../analysis/evidence.ts";
|
|
1
2
|
import { renderReadNextSection } from "../../analysis/read-next.ts";
|
|
3
|
+
import type {
|
|
4
|
+
OrientationItem,
|
|
5
|
+
OrientationProvenance,
|
|
6
|
+
OrientationSectionData,
|
|
7
|
+
} from "../../session/orientation-types.ts";
|
|
2
8
|
import { assembledReadNext } from "../result/assembly.ts";
|
|
3
9
|
import type { OrientationResultAssembly } from "../result/orientation.ts";
|
|
4
10
|
|
|
5
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Sole model-facing markdown adapter for Orientation.
|
|
13
|
+
*
|
|
14
|
+
* Collectors emit presentation-neutral facts (title, notes, and per-section
|
|
15
|
+
* {@link OrientationItem}s); this adapter owns all document framing — the
|
|
16
|
+
* leading notes, the h1 title, the optional Focus section, and each section's
|
|
17
|
+
* h2 heading + status note.
|
|
18
|
+
*/
|
|
6
19
|
export function renderOrientationResult(assembly: OrientationResultAssembly): string {
|
|
7
20
|
const data = assembly.assembled.data;
|
|
8
21
|
const lines: string[] = [];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
22
|
+
|
|
23
|
+
for (const note of data.notes) lines.push(note);
|
|
24
|
+
if (data.notes.length > 0) lines.push("");
|
|
25
|
+
|
|
26
|
+
lines.push(`# ${data.title}`, "");
|
|
27
|
+
|
|
28
|
+
if (data.target && data.focusTarget) {
|
|
29
|
+
lines.push("## Focus", `- \`${data.focusTarget}\``, "");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (const section of data.sections) {
|
|
33
|
+
lines.push(`## ${section.title}`, formatSectionNote(section));
|
|
34
|
+
renderItems(lines, section.items);
|
|
35
|
+
lines.push("");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
lines.push(...renderReadNextSection(assembledReadNext(assembly.assembled)));
|
|
39
|
+
return lines.join("\n").trimEnd();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderItems(lines: string[], items: readonly OrientationItem[]): void {
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
switch (item.kind) {
|
|
14
45
|
case "paragraph":
|
|
15
|
-
lines.push(
|
|
46
|
+
lines.push(item.text);
|
|
16
47
|
break;
|
|
17
48
|
case "list-item":
|
|
18
|
-
lines.push(`- ${
|
|
49
|
+
lines.push(`- ${item.text}`);
|
|
50
|
+
break;
|
|
51
|
+
case "subheading":
|
|
52
|
+
lines.push(`### ${item.text}`);
|
|
19
53
|
break;
|
|
20
54
|
case "code":
|
|
21
|
-
lines.push(`\`\`\`${
|
|
55
|
+
lines.push(`\`\`\`${item.language ?? ""}`, ...item.lines, "```");
|
|
22
56
|
break;
|
|
23
57
|
case "blank":
|
|
24
58
|
lines.push("");
|
|
25
59
|
break;
|
|
26
60
|
}
|
|
27
61
|
}
|
|
28
|
-
|
|
29
|
-
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Render the same section status/provenance metadata stored in result details. */
|
|
65
|
+
function formatSectionNote(section: OrientationSectionData): string {
|
|
66
|
+
const facts = section.evidenceLists.map(evidenceSummary).join("; ");
|
|
67
|
+
const provenance = section.provenance.map(formatProvenance).join(", ");
|
|
68
|
+
return `_(status: ${section.status}; provenance: ${provenance || "none"}${section.reason ? `; ${section.reason}` : ""}${facts ? `; ${facts}` : ""})_`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function evidenceSummary(metadata: EvidenceListMetadata): string {
|
|
72
|
+
if (metadata.totalCount === null) {
|
|
73
|
+
const omitted = metadata.omittedCount ? `; ${metadata.omittedCount} collected omitted` : "";
|
|
74
|
+
return `showing ${metadata.shownCount}${omitted}; more may exist — ${metadata.partialReason ?? "partial"}`;
|
|
75
|
+
}
|
|
76
|
+
if ((metadata.omittedCount ?? 0) > 0) {
|
|
77
|
+
return `showing ${metadata.shownCount} of ${metadata.totalCount}; ${metadata.omittedCount} omitted`;
|
|
78
|
+
}
|
|
79
|
+
return `${metadata.totalCount} observed`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatProvenance(provenance: OrientationProvenance): string {
|
|
83
|
+
const detail = provenance.detail ? ` \`${provenance.detail}\`` : "";
|
|
84
|
+
return `${provenance.source}${provenance.capability ? ` (${provenance.capability})` : ""}${detail}`;
|
|
30
85
|
}
|
package/src/tool/register.ts
CHANGED
|
@@ -74,12 +74,22 @@ function getToolRenderer(name: string): ToolRenderer {
|
|
|
74
74
|
* @param getOrCreateSession — session factory; production passes the app-managed
|
|
75
75
|
* sessions, tests pass a `createSessionCache`-backed factory.
|
|
76
76
|
*/
|
|
77
|
+
const registeredPis = new WeakSet<object>();
|
|
78
|
+
|
|
77
79
|
export function registerCodeIntelligenceTools(
|
|
78
80
|
pi: ExtensionAPI,
|
|
79
81
|
getOrCreateSession: (cwd: string) => WorkspaceCodeIntelligenceSession,
|
|
80
82
|
promptSurfaces: CodeIntelligenceToolPromptSurfaceMap = CODE_INTELLIGENCE_TOOL_PROMPT_SURFACES,
|
|
81
83
|
specs: readonly CodeIntelligenceToolDefinitionSpec[] = CODE_INTELLIGENCE_TOOL_SPECS,
|
|
82
84
|
): void {
|
|
85
|
+
// Skip when another copy is already loaded (e.g. standalone install + bundled
|
|
86
|
+
// copy inside supi-review). Pi loads packages with separate module roots, so
|
|
87
|
+
// module-level state is not shared. Key on the pi object: both copies receive
|
|
88
|
+
// the same ExtensionAPI in one process, while tests get fresh mocks.
|
|
89
|
+
// ponytail: WeakSet on pi identity; getAllTools() is not callable during load.
|
|
90
|
+
if (registeredPis.has(pi)) return;
|
|
91
|
+
registeredPis.add(pi);
|
|
92
|
+
|
|
83
93
|
for (const spec of specs) {
|
|
84
94
|
const surface = promptSurfaces[spec.name];
|
|
85
95
|
pi.registerTool({
|
package/src/ui/footer.ts
CHANGED
|
@@ -1,29 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LSP footer stats-end contribution for code-intelligence.
|
|
3
3
|
*
|
|
4
|
-
* Provides the persistent "| λ lsp • N
|
|
4
|
+
* Provides the persistent "| λ lsp • N ✓ • M open files" suffix on the
|
|
5
5
|
* footer stats line, after cost and context info. This was removed during the
|
|
6
6
|
* supi-lsp → supi-code-intelligence migration.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
10
|
import { footerContributions } from "@mrclrchtr/supi-core/footer-registry";
|
|
10
|
-
import type
|
|
11
|
+
import { LSP_STATE_CHANGE_EVENT, type LspAdapterState } from "../substrate/lsp/state.ts";
|
|
11
12
|
|
|
12
|
-
/** Build the LSP status text: "λ lsp •
|
|
13
|
+
/** Build the LSP status text: "λ lsp • 3 ✓ • 1 ⟳ • 1 ✗ • 2 open files" */
|
|
13
14
|
export function buildLspStatusText(lspState: LspAdapterState): string | undefined {
|
|
14
15
|
const controller = lspState.controller;
|
|
15
16
|
const runtime = controller?.workspaceRuntime;
|
|
16
17
|
if (!runtime) return undefined;
|
|
17
18
|
|
|
18
19
|
const servers = runtime.getProjectServers();
|
|
19
|
-
const runningServers = servers.filter((s) => s.status === "running").length;
|
|
20
20
|
const openFiles = servers.reduce((sum, s) => sum + s.openFiles.length, 0);
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// Aggregate server states
|
|
23
|
+
const ready = servers.filter((s) => s.status === "running" && s.ready).length;
|
|
24
|
+
const starting = servers.filter((s) => s.status === "running" && !s.ready).length;
|
|
25
|
+
const error = servers.filter((s) => s.status === "error").length;
|
|
26
|
+
const unavailable = servers.filter((s) => s.status === "unavailable").length;
|
|
27
|
+
|
|
28
|
+
const hasServers = ready + starting + error + unavailable > 0;
|
|
29
|
+
if (!hasServers && openFiles === 0) return undefined;
|
|
23
30
|
|
|
24
31
|
const parts = ["λ lsp"];
|
|
25
|
-
if (
|
|
26
|
-
|
|
32
|
+
if (ready > 0) parts.push(`${ready} ✓`);
|
|
33
|
+
if (starting > 0) parts.push(`${starting} ⟳`);
|
|
34
|
+
if (error > 0) parts.push(`${error} ✗`);
|
|
35
|
+
if (unavailable > 0) parts.push(`${unavailable} ⊘`);
|
|
27
36
|
if (openFiles > 0) parts.push(`${openFiles} ${openFiles === 1 ? "open file" : "open files"}`);
|
|
28
37
|
return parts.join(" • ");
|
|
29
38
|
}
|
|
@@ -33,7 +42,12 @@ export function buildLspStatusText(lspState: LspAdapterState): string | undefine
|
|
|
33
42
|
const FOOTER_KEY = "lsp-status";
|
|
34
43
|
|
|
35
44
|
/** Register the LSP stats-end footer contribution for the given adapter state. */
|
|
36
|
-
export function registerLspFooterContribution(
|
|
45
|
+
export function registerLspFooterContribution(
|
|
46
|
+
pi: ExtensionAPI,
|
|
47
|
+
lspState: LspAdapterState,
|
|
48
|
+
): {
|
|
49
|
+
dispose: () => void;
|
|
50
|
+
} {
|
|
37
51
|
footerContributions.register({
|
|
38
52
|
key: FOOTER_KEY,
|
|
39
53
|
placement: "stats-end",
|
|
@@ -44,6 +58,17 @@ export function registerLspFooterContribution(lspState: LspAdapterState): void {
|
|
|
44
58
|
return `| ${text}`;
|
|
45
59
|
},
|
|
46
60
|
});
|
|
61
|
+
|
|
62
|
+
// Trigger TUI re-render when server states change.
|
|
63
|
+
const handler = () => pi.events.emit("supi:lsp:invalidate", {});
|
|
64
|
+
lspState.stateChanges.addEventListener(LSP_STATE_CHANGE_EVENT, handler);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
dispose() {
|
|
68
|
+
lspState.stateChanges.removeEventListener(LSP_STATE_CHANGE_EVENT, handler);
|
|
69
|
+
unregisterLspFooterContribution();
|
|
70
|
+
},
|
|
71
|
+
};
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
/** Remove the LSP stats footer contribution. */
|