@danypops/pi-lector 0.13.9 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extension/src/apply-patch/rendering.ts +2 -1
- package/extension/src/code-intelligence/operations.ts +109 -3
- package/extension/src/code-intelligence/rendering.ts +129 -5
- package/extension/src/cross-workspace-search/rendering.ts +10 -2
- package/extension/src/editor/editor-state.ts +38 -1
- package/extension/src/editor/index.ts +9 -1
- package/extension/src/editor/modal-editor-component.ts +9 -3
- package/extension/src/external-search/rendering.ts +46 -12
- package/extension/src/find-files/rendering.ts +2 -1
- package/extension/src/find-symbols/rendering.ts +2 -1
- package/extension/src/git/operations.ts +23 -1
- package/extension/src/git/rendering.ts +48 -8
- package/extension/src/index.ts +495 -55
- package/extension/src/line-edit/rendering.ts +2 -1
- package/extension/src/package-source/rendering.ts +4 -3
- package/extension/src/presentation/model-content.ts +62 -0
- package/extension/src/presentation/presentation-contract.ts +148 -0
- package/extension/src/presentation/tool-presentation.ts +170 -0
- package/extension/src/repo-cache/rendering.ts +4 -3
- package/extension/src/search/rendering.ts +6 -3
- package/extension/src/symbol-annotation/operations.ts +8 -4
- package/extension/src/vehicle-client.ts +4 -2
- package/extension/src/workspace-cache/rendering.ts +4 -3
- package/package.json +4 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { EditOutcome } from "@danypops/lector";
|
|
2
2
|
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
3
3
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
4
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The applied result (EditOutcome) only carries a hash transition, not the
|
|
@@ -15,7 +16,7 @@ const DEFAULT_VISIBLE_PATCH_LINES = 12;
|
|
|
15
16
|
|
|
16
17
|
export function formatApplyPatchCall(args: { path?: unknown; patchText?: unknown }, theme: LectorTheme, measure?: TextMeasure): string {
|
|
17
18
|
const path = typeof args.path === "string" ? args.path : "";
|
|
18
|
-
const header = `${theme.fg("toolTitle", theme.bold("apply_patch"))} ${theme.fg("accent", path)}`;
|
|
19
|
+
const header = `${theme.fg("toolTitle", theme.bold(presentationTitle("apply_patch")))} ${theme.fg("accent", path)}`;
|
|
19
20
|
if (typeof args.patchText !== "string" || args.patchText.length === 0) return header;
|
|
20
21
|
|
|
21
22
|
const styledLines = renderDiffLines(
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JobSnapshot, OperationInputs, OperationOutputs, PopulateSymbolGraphResult, SymbolEdgeKind, SymbolNode } from "@danypops/lector";
|
|
2
2
|
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath, workspaceForPathOrDirectory } from "../lector-client.ts";
|
|
3
|
+
import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
|
|
4
|
+
|
|
5
|
+
const CODE_ACTION_PREVIEW_PERMISSIONS = ["workspace:read"];
|
|
6
|
+
const CODE_ACTION_APPLY_PERMISSIONS = ["workspace:write"];
|
|
3
7
|
|
|
4
8
|
/**
|
|
5
9
|
* Thin wrappers over Lector's code-intelligence operations: goToDefinition,
|
|
@@ -34,9 +38,50 @@ export interface CodeIntelligenceOperations {
|
|
|
34
38
|
hover(path: string, line: number, character: number): Promise<OperationOutputs["workspace.hover"]>;
|
|
35
39
|
documentSymbols(path: string): Promise<OperationOutputs["workspace.documentSymbols"]>;
|
|
36
40
|
diagnostics(path: string): Promise<OperationOutputs["workspace.diagnostics"]>;
|
|
41
|
+
previewCodeActions(
|
|
42
|
+
path: string,
|
|
43
|
+
input: Omit<OperationInputs["workspace.previewCodeActions"], "workspaceId" | "path">,
|
|
44
|
+
call: LectorVehicleCall,
|
|
45
|
+
): Promise<OperationOutputs["workspace.previewCodeActions"]>;
|
|
46
|
+
applyCodeAction(
|
|
47
|
+
path: string,
|
|
48
|
+
previewId: OperationInputs["workspace.applyCodeAction"]["previewId"],
|
|
49
|
+
call: LectorVehicleCall,
|
|
50
|
+
): Promise<OperationOutputs["workspace.applyCodeAction"]>;
|
|
51
|
+
diagnosticDelta(
|
|
52
|
+
path: string,
|
|
53
|
+
source: OperationInputs["workspace.diagnosticDelta"]["source"],
|
|
54
|
+
bounds?: Omit<OperationInputs["workspace.diagnosticDelta"], "workspaceId" | "source">,
|
|
55
|
+
): Promise<OperationOutputs["workspace.diagnosticDelta"]>;
|
|
37
56
|
prepareCallHierarchy(path: string, line: number, character: number): Promise<OperationOutputs["workspace.prepareCallHierarchy"]>;
|
|
38
57
|
incomingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.incomingCalls"]>;
|
|
39
58
|
outgoingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.outgoingCalls"]>;
|
|
59
|
+
prepareTypeHierarchy(
|
|
60
|
+
path: string,
|
|
61
|
+
line: number,
|
|
62
|
+
character: number,
|
|
63
|
+
bounds?: Pick<OperationInputs["workspace.prepareTypeHierarchy"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
64
|
+
): Promise<OperationOutputs["workspace.prepareTypeHierarchy"]>;
|
|
65
|
+
supertypes(
|
|
66
|
+
path: string,
|
|
67
|
+
line: number,
|
|
68
|
+
character: number,
|
|
69
|
+
bounds?: Pick<OperationInputs["workspace.supertypes"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
70
|
+
): Promise<OperationOutputs["workspace.supertypes"]>;
|
|
71
|
+
subtypes(
|
|
72
|
+
path: string,
|
|
73
|
+
line: number,
|
|
74
|
+
character: number,
|
|
75
|
+
bounds?: Pick<OperationInputs["workspace.subtypes"], "maxResults" | "maxBytes" | "deadlineMs">,
|
|
76
|
+
): Promise<OperationOutputs["workspace.subtypes"]>;
|
|
77
|
+
impactAnalysis(
|
|
78
|
+
path: string,
|
|
79
|
+
source: OperationInputs["workspace.impactAnalysis"]["source"],
|
|
80
|
+
bounds: Pick<
|
|
81
|
+
OperationInputs["workspace.impactAnalysis"],
|
|
82
|
+
"maxDepth" | "maxNodes" | "maxEdges" | "maxBytes" | "deadlineMs" | "coverage" | "autoPopulate" | "maxFiles" | "maxSymbolsPerFile"
|
|
83
|
+
>,
|
|
84
|
+
): Promise<OperationOutputs["workspace.impactAnalysis"]>;
|
|
40
85
|
/**
|
|
41
86
|
* Not exposed as a standalone Pi tool -- every workspace auto-populates on first touch via
|
|
42
87
|
* monitorWorkspaceCache (workspace-cache/operations.ts). Kept here as an internal capability
|
|
@@ -46,7 +91,14 @@ export interface CodeIntelligenceOperations {
|
|
|
46
91
|
populateSymbolGraph(path: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
47
92
|
/** Not exposed as a standalone Pi tool -- see populateSymbolGraph. */
|
|
48
93
|
jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
49
|
-
reachableFrom(
|
|
94
|
+
reachableFrom(
|
|
95
|
+
path: string,
|
|
96
|
+
line: number,
|
|
97
|
+
character: number,
|
|
98
|
+
maxDepth: number,
|
|
99
|
+
kind?: SymbolEdgeKind,
|
|
100
|
+
autoPopulation?: Pick<OperationInputs["workspace.reachableFrom"], "autoPopulate" | "maxFiles" | "maxSymbolsPerFile">,
|
|
101
|
+
): Promise<readonly SymbolNode[]>;
|
|
50
102
|
/** Never spawns a symbol index -- safe to call opportunistically (e.g. before deciding whether to enrich a result). */
|
|
51
103
|
hasWarmIndex(path: string): Promise<boolean>;
|
|
52
104
|
workspaceMap(path: string, maxNodes: number, maxEdges: number, maxEntries: number, maxBytes: number): Promise<OperationOutputs["workspace.map"]>;
|
|
@@ -120,6 +172,36 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
120
172
|
},
|
|
121
173
|
);
|
|
122
174
|
},
|
|
175
|
+
async previewCodeActions(path, input, call) {
|
|
176
|
+
return withWorkspace(
|
|
177
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
178
|
+
({ workspaceId }) =>
|
|
179
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.previewCodeActions"]>(
|
|
180
|
+
"workspace.previewCodeActions",
|
|
181
|
+
{ workspaceId, path, ...input },
|
|
182
|
+
CODE_ACTION_PREVIEW_PERMISSIONS,
|
|
183
|
+
call,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
},
|
|
187
|
+
async applyCodeAction(path, previewId, call) {
|
|
188
|
+
return withWorkspace(
|
|
189
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
190
|
+
({ workspaceId }) =>
|
|
191
|
+
invokeLectorVehicleOperation<OperationOutputs["workspace.applyCodeAction"]>(
|
|
192
|
+
"workspace.applyCodeAction",
|
|
193
|
+
{ workspaceId, previewId },
|
|
194
|
+
CODE_ACTION_APPLY_PERMISSIONS,
|
|
195
|
+
call,
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
},
|
|
199
|
+
async diagnosticDelta(path, source, bounds) {
|
|
200
|
+
return withWorkspace(
|
|
201
|
+
() => workspaceForPathOrDirectory(path),
|
|
202
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.diagnosticDelta", { workspaceId, source, ...bounds }),
|
|
203
|
+
);
|
|
204
|
+
},
|
|
123
205
|
async prepareCallHierarchy(path, line, character) {
|
|
124
206
|
return withWorkspace(
|
|
125
207
|
() => workspaceForCodeIntelligencePath(path),
|
|
@@ -147,6 +229,30 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
147
229
|
},
|
|
148
230
|
);
|
|
149
231
|
},
|
|
232
|
+
async prepareTypeHierarchy(path, line, character, bounds) {
|
|
233
|
+
return withWorkspace(
|
|
234
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
235
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.prepareTypeHierarchy", { workspaceId, path, line, character, ...bounds }),
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
async supertypes(path, line, character, bounds) {
|
|
239
|
+
return withWorkspace(
|
|
240
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
241
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.supertypes", { workspaceId, path, line, character, ...bounds }),
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
async subtypes(path, line, character, bounds) {
|
|
245
|
+
return withWorkspace(
|
|
246
|
+
() => workspaceForCodeIntelligencePath(path),
|
|
247
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.subtypes", { workspaceId, path, line, character, ...bounds }),
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
async impactAnalysis(path, source, bounds) {
|
|
251
|
+
return withWorkspace(
|
|
252
|
+
() => workspaceForPathOrDirectory(path),
|
|
253
|
+
async ({ workspaceId }) => (await lectorClient()).call("workspace.impactAnalysis", { workspaceId, source, ...bounds }),
|
|
254
|
+
);
|
|
255
|
+
},
|
|
150
256
|
async populateSymbolGraph(path, maxFiles, maxSymbolsPerFile, waitMs = 500) {
|
|
151
257
|
return withWorkspace(
|
|
152
258
|
() => workspaceForPathOrDirectory(path),
|
|
@@ -167,12 +273,12 @@ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIn
|
|
|
167
273
|
const { job } = await client.call("job.status", { jobId });
|
|
168
274
|
return job;
|
|
169
275
|
},
|
|
170
|
-
async reachableFrom(path, line, character, maxDepth, kind) {
|
|
276
|
+
async reachableFrom(path, line, character, maxDepth, kind, autoPopulation) {
|
|
171
277
|
return withWorkspace(
|
|
172
278
|
() => workspaceForCodeIntelligencePath(path),
|
|
173
279
|
async ({ workspaceId }) => {
|
|
174
280
|
const client = await lectorClient();
|
|
175
|
-
const { symbols } = await client.call("workspace.reachableFrom", { workspaceId, path, line, character, maxDepth, kind });
|
|
281
|
+
const { symbols } = await client.call("workspace.reachableFrom", { workspaceId, path, line, character, maxDepth, kind, ...autoPopulation });
|
|
176
282
|
return symbols;
|
|
177
283
|
},
|
|
178
284
|
);
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
Hover,
|
|
6
6
|
IncomingCall,
|
|
7
7
|
IntelligenceProvenance,
|
|
8
|
+
OperationOutputs,
|
|
8
9
|
OutgoingCall,
|
|
9
10
|
SymbolNode,
|
|
10
11
|
WorkspaceLocation,
|
|
@@ -13,6 +14,7 @@ import type {
|
|
|
13
14
|
import { keyHint, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
15
16
|
import { colorForKind, formatLocation, type LectorTheme } from "../lector-tui-theme.ts";
|
|
17
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* Custom TUI rendering for the code-intelligence tools: go_to_definition,
|
|
@@ -27,6 +29,7 @@ const DEFAULT_VISIBLE_LOCATIONS = 8;
|
|
|
27
29
|
const DEFAULT_VISIBLE_SYMBOLS = 12;
|
|
28
30
|
const DEFAULT_VISIBLE_DIAGNOSTICS = 12;
|
|
29
31
|
const DEFAULT_VISIBLE_CALLS = 12;
|
|
32
|
+
const DEFAULT_VISIBLE_CHANGES = 12;
|
|
30
33
|
|
|
31
34
|
const DIAGNOSTIC_SEVERITY_COLOR: Record<Diagnostic["severity"], ThemeColor> = {
|
|
32
35
|
error: "error",
|
|
@@ -39,7 +42,7 @@ function formatPositionalCall(toolName: string, args: { path?: unknown; line?: u
|
|
|
39
42
|
const path = typeof args.path === "string" ? args.path : "";
|
|
40
43
|
const line = typeof args.line === "number" ? args.line : "?";
|
|
41
44
|
const character = typeof args.character === "number" ? args.character : "?";
|
|
42
|
-
return `${theme.fg("toolTitle", theme.bold(toolName))} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
45
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle(toolName)))} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
function moreLine(theme: LectorTheme): (hidden: number) => string {
|
|
@@ -108,7 +111,7 @@ export function formatHoverResult(hover: Hover | undefined, expanded: boolean, t
|
|
|
108
111
|
|
|
109
112
|
export function formatDocumentSymbolsCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
110
113
|
const path = typeof args.path === "string" ? args.path : "";
|
|
111
|
-
return `${theme.fg("toolTitle", theme.bold("document_symbols"))} ${theme.fg("accent", path)}`;
|
|
114
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("document_symbols")))} ${theme.fg("accent", path)}`;
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/** Flattens a hierarchical DocumentSymbolEntry[] into (depth, entry) pairs, depth-first, for bounded rendering. */
|
|
@@ -147,7 +150,7 @@ export function formatDocumentSymbolsResult(symbols: readonly DocumentSymbolEntr
|
|
|
147
150
|
|
|
148
151
|
export function formatDiagnosticsCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
149
152
|
const path = typeof args.path === "string" ? args.path : "";
|
|
150
|
-
return `${theme.fg("toolTitle", theme.bold("diagnostics"))} ${theme.fg("accent", path)}`;
|
|
153
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("diagnostics")))} ${theme.fg("accent", path)}`;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
@@ -171,6 +174,127 @@ export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | und
|
|
|
171
174
|
return lines.join("\n");
|
|
172
175
|
}
|
|
173
176
|
|
|
177
|
+
function formatPathCall(toolName: string, args: { path?: unknown }, theme: LectorTheme, qualifier = ""): string {
|
|
178
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
179
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle(toolName)))}${qualifier ? ` ${theme.fg("muted", qualifier)}` : ""} ${theme.fg("accent", path)}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function formatCodeActionPreviewCall(
|
|
183
|
+
args: { path?: unknown; startLine?: unknown; startCharacter?: unknown; endLine?: unknown; endCharacter?: unknown },
|
|
184
|
+
theme: LectorTheme,
|
|
185
|
+
): string {
|
|
186
|
+
const range = `${typeof args.startLine === "number" ? args.startLine : "?"}:${typeof args.startCharacter === "number" ? args.startCharacter : "?"}-${typeof args.endLine === "number" ? args.endLine : "?"}:${typeof args.endCharacter === "number" ? args.endCharacter : "?"}`;
|
|
187
|
+
return formatPathCall("code_action_preview", args, theme, range);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function formatCodeActionPreviewResult(
|
|
191
|
+
result: OperationOutputs["workspace.previewCodeActions"] | undefined,
|
|
192
|
+
expanded: boolean,
|
|
193
|
+
theme: LectorTheme,
|
|
194
|
+
): string {
|
|
195
|
+
if (!result || result.actions.length === 0) return theme.fg("dim", "No code actions.");
|
|
196
|
+
return [
|
|
197
|
+
theme.fg("muted", `${result.actions.length} code action${result.actions.length === 1 ? "" : "s"}:`),
|
|
198
|
+
...renderTruncatedList({
|
|
199
|
+
items: result.actions,
|
|
200
|
+
expanded,
|
|
201
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
202
|
+
formatItem: (action) => {
|
|
203
|
+
const state = action.disabledReason
|
|
204
|
+
? theme.fg("warning", `disabled: ${action.disabledReason}`)
|
|
205
|
+
: action.preferred
|
|
206
|
+
? theme.fg("success", "preferred")
|
|
207
|
+
: "";
|
|
208
|
+
const paths = action.affectedPaths.length > 0 ? action.affectedPaths.join(", ") : "command only";
|
|
209
|
+
return ` ${theme.bold(action.title)}${action.kind ? ` (${action.kind})` : ""} -- ${paths}${state ? ` -- ${state}` : ""}`;
|
|
210
|
+
},
|
|
211
|
+
moreLine: moreLine(theme),
|
|
212
|
+
truncationWarning: result.truncated || result.deadlineReached ? theme.fg("warning", "results truncated by the requested bounds") : undefined,
|
|
213
|
+
}),
|
|
214
|
+
].join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function formatCodeActionApplyCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
218
|
+
return formatPathCall("code_action_apply", args, theme);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function formatCodeActionApplyResult(result: OperationOutputs["workspace.applyCodeAction"] | undefined, theme: LectorTheme): string {
|
|
222
|
+
if (!result) return theme.fg("dim", "No code-action result.");
|
|
223
|
+
const lines = result.touchedPaths.map((path) => ` ${path}`);
|
|
224
|
+
if (result.transactionId) lines.push(theme.fg("muted", `transaction ${result.transactionId}`));
|
|
225
|
+
if (result.pendingCommand) lines.push(theme.fg("warning", `pending command ${result.pendingCommand.command}`));
|
|
226
|
+
return lines.length > 0 ? lines.join("\n") : theme.fg("dim", "Code action made no file changes.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function formatDiagnosticDeltaCall(args: { path?: unknown; source?: unknown }, theme: LectorTheme): string {
|
|
230
|
+
return formatPathCall("diagnostic_delta", args, theme, typeof args.source === "string" ? args.source : "");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function formatDiagnosticDeltaResult(result: OperationOutputs["workspace.diagnosticDelta"] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
234
|
+
if (!result) return theme.fg("dim", "No diagnostic delta.");
|
|
235
|
+
const changes = [
|
|
236
|
+
...result.introduced.map((diagnostic) => ({ label: "introduced", diagnostic })),
|
|
237
|
+
...result.resolved.map((diagnostic) => ({ label: "resolved", diagnostic })),
|
|
238
|
+
...result.changed.map(({ after }) => ({ label: "changed", diagnostic: after })),
|
|
239
|
+
];
|
|
240
|
+
if (changes.length === 0) return theme.fg("success", "No diagnostic changes.");
|
|
241
|
+
return renderTruncatedList({
|
|
242
|
+
items: changes,
|
|
243
|
+
expanded,
|
|
244
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
245
|
+
formatItem: ({ label, diagnostic }) =>
|
|
246
|
+
`${theme.fg(label === "introduced" ? "error" : label === "resolved" ? "success" : "warning", label)} ${formatLocation(theme, diagnostic.range.path, diagnostic.range.start.line, diagnostic.range.start.character)} -- ${diagnostic.message}`,
|
|
247
|
+
moreLine: moreLine(theme),
|
|
248
|
+
truncationWarning: result.truncated ? theme.fg("warning", "results truncated by maxResults/maxBytes") : undefined,
|
|
249
|
+
}).join("\n");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function formatTypeHierarchyCall(args: { direction?: unknown; path?: unknown; line?: unknown; character?: unknown }, theme: LectorTheme): string {
|
|
253
|
+
const direction = typeof args.direction === "string" ? args.direction : undefined;
|
|
254
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
255
|
+
const line = typeof args.line === "number" ? args.line : "?";
|
|
256
|
+
const character = typeof args.character === "number" ? args.character : "?";
|
|
257
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("type_hierarchy", direction)))} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function formatTypeHierarchyResult(
|
|
261
|
+
result: OperationOutputs["workspace.prepareTypeHierarchy"] | undefined,
|
|
262
|
+
expanded: boolean,
|
|
263
|
+
theme: LectorTheme,
|
|
264
|
+
): string {
|
|
265
|
+
if (!result || result.items.length === 0) return theme.fg("dim", "No type-hierarchy items found.");
|
|
266
|
+
return renderTruncatedList({
|
|
267
|
+
items: result.items,
|
|
268
|
+
expanded,
|
|
269
|
+
visibleCount: DEFAULT_VISIBLE_CALLS,
|
|
270
|
+
formatItem: (item) => ` ${formatCallHierarchyEntry(item, theme)}${item.detail ? theme.fg("dim", ` -- ${item.detail}`) : ""}`,
|
|
271
|
+
moreLine: moreLine(theme),
|
|
272
|
+
truncationWarning: result.truncated ? theme.fg("warning", "results truncated by the requested bounds") : undefined,
|
|
273
|
+
}).join("\n");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function formatImpactAnalysisCall(args: { path?: unknown; source?: unknown }, theme: LectorTheme): string {
|
|
277
|
+
return formatPathCall("impact_analysis", args, theme, typeof args.source === "string" ? args.source : "");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function formatImpactAnalysisResult(result: OperationOutputs["workspace.impactAnalysis"] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
281
|
+
if (!result) return theme.fg("dim", "No impact-analysis result.");
|
|
282
|
+
const impacts = [
|
|
283
|
+
...result.changedSymbols.map(({ symbol, side }) => ({ label: `changed ${side}`, symbol })),
|
|
284
|
+
...result.impactedSymbols.map(({ symbol, depth }) => ({ label: `impact depth=${depth}`, symbol })),
|
|
285
|
+
...result.relatedTests.map(({ symbol, evidence }) => ({ label: `test ${evidence.kind}`, symbol })),
|
|
286
|
+
];
|
|
287
|
+
if (impacts.length === 0) return theme.fg("dim", "No changed symbols resolved.");
|
|
288
|
+
return renderTruncatedList({
|
|
289
|
+
items: impacts,
|
|
290
|
+
expanded,
|
|
291
|
+
visibleCount: DEFAULT_VISIBLE_CHANGES,
|
|
292
|
+
formatItem: ({ label, symbol }) => `${theme.fg("muted", label)} ${formatCallHierarchyEntry(symbol, theme)}`,
|
|
293
|
+
moreLine: moreLine(theme),
|
|
294
|
+
truncationWarning: result.truncated || result.deadlineReached ? theme.fg("warning", "analysis truncated by the requested bounds") : undefined,
|
|
295
|
+
}).join("\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
174
298
|
function formatCallHierarchyEntry(entry: { kind: string; name: string; location: WorkspaceLocation }, theme: LectorTheme): string {
|
|
175
299
|
const kind = theme.fg(colorForKind(entry.kind), entry.kind);
|
|
176
300
|
const name = theme.fg("text", theme.bold(entry.name));
|
|
@@ -192,7 +316,7 @@ export function formatCallHierarchyCall(args: { direction?: unknown; path?: unkn
|
|
|
192
316
|
const path = typeof args.path === "string" ? args.path : "";
|
|
193
317
|
const line = typeof args.line === "number" ? args.line : "?";
|
|
194
318
|
const character = typeof args.character === "number" ? args.character : "?";
|
|
195
|
-
return `${theme.fg("toolTitle", theme.bold("call_hierarchy"
|
|
319
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("call_hierarchy", direction)))} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
196
320
|
}
|
|
197
321
|
|
|
198
322
|
function formatPrepareCallHierarchyResult(items: readonly CallHierarchyEntry[] | undefined, theme: LectorTheme): string {
|
|
@@ -265,7 +389,7 @@ export function formatReachableFromResult(symbols: readonly SymbolNode[] | undef
|
|
|
265
389
|
export function formatWorkspaceMapCall(args: { path?: unknown; maxEntries?: unknown }, theme: LectorTheme): string {
|
|
266
390
|
const path = typeof args.path === "string" ? args.path : "";
|
|
267
391
|
const maxEntries = typeof args.maxEntries === "number" ? ` (top ${args.maxEntries})` : "";
|
|
268
|
-
return `${theme.fg("toolTitle", theme.bold("workspace_map"))} ${theme.fg("dim", path)}${theme.fg("muted", maxEntries)}`;
|
|
392
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("workspace_map")))} ${theme.fg("dim", path)}${theme.fg("muted", maxEntries)}`;
|
|
269
393
|
}
|
|
270
394
|
|
|
271
395
|
export function formatWorkspaceMapResult(result: WorkspaceMapResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
@@ -3,14 +3,19 @@ import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
4
|
import { describeFindSymbolSources } from "../find-symbols/rendering.ts";
|
|
5
5
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
6
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
6
7
|
import type { CrossWorkspaceOutcome } from "./operations.ts";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_VISIBLE_PER_WORKSPACE = 10;
|
|
9
10
|
|
|
10
|
-
export function formatCrossWorkspaceCall(
|
|
11
|
+
export function formatCrossWorkspaceCall(
|
|
12
|
+
toolName: "find_symbols_across_projects" | "search_code_across_projects",
|
|
13
|
+
args: { directories?: unknown; query?: unknown },
|
|
14
|
+
theme: LectorTheme,
|
|
15
|
+
): string {
|
|
11
16
|
const directories = Array.isArray(args.directories) ? args.directories.filter((d): d is string => typeof d === "string") : [];
|
|
12
17
|
const query = typeof args.query === "string" ? args.query : "";
|
|
13
|
-
return `${theme.fg("accent", `"${query}"`)} ${theme.fg("dim", `across ${directories.length} project(s)`)}`;
|
|
18
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle(toolName)))} ${theme.fg("accent", `"${query}"`)} ${theme.fg("dim", `across ${directories.length} project(s)`)}`;
|
|
14
19
|
}
|
|
15
20
|
|
|
16
21
|
function formatOutcomeHeader(entry: CrossWorkspaceOutcome<unknown>, theme: LectorTheme): string {
|
|
@@ -83,6 +88,9 @@ export function formatSearchTextAcrossProjectsResult(
|
|
|
83
88
|
lines.push(formatOutcomeHeader(entry, theme));
|
|
84
89
|
const { outcome } = entry;
|
|
85
90
|
if (outcome.status !== "ready") continue;
|
|
91
|
+
if (outcome.result.provenance) {
|
|
92
|
+
lines.push(theme.fg("dim", ` lexical via ${outcome.result.provenance.backend} (${outcome.result.provenance.indexState})`));
|
|
93
|
+
}
|
|
86
94
|
if (outcome.result.matches.length === 0) {
|
|
87
95
|
lines.push(theme.fg("dim", " no matches"));
|
|
88
96
|
continue;
|
|
@@ -4,6 +4,26 @@ export type EditorMode = "normal" | "insert" | "command";
|
|
|
4
4
|
|
|
5
5
|
export type EditorAction = { kind: "save" } | { kind: "save-and-quit" } | { kind: "quit" } | { kind: "hover" };
|
|
6
6
|
|
|
7
|
+
export interface EditorSourcePosition {
|
|
8
|
+
/** One-based line number. */
|
|
9
|
+
readonly line: number;
|
|
10
|
+
/** One-based UTF-16 code-unit position within the line. */
|
|
11
|
+
readonly character: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type EditorPositionErrorCode = "line-out-of-range" | "character-out-of-range";
|
|
15
|
+
|
|
16
|
+
/** Reports why a requested source position cannot identify a cursor location in the current buffer. */
|
|
17
|
+
export class EditorPositionError extends Error {
|
|
18
|
+
constructor(
|
|
19
|
+
readonly code: EditorPositionErrorCode,
|
|
20
|
+
message: string,
|
|
21
|
+
) {
|
|
22
|
+
super(`${code}: ${message}`);
|
|
23
|
+
this.name = "EditorPositionError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
7
27
|
const BACKSPACE_KEYS = new Set(["\x7f", "\b"]);
|
|
8
28
|
|
|
9
29
|
/**
|
|
@@ -27,8 +47,25 @@ export class EditorState {
|
|
|
27
47
|
private insertPendingJ = false;
|
|
28
48
|
private yankedLine: string | undefined;
|
|
29
49
|
|
|
30
|
-
constructor(content: string) {
|
|
50
|
+
constructor(content: string, initialPosition?: EditorSourcePosition) {
|
|
31
51
|
this.buffer = new LiveBuffer(content);
|
|
52
|
+
if (initialPosition) this.moveToInitialPosition(initialPosition);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private moveToInitialPosition(position: EditorSourcePosition): void {
|
|
56
|
+
if (!Number.isSafeInteger(position.line) || position.line < 1 || position.line > this.buffer.lineCount) {
|
|
57
|
+
throw new EditorPositionError("line-out-of-range", `Line ${position.line} is outside this ${this.buffer.lineCount}-line buffer`);
|
|
58
|
+
}
|
|
59
|
+
const lineLength = this.buffer.lineText(position.line).length;
|
|
60
|
+
const maxCharacter = Math.max(lineLength, 1);
|
|
61
|
+
if (!Number.isSafeInteger(position.character) || position.character < 1 || position.character > maxCharacter) {
|
|
62
|
+
throw new EditorPositionError(
|
|
63
|
+
"character-out-of-range",
|
|
64
|
+
`Character ${position.character} is outside line ${position.line}'s ${lineLength} UTF-16 code units`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
this.cursorLine = position.line;
|
|
68
|
+
this.cursorCharacter = position.character;
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
get currentLineText(): string {
|
|
@@ -29,7 +29,14 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
export type { DirectoryExplorerSession } from "./directory-explorer-operations.ts";
|
|
32
|
-
export {
|
|
32
|
+
export {
|
|
33
|
+
type EditorAction,
|
|
34
|
+
type EditorMode,
|
|
35
|
+
EditorPositionError,
|
|
36
|
+
type EditorPositionErrorCode,
|
|
37
|
+
type EditorSourcePosition,
|
|
38
|
+
EditorState,
|
|
39
|
+
} from "./editor-state.ts";
|
|
33
40
|
export type { EditorTheme } from "./editor-theme.ts";
|
|
34
41
|
export { ExplorerComponent, type ExplorerResult, type ExplorerViewState, joinExplorerPath } from "./explorer-component.ts";
|
|
35
42
|
// runExplorerFlow is pure orchestration over the two interfaces above (browse, open a file into
|
|
@@ -44,4 +51,5 @@ export {
|
|
|
44
51
|
type EditorHoverRequest,
|
|
45
52
|
ModalEditorComponent,
|
|
46
53
|
type ModalEditorHost,
|
|
54
|
+
type ModalEditorOptions,
|
|
47
55
|
} from "./modal-editor-component.ts";
|
|
@@ -4,7 +4,7 @@ import { contentHashOf, highlightSpans } from "@danypops/lector";
|
|
|
4
4
|
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
6
6
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
-
import type { EditorAction } from "./editor-state.ts";
|
|
7
|
+
import type { EditorAction, EditorSourcePosition } from "./editor-state.ts";
|
|
8
8
|
import { EditorState } from "./editor-state.ts";
|
|
9
9
|
import type { EditorTheme } from "./editor-theme.ts";
|
|
10
10
|
|
|
@@ -26,6 +26,11 @@ export type EditorHoverOutcome =
|
|
|
26
26
|
| { readonly kind: "ready"; readonly hover?: { readonly contents: string } }
|
|
27
27
|
| { readonly kind: "stale-active-buffer"; readonly bufferHash: ContentHash };
|
|
28
28
|
|
|
29
|
+
export interface ModalEditorOptions {
|
|
30
|
+
/** Positions the cursor using one-based UTF-16 coordinates and scrolls it into the first viewport. */
|
|
31
|
+
readonly initialPosition?: EditorSourcePosition;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export interface ModalEditorHost {
|
|
30
35
|
filePath: string;
|
|
31
36
|
/** Saves the buffer's current text through Lector's hash-guarded write. Throws (surfaced as a status message, not a crash) on a genuinely concurrent external change. */
|
|
@@ -65,13 +70,14 @@ export class ModalEditorComponent implements Component {
|
|
|
65
70
|
private statusMessage = "";
|
|
66
71
|
private highlightCache: { text: string; spans: readonly HighlightSpan[] } | undefined;
|
|
67
72
|
|
|
68
|
-
constructor(tui: TUI, theme: EditorTheme, host: ModalEditorHost, content: string, done: () => void) {
|
|
73
|
+
constructor(tui: TUI, theme: EditorTheme, host: ModalEditorHost, content: string, done: () => void, options: ModalEditorOptions = {}) {
|
|
69
74
|
this.tui = tui;
|
|
70
75
|
this.theme = theme;
|
|
71
76
|
this.host = host;
|
|
72
77
|
this.done = done;
|
|
73
78
|
this.extension = extname(host.filePath);
|
|
74
|
-
this.state = new EditorState(content);
|
|
79
|
+
this.state = new EditorState(content, options.initialPosition);
|
|
80
|
+
this.scrollToKeepCursorVisible();
|
|
75
81
|
this.refreshHighlightsSafely();
|
|
76
82
|
}
|
|
77
83
|
|
|
@@ -1,27 +1,61 @@
|
|
|
1
1
|
import type { GithubRepoSearchResult, NpmPackageCandidate, SourcegraphCodeCandidate } from "@danypops/lector";
|
|
2
2
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
3
4
|
|
|
4
5
|
type ExternalSearchAction = "github_repos" | "npm_packages" | "sourcegraph_code";
|
|
5
6
|
|
|
6
7
|
export function formatExternalSearchCall(action: ExternalSearchAction, args: { query?: unknown }, theme: LectorTheme): string {
|
|
7
|
-
const label = theme.fg("toolTitle", theme.bold("external_search"));
|
|
8
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("external_search", action)));
|
|
8
9
|
const query = typeof args.query === "string" ? args.query : "";
|
|
9
|
-
return `${label} ${theme.fg("accent",
|
|
10
|
+
return `${label} ${theme.fg("accent", `"${query}"`)}`;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
const VISIBLE_CANDIDATES = 8;
|
|
14
|
+
|
|
15
|
+
function boundedCandidates<T>(candidates: readonly T[], expanded: boolean): { visible: readonly T[]; more: number } {
|
|
16
|
+
const visible = expanded ? candidates : candidates.slice(0, VISIBLE_CANDIDATES);
|
|
17
|
+
return { visible, more: candidates.length - visible.length };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatGithubRepoSearchResult(result: GithubRepoSearchResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
13
21
|
if (!result) return theme.fg("dim", "No result.");
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
22
|
+
if (result.candidates.length === 0) return theme.fg("dim", "no repositories matched");
|
|
23
|
+
const { visible, more } = boundedCandidates(result.candidates, expanded);
|
|
24
|
+
const lines = visible.map(
|
|
25
|
+
(candidate) =>
|
|
26
|
+
`${theme.fg("accent", `${candidate.host}/${candidate.owner}/${candidate.repo}`)} · ${candidate.stars} stars${candidate.language ? ` · ${candidate.language}` : ""}${candidate.description ? `\n ${candidate.description}` : ""}`,
|
|
27
|
+
);
|
|
28
|
+
if (more > 0) lines.push(theme.fg("muted", `… ${more} more (expand to show)`));
|
|
29
|
+
if (!result.authenticated) lines.push(theme.fg("warning", "unauthenticated -- lower rate limit"));
|
|
30
|
+
return lines.join("\n");
|
|
17
31
|
}
|
|
18
32
|
|
|
19
|
-
export function formatNpmPackageSearchResult(
|
|
20
|
-
|
|
21
|
-
|
|
33
|
+
export function formatNpmPackageSearchResult(
|
|
34
|
+
result: { candidates: readonly NpmPackageCandidate[] } | undefined,
|
|
35
|
+
expanded: boolean,
|
|
36
|
+
theme: LectorTheme,
|
|
37
|
+
): string {
|
|
38
|
+
if (!result || result.candidates.length === 0) return theme.fg("dim", "no packages matched");
|
|
39
|
+
const { visible, more } = boundedCandidates(result.candidates, expanded);
|
|
40
|
+
const lines = visible.map(
|
|
41
|
+
(candidate) =>
|
|
42
|
+
`${theme.fg("accent", `${candidate.name}@${candidate.version}`)} · score ${candidate.score.toFixed(3)}${candidate.description ? `\n ${candidate.description}` : ""}`,
|
|
43
|
+
);
|
|
44
|
+
if (more > 0) lines.push(theme.fg("muted", `… ${more} more (expand to show)`));
|
|
45
|
+
return lines.join("\n");
|
|
22
46
|
}
|
|
23
47
|
|
|
24
|
-
export function formatSourcegraphCodeSearchResult(
|
|
25
|
-
|
|
26
|
-
|
|
48
|
+
export function formatSourcegraphCodeSearchResult(
|
|
49
|
+
result: { candidates: readonly SourcegraphCodeCandidate[] } | undefined,
|
|
50
|
+
expanded: boolean,
|
|
51
|
+
theme: LectorTheme,
|
|
52
|
+
): string {
|
|
53
|
+
if (!result || result.candidates.length === 0) return theme.fg("dim", "no code matches");
|
|
54
|
+
const { visible, more } = boundedCandidates(result.candidates, expanded);
|
|
55
|
+
const lines = visible.map((candidate) => {
|
|
56
|
+
const matches = candidate.lineMatches.slice(0, expanded ? candidate.lineMatches.length : 3);
|
|
57
|
+
return `${theme.fg("accent", `${candidate.repository}/${candidate.path}`)}\n${matches.map((match) => ` ${match.line}: ${match.preview}`).join("\n")}`;
|
|
58
|
+
});
|
|
59
|
+
if (more > 0) lines.push(theme.fg("muted", `… ${more} more (expand to show)`));
|
|
60
|
+
return lines.join("\n");
|
|
27
61
|
}
|
|
@@ -2,13 +2,14 @@ import type { FindFilesResult } from "@danypops/lector";
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
4
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
5
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
5
6
|
|
|
6
7
|
const DEFAULT_VISIBLE_PATHS = 40;
|
|
7
8
|
|
|
8
9
|
export function formatFindFilesCall(args: { directory?: unknown; patterns?: unknown }, theme: LectorTheme): string {
|
|
9
10
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
10
11
|
const patterns = Array.isArray(args.patterns) ? args.patterns.filter((p): p is string => typeof p === "string") : [];
|
|
11
|
-
return `${theme.fg("toolTitle", theme.bold("find_files"))} ${theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "))} ${theme.fg("dim", directory)}`;
|
|
12
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("find_files")))} ${theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "))} ${theme.fg("dim", directory)}`;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function formatFindFilesResult(result: FindFilesResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
@@ -2,6 +2,7 @@ import type { SymbolSearchResult, WorkspaceSymbol } from "@danypops/lector";
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
4
|
import { colorForKind, formatLocation, type LectorTheme } from "../lector-tui-theme.ts";
|
|
5
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Custom TUI rendering for find_symbols -- the one Lector-backed tool with
|
|
@@ -18,7 +19,7 @@ const DEFAULT_VISIBLE_RESULTS = 8;
|
|
|
18
19
|
|
|
19
20
|
export function formatFindSymbolsCall(args: { query?: unknown; directory?: unknown }, theme: FindSymbolsTheme): string {
|
|
20
21
|
const query = typeof args.query === "string" ? args.query : "";
|
|
21
|
-
let content = `${theme.fg("toolTitle", theme.bold("find_symbols"))} ${theme.fg("accent", `"${query}"`)}`;
|
|
22
|
+
let content = `${theme.fg("toolTitle", theme.bold(presentationTitle("find_symbols")))} ${theme.fg("accent", `"${query}"`)}`;
|
|
22
23
|
if (typeof args.directory === "string" && args.directory.length > 0) {
|
|
23
24
|
content += ` ${theme.fg("muted", `in ${args.directory}`)}`;
|
|
24
25
|
}
|