@danypops/pi-lector 0.13.11 → 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/rendering.ts +12 -7
- package/extension/src/cross-workspace-search/rendering.ts +7 -2
- 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/rendering.ts +7 -5
- package/extension/src/index.ts +99 -42
- 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 +2 -1
- package/extension/src/workspace-cache/rendering.ts +4 -3
- package/package.json +1 -1
|
@@ -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(
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
import { keyHint, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
16
16
|
import { colorForKind, formatLocation, type LectorTheme } from "../lector-tui-theme.ts";
|
|
17
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Custom TUI rendering for the code-intelligence tools: go_to_definition,
|
|
@@ -41,7 +42,7 @@ function formatPositionalCall(toolName: string, args: { path?: unknown; line?: u
|
|
|
41
42
|
const path = typeof args.path === "string" ? args.path : "";
|
|
42
43
|
const line = typeof args.line === "number" ? args.line : "?";
|
|
43
44
|
const character = typeof args.character === "number" ? args.character : "?";
|
|
44
|
-
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}`)}`;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function moreLine(theme: LectorTheme): (hidden: number) => string {
|
|
@@ -110,7 +111,7 @@ export function formatHoverResult(hover: Hover | undefined, expanded: boolean, t
|
|
|
110
111
|
|
|
111
112
|
export function formatDocumentSymbolsCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
112
113
|
const path = typeof args.path === "string" ? args.path : "";
|
|
113
|
-
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)}`;
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
/** Flattens a hierarchical DocumentSymbolEntry[] into (depth, entry) pairs, depth-first, for bounded rendering. */
|
|
@@ -149,7 +150,7 @@ export function formatDocumentSymbolsResult(symbols: readonly DocumentSymbolEntr
|
|
|
149
150
|
|
|
150
151
|
export function formatDiagnosticsCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
151
152
|
const path = typeof args.path === "string" ? args.path : "";
|
|
152
|
-
return `${theme.fg("toolTitle", theme.bold("diagnostics"))} ${theme.fg("accent", path)}`;
|
|
153
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("diagnostics")))} ${theme.fg("accent", path)}`;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
@@ -175,7 +176,7 @@ export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | und
|
|
|
175
176
|
|
|
176
177
|
function formatPathCall(toolName: string, args: { path?: unknown }, theme: LectorTheme, qualifier = ""): string {
|
|
177
178
|
const path = typeof args.path === "string" ? args.path : "";
|
|
178
|
-
return `${theme.fg("toolTitle", theme.bold(toolName))}${qualifier ? ` ${theme.fg("muted", qualifier)}` : ""} ${theme.fg("accent", path)}`;
|
|
179
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle(toolName)))}${qualifier ? ` ${theme.fg("muted", qualifier)}` : ""} ${theme.fg("accent", path)}`;
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
export function formatCodeActionPreviewCall(
|
|
@@ -249,7 +250,11 @@ export function formatDiagnosticDeltaResult(result: OperationOutputs["workspace.
|
|
|
249
250
|
}
|
|
250
251
|
|
|
251
252
|
export function formatTypeHierarchyCall(args: { direction?: unknown; path?: unknown; line?: unknown; character?: unknown }, theme: LectorTheme): string {
|
|
252
|
-
|
|
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}`)}`;
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
export function formatTypeHierarchyResult(
|
|
@@ -311,7 +316,7 @@ export function formatCallHierarchyCall(args: { direction?: unknown; path?: unkn
|
|
|
311
316
|
const path = typeof args.path === "string" ? args.path : "";
|
|
312
317
|
const line = typeof args.line === "number" ? args.line : "?";
|
|
313
318
|
const character = typeof args.character === "number" ? args.character : "?";
|
|
314
|
-
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}`)}`;
|
|
315
320
|
}
|
|
316
321
|
|
|
317
322
|
function formatPrepareCallHierarchyResult(items: readonly CallHierarchyEntry[] | undefined, theme: LectorTheme): string {
|
|
@@ -384,7 +389,7 @@ export function formatReachableFromResult(symbols: readonly SymbolNode[] | undef
|
|
|
384
389
|
export function formatWorkspaceMapCall(args: { path?: unknown; maxEntries?: unknown }, theme: LectorTheme): string {
|
|
385
390
|
const path = typeof args.path === "string" ? args.path : "";
|
|
386
391
|
const maxEntries = typeof args.maxEntries === "number" ? ` (top ${args.maxEntries})` : "";
|
|
387
|
-
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)}`;
|
|
388
393
|
}
|
|
389
394
|
|
|
390
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 {
|
|
@@ -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
|
}
|
|
@@ -3,6 +3,7 @@ import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
5
5
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
6
|
+
import { presentationTitle } from "../presentation/tool-presentation.ts";
|
|
6
7
|
|
|
7
8
|
/** Real ANSI-aware measurement, not Malevich's own ASCII-only default -- every diff line renderDiffLines receives is already theme.fg-styled. */
|
|
8
9
|
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
@@ -61,31 +62,32 @@ export function formatGitCall(
|
|
|
61
62
|
theme: LectorTheme,
|
|
62
63
|
): string {
|
|
63
64
|
const action = typeof args.action === "string" ? args.action : "";
|
|
65
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("git", action)));
|
|
64
66
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
65
67
|
if (action === "compare-symbol") {
|
|
66
68
|
const path = typeof args.path === "string" ? args.path : "";
|
|
67
69
|
const symbol = typeof args.symbol === "string" ? args.symbol : "";
|
|
68
70
|
const fromRef = typeof args.fromRef === "string" ? args.fromRef : "";
|
|
69
71
|
const toRef = typeof args.toRef === "string" ? ` -> ${args.toRef}` : "";
|
|
70
|
-
return `${
|
|
72
|
+
return `${label} ${theme.fg("accent", `${directory}/${path}`)} (${symbol}) ${fromRef}${toRef}`;
|
|
71
73
|
}
|
|
72
74
|
if (action === "is-ancestor") {
|
|
73
75
|
const ancestorRef = typeof args.ancestorRef === "string" ? args.ancestorRef : "";
|
|
74
76
|
const ref = typeof args.ref === "string" ? args.ref : "";
|
|
75
|
-
return `${
|
|
77
|
+
return `${label} ${theme.fg("accent", directory)} ${ancestorRef} -> ${ref}`;
|
|
76
78
|
}
|
|
77
79
|
if (action === "grep-ref" || action === "grep-history") {
|
|
78
80
|
const ref = action === "grep-ref" && typeof args.ref === "string" ? `${args.ref} ` : "";
|
|
79
81
|
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
80
|
-
return `${
|
|
82
|
+
return `${label} ${theme.fg("accent", directory)} ${ref}"${pattern}"`;
|
|
81
83
|
}
|
|
82
84
|
if (action === "show") {
|
|
83
85
|
const ref = typeof args.ref === "string" ? args.ref : "";
|
|
84
86
|
const path = typeof args.path === "string" ? args.path : "";
|
|
85
|
-
return `${
|
|
87
|
+
return `${label} ${theme.fg("accent", `${directory}/${path}`)} @ ${ref}`;
|
|
86
88
|
}
|
|
87
89
|
const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
|
|
88
|
-
return `${
|
|
90
|
+
return `${label} ${theme.fg("accent", directory)}${ref}`;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
function formatGitWorktreeAddResult(result: GitWorktreeAddResult | undefined, theme: LectorTheme): string {
|
package/extension/src/index.ts
CHANGED
|
@@ -4,7 +4,6 @@ import { resolve } from "node:path";
|
|
|
4
4
|
import type {
|
|
5
5
|
CachedRepositoryPage,
|
|
6
6
|
ContentHash,
|
|
7
|
-
ContextBundleResult,
|
|
8
7
|
Diagnostic,
|
|
9
8
|
DocumentSymbolEntry,
|
|
10
9
|
EditOutcome,
|
|
@@ -121,6 +120,9 @@ import {
|
|
|
121
120
|
PACKAGE_SOURCE_LIST_VISIBLE_ROWS,
|
|
122
121
|
packageSourceListMoreLine,
|
|
123
122
|
} from "./package-source/rendering.ts";
|
|
123
|
+
import { formatSemanticModelContent } from "./presentation/model-content.ts";
|
|
124
|
+
import { withLectorPresentation } from "./presentation/presentation-contract.ts";
|
|
125
|
+
import { presentationTitle } from "./presentation/tool-presentation.ts";
|
|
124
126
|
import { createLectorReadOperations } from "./read/operations.ts";
|
|
125
127
|
import { createReferenceBasedRenameOperations } from "./reference-based-rename/operations.ts";
|
|
126
128
|
import { createRenameOperations } from "./rename/operations.ts";
|
|
@@ -189,7 +191,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
189
191
|
const customToolNames = new Set<string>();
|
|
190
192
|
function registerLectorTool<TParams extends TSchema, TDetails = unknown, TState = unknown>(tool: ToolDefinition<TParams, TDetails, TState>): void {
|
|
191
193
|
customToolNames.add(tool.name);
|
|
192
|
-
pi.registerTool(tool);
|
|
194
|
+
pi.registerTool(withLectorPresentation(tool));
|
|
193
195
|
}
|
|
194
196
|
pi.on("tool_result", (event) => {
|
|
195
197
|
if (!customToolNames.has(event.toolName)) return;
|
|
@@ -471,7 +473,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
471
473
|
return { content: [{ type: "text", text }], details: result };
|
|
472
474
|
},
|
|
473
475
|
renderCall(args, theme) {
|
|
474
|
-
return new Text(
|
|
476
|
+
return new Text(
|
|
477
|
+
`${theme.fg("toolTitle", theme.bold(presentationTitle("localize_context")))} ${theme.fg("accent", typeof args.query === "string" ? `"${args.query}"` : "")}`,
|
|
478
|
+
0,
|
|
479
|
+
0,
|
|
480
|
+
);
|
|
475
481
|
},
|
|
476
482
|
renderResult(result, { isPartial }, theme, context) {
|
|
477
483
|
if (isPartial) return new Text(theme.fg("warning", "Localizing..."), 0, 0);
|
|
@@ -482,8 +488,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
482
488
|
.join("\n");
|
|
483
489
|
return new Text(theme.fg("error", errorText || "localize_context failed"), 0, 0);
|
|
484
490
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
491
|
+
const contentText = result.content
|
|
492
|
+
.filter((block) => block.type === "text")
|
|
493
|
+
.map((block) => block.text)
|
|
494
|
+
.join("\n");
|
|
495
|
+
return new Text(contentText || "Localization complete", 0, 0);
|
|
487
496
|
},
|
|
488
497
|
});
|
|
489
498
|
|
|
@@ -1195,7 +1204,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1195
1204
|
const toPath = typeof args.toPath === "string" ? args.toPath : "";
|
|
1196
1205
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1197
1206
|
text.setText(
|
|
1198
|
-
`${theme.fg("toolTitle", theme.bold("reference_based_rename"))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
1207
|
+
`${theme.fg("toolTitle", theme.bold(presentationTitle("reference_based_rename")))} ${theme.fg("accent", fromPath)} ${theme.fg("dim", "->")} ${theme.fg("accent", toPath)}`,
|
|
1199
1208
|
);
|
|
1200
1209
|
return text;
|
|
1201
1210
|
},
|
|
@@ -1260,7 +1269,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1260
1269
|
const action = typeof args.action === "string" ? args.action : "";
|
|
1261
1270
|
const path = typeof args.path === "string" ? args.path : "";
|
|
1262
1271
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1263
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("rename"
|
|
1272
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("rename", action)))} ${theme.fg("accent", path)}`);
|
|
1264
1273
|
return text;
|
|
1265
1274
|
},
|
|
1266
1275
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1441,10 +1450,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1441
1450
|
? ` ${args.rootId}`
|
|
1442
1451
|
: "";
|
|
1443
1452
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1444
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("symbol_annotations"
|
|
1453
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("symbol_annotations", action)))}${theme.fg("accent", id)}`);
|
|
1445
1454
|
return text;
|
|
1446
1455
|
},
|
|
1447
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
1456
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
1448
1457
|
if (isPartial) return new Text(theme.fg("warning", "Working on annotation..."), 0, 0);
|
|
1449
1458
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1450
1459
|
if (context.isError) {
|
|
@@ -1457,11 +1466,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1457
1466
|
}
|
|
1458
1467
|
const details = result.details as SymbolAnnotationToolDetails | undefined;
|
|
1459
1468
|
if (details?.annotations) {
|
|
1460
|
-
text.setText(formatAnnotationListSummary(details.annotations, theme));
|
|
1469
|
+
text.setText(expanded ? details.annotations.map(formatAnnotationDetail).join("\n\n") : formatAnnotationListSummary(details.annotations, theme));
|
|
1461
1470
|
return text;
|
|
1462
1471
|
}
|
|
1463
1472
|
if (details?.annotation) {
|
|
1464
|
-
text.setText(formatAnnotationSummary(details.annotation, theme));
|
|
1473
|
+
text.setText(expanded ? formatAnnotationDetail(details.annotation) : formatAnnotationSummary(details.annotation, theme));
|
|
1465
1474
|
return text;
|
|
1466
1475
|
}
|
|
1467
1476
|
if (details?.scrubbed !== undefined) {
|
|
@@ -1633,7 +1642,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1633
1642
|
if (params.action === "job_status") {
|
|
1634
1643
|
if (!params.jobId) throw new Error("workspace_cache action=job_status requires jobId");
|
|
1635
1644
|
const job = await cacheOperations.jobStatus(params.jobId);
|
|
1636
|
-
return {
|
|
1645
|
+
return {
|
|
1646
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "job_status"), job) }],
|
|
1647
|
+
details: { action: "job_status", job },
|
|
1648
|
+
};
|
|
1637
1649
|
}
|
|
1638
1650
|
if (params.action === "wait") {
|
|
1639
1651
|
if (!params.jobId) throw new Error("workspace_cache action=wait requires jobId");
|
|
@@ -1655,7 +1667,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
1655
1667
|
);
|
|
1656
1668
|
}
|
|
1657
1669
|
const job = outcome.kind === "terminal" ? outcome.job : await cacheOperations.jobStatus(params.jobId);
|
|
1658
|
-
return {
|
|
1670
|
+
return {
|
|
1671
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "wait"), job) }],
|
|
1672
|
+
details: { action: "wait", job },
|
|
1673
|
+
};
|
|
1659
1674
|
}
|
|
1660
1675
|
if (!params.directory) throw new Error(`workspace_cache action=${params.action} requires directory`);
|
|
1661
1676
|
const directory = resolve(cwd, params.directory);
|
|
@@ -1663,10 +1678,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
1663
1678
|
const maxSymbolsPerFile = params.maxSymbolsPerFile ?? 100;
|
|
1664
1679
|
if (params.action === "status") {
|
|
1665
1680
|
const status = await cacheOperations.status(directory, maxFiles, maxSymbolsPerFile);
|
|
1666
|
-
return {
|
|
1681
|
+
return {
|
|
1682
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "status"), status) }],
|
|
1683
|
+
details: { action: "status", status },
|
|
1684
|
+
};
|
|
1667
1685
|
}
|
|
1668
1686
|
const job = await cacheOperations.submit(directory, maxFiles, maxSymbolsPerFile, params.waitMs ?? 3_000);
|
|
1669
|
-
return {
|
|
1687
|
+
return {
|
|
1688
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("workspace_cache", "populate"), job) }],
|
|
1689
|
+
details: { action: "populate", job },
|
|
1690
|
+
};
|
|
1670
1691
|
},
|
|
1671
1692
|
renderCall(args, theme, context) {
|
|
1672
1693
|
const action = args.action === "populate" || args.action === "wait" || args.action === "job_status" ? args.action : "status";
|
|
@@ -1764,7 +1785,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1764
1785
|
if (params.action === "status") {
|
|
1765
1786
|
const summary = await gitOperations.status(directory, vehicleCall);
|
|
1766
1787
|
const details: GitToolDetails = { action: "status", summary };
|
|
1767
|
-
return { content: [{ type: "text", text:
|
|
1788
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "status"), summary) }], details };
|
|
1768
1789
|
}
|
|
1769
1790
|
if (params.action === "log") {
|
|
1770
1791
|
if (params.maxCount === undefined) throw new Error("git action=log requires maxCount");
|
|
@@ -1785,18 +1806,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1785
1806
|
if (params.maxBytes === undefined) throw new Error("git action=compare-symbol requires maxBytes");
|
|
1786
1807
|
const comparison = await gitOperations.compareSymbol(directory, params.path, params.symbol, params.fromRef, params.toRef, params.maxBytes);
|
|
1787
1808
|
const details: GitToolDetails = { action: "compare-symbol", comparison };
|
|
1788
|
-
return { content: [{ type: "text", text:
|
|
1809
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "compare-symbol"), comparison) }], details };
|
|
1789
1810
|
}
|
|
1790
1811
|
if (params.action === "worktree-add") {
|
|
1791
1812
|
if (!params.ref) throw new Error("git action=worktree-add requires ref");
|
|
1792
1813
|
const worktreeAdd = await gitOperations.worktreeAdd(directory, params.ref, params.forceRefresh, vehicleCall);
|
|
1793
1814
|
const details: GitToolDetails = { action: "worktree-add", worktreeAdd };
|
|
1794
|
-
return { content: [{ type: "text", text:
|
|
1815
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-add"), worktreeAdd) }], details };
|
|
1795
1816
|
}
|
|
1796
1817
|
if (params.action === "worktree-remove") {
|
|
1797
1818
|
const worktreeRemove = await gitOperations.worktreeRemove(directory, vehicleCall);
|
|
1798
1819
|
const details: GitToolDetails = { action: "worktree-remove", worktreeRemove };
|
|
1799
|
-
return { content: [{ type: "text", text:
|
|
1820
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "worktree-remove"), worktreeRemove) }], details };
|
|
1800
1821
|
}
|
|
1801
1822
|
if (params.action === "show") {
|
|
1802
1823
|
if (!params.ref || !params.path) throw new Error("git action=show requires ref and path");
|
|
@@ -1809,7 +1830,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1809
1830
|
if (params.maxMatches === undefined || params.maxBytes === undefined) throw new Error("git action=grep-ref requires maxMatches and maxBytes");
|
|
1810
1831
|
const grep = await gitOperations.grep(directory, params.ref, params.pattern, params.pathspecs, params.maxMatches, params.maxBytes, vehicleCall);
|
|
1811
1832
|
const details: GitToolDetails = { action: "grep-ref", grep };
|
|
1812
|
-
return { content: [{ type: "text", text:
|
|
1833
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-ref"), grep) }], details };
|
|
1813
1834
|
}
|
|
1814
1835
|
if (params.action === "grep-history") {
|
|
1815
1836
|
if (!params.pattern) throw new Error("git action=grep-history requires pattern");
|
|
@@ -1835,20 +1856,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1835
1856
|
vehicleCall,
|
|
1836
1857
|
);
|
|
1837
1858
|
const details: GitToolDetails = { action: "grep-history", historyGrep };
|
|
1838
|
-
return { content: [{ type: "text", text:
|
|
1859
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "grep-history"), historyGrep) }], details };
|
|
1839
1860
|
}
|
|
1840
1861
|
if (params.action === "ls-ref") {
|
|
1841
1862
|
if (!params.ref) throw new Error("git action=ls-ref requires ref");
|
|
1842
1863
|
if (params.maxResults === undefined) throw new Error("git action=ls-ref requires maxResults");
|
|
1843
1864
|
const listFiles = await gitOperations.listFiles(directory, params.ref, params.pathspecs, params.maxResults, vehicleCall);
|
|
1844
1865
|
const details: GitToolDetails = { action: "ls-ref", listFiles };
|
|
1845
|
-
return { content: [{ type: "text", text:
|
|
1866
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "ls-ref"), listFiles) }], details };
|
|
1846
1867
|
}
|
|
1847
1868
|
if (params.action === "is-ancestor") {
|
|
1848
1869
|
if (!params.ancestorRef || !params.ref) throw new Error("git action=is-ancestor requires ancestorRef and ref");
|
|
1849
1870
|
const result = await gitOperations.isAncestor(directory, params.ancestorRef, params.ref, vehicleCall);
|
|
1850
1871
|
const details: GitToolDetails = { action: "is-ancestor", isAncestor: { ancestorRef: params.ancestorRef, ref: params.ref, result } };
|
|
1851
|
-
return { content: [{ type: "text", text:
|
|
1872
|
+
return { content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("git", "is-ancestor"), { isAncestor: result }) }], details };
|
|
1852
1873
|
}
|
|
1853
1874
|
throw new Error(`unknown git action: ${String(params.action)}`);
|
|
1854
1875
|
},
|
|
@@ -2134,7 +2155,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2134
2155
|
const action = typeof args.action === "string" ? args.action : "";
|
|
2135
2156
|
const path = typeof args.path === "string" ? args.path : "";
|
|
2136
2157
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2137
|
-
text.setText(`${theme.fg("toolTitle", theme.bold("mutation_history"
|
|
2158
|
+
text.setText(`${theme.fg("toolTitle", theme.bold(presentationTitle("mutation_history", action)))} ${theme.fg("accent", path)}`);
|
|
2138
2159
|
return text;
|
|
2139
2160
|
},
|
|
2140
2161
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -2210,7 +2231,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2210
2231
|
maxResults: params.maxResults,
|
|
2211
2232
|
cursor: params.cursor,
|
|
2212
2233
|
});
|
|
2213
|
-
return {
|
|
2234
|
+
return {
|
|
2235
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "list"), page) }],
|
|
2236
|
+
details: { action: "list", page },
|
|
2237
|
+
};
|
|
2214
2238
|
}
|
|
2215
2239
|
if (params.action === "remove") {
|
|
2216
2240
|
if (!params.name || !params.resolvedVersion) throw new Error("package_source action=remove requires ecosystem, name, and resolvedVersion");
|
|
@@ -2220,11 +2244,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
2220
2244
|
params.name,
|
|
2221
2245
|
params.resolvedVersion,
|
|
2222
2246
|
);
|
|
2223
|
-
return {
|
|
2247
|
+
return {
|
|
2248
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "remove"), result) }],
|
|
2249
|
+
details: { action: "remove", result },
|
|
2250
|
+
};
|
|
2224
2251
|
}
|
|
2225
2252
|
if (params.action === "clean") {
|
|
2226
2253
|
const result = await packageSourceOperations.clean(optionalPackageEcosystem(params.ecosystem));
|
|
2227
|
-
return {
|
|
2254
|
+
return {
|
|
2255
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "clean"), result) }],
|
|
2256
|
+
details: { action: "clean", result },
|
|
2257
|
+
};
|
|
2228
2258
|
}
|
|
2229
2259
|
if (!params.directory || !params.name) throw new Error("package_source action=resolve requires directory and name");
|
|
2230
2260
|
const directory = resolve(cwd, params.directory);
|
|
@@ -2235,7 +2265,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2235
2265
|
params.registry ?? null,
|
|
2236
2266
|
optionalPackageEcosystem(params.ecosystem),
|
|
2237
2267
|
);
|
|
2238
|
-
return {
|
|
2268
|
+
return {
|
|
2269
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("package_source", "resolve"), result) }],
|
|
2270
|
+
details: { action: "resolve", result },
|
|
2271
|
+
};
|
|
2239
2272
|
},
|
|
2240
2273
|
renderCall(args, theme, context) {
|
|
2241
2274
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
@@ -2332,12 +2365,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
2332
2365
|
params.forceRefresh,
|
|
2333
2366
|
vehicleCall,
|
|
2334
2367
|
);
|
|
2335
|
-
return {
|
|
2368
|
+
return {
|
|
2369
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "fetch"), result) }],
|
|
2370
|
+
details: { action: "fetch", result },
|
|
2371
|
+
};
|
|
2336
2372
|
}
|
|
2337
2373
|
if (params.action === "evict") {
|
|
2338
2374
|
if (!params.owner || !params.repo) throw new Error("repo_cache action=evict requires owner and repo");
|
|
2339
2375
|
const result = await repoCacheEvictOperations.evict(params.host ?? "github.com", params.owner, params.repo, params.ref ?? null, vehicleCall);
|
|
2340
|
-
return {
|
|
2376
|
+
return {
|
|
2377
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "evict"), result) }],
|
|
2378
|
+
details: { action: "evict", result },
|
|
2379
|
+
};
|
|
2341
2380
|
}
|
|
2342
2381
|
if (params.maxResults === undefined) throw new Error("repo_cache action=list requires maxResults");
|
|
2343
2382
|
const page = await repoCacheListOperations.list(
|
|
@@ -2346,7 +2385,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
2346
2385
|
params.cursor,
|
|
2347
2386
|
vehicleCall,
|
|
2348
2387
|
);
|
|
2349
|
-
return {
|
|
2388
|
+
return {
|
|
2389
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("repo_cache", "list"), page) }],
|
|
2390
|
+
details: { action: "list", page },
|
|
2391
|
+
};
|
|
2350
2392
|
},
|
|
2351
2393
|
renderCall(args, theme, context) {
|
|
2352
2394
|
const action = args.action === "list" || args.action === "evict" ? args.action : "fetch";
|
|
@@ -2419,14 +2461,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
2419
2461
|
};
|
|
2420
2462
|
if (params.action === "github_repos") {
|
|
2421
2463
|
const result = await externalSearchOperations.githubRepos(params.query, maxResults, vehicleCall);
|
|
2422
|
-
return {
|
|
2464
|
+
return {
|
|
2465
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "github_repos"), result) }],
|
|
2466
|
+
details: { action: "github_repos", result },
|
|
2467
|
+
};
|
|
2423
2468
|
}
|
|
2424
2469
|
if (params.action === "npm_packages") {
|
|
2425
2470
|
const result = await externalSearchOperations.npmPackages(params.query, maxResults, vehicleCall);
|
|
2426
|
-
return {
|
|
2471
|
+
return {
|
|
2472
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "npm_packages"), result) }],
|
|
2473
|
+
details: { action: "npm_packages", result },
|
|
2474
|
+
};
|
|
2427
2475
|
}
|
|
2428
2476
|
const result = await externalSearchOperations.sourcegraphCode(params.query, maxResults, vehicleCall);
|
|
2429
|
-
return {
|
|
2477
|
+
return {
|
|
2478
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("external_search", "sourcegraph_code"), result) }],
|
|
2479
|
+
details: { action: "sourcegraph_code", result },
|
|
2480
|
+
};
|
|
2430
2481
|
},
|
|
2431
2482
|
renderCall(args, theme, context) {
|
|
2432
2483
|
const action = args.action === "npm_packages" || args.action === "sourcegraph_code" ? args.action : "github_repos";
|
|
@@ -2434,7 +2485,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
2434
2485
|
text.setText(formatExternalSearchCall(action, args, theme));
|
|
2435
2486
|
return text;
|
|
2436
2487
|
},
|
|
2437
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
2488
|
+
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
2438
2489
|
if (isPartial) return new Text(theme.fg("warning", "Searching..."), 0, 0);
|
|
2439
2490
|
if (context.isError) {
|
|
2440
2491
|
const errorText = result.content
|
|
@@ -2445,9 +2496,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2445
2496
|
}
|
|
2446
2497
|
const details = result.details as ExternalSearchToolDetails | undefined;
|
|
2447
2498
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2448
|
-
if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, theme));
|
|
2449
|
-
else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, theme));
|
|
2450
|
-
else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, theme));
|
|
2499
|
+
if (details?.action === "npm_packages") text.setText(formatNpmPackageSearchResult(details.result, expanded, theme));
|
|
2500
|
+
else if (details?.action === "sourcegraph_code") text.setText(formatSourcegraphCodeSearchResult(details.result, expanded, theme));
|
|
2501
|
+
else text.setText(formatGithubRepoSearchResult(details?.action === "github_repos" ? details.result : undefined, expanded, theme));
|
|
2451
2502
|
return text;
|
|
2452
2503
|
},
|
|
2453
2504
|
});
|
|
@@ -2470,11 +2521,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2470
2521
|
async execute(_toolCallId, params) {
|
|
2471
2522
|
const directories = params.directories.map((directory) => resolve(cwd, directory));
|
|
2472
2523
|
const results = await crossWorkspaceSearchOperations.findSymbols(params.query, directories, params.timeoutMs, params.maxResults);
|
|
2473
|
-
return {
|
|
2524
|
+
return {
|
|
2525
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("find_symbols_across_projects"), results) }],
|
|
2526
|
+
details: { results },
|
|
2527
|
+
};
|
|
2474
2528
|
},
|
|
2475
2529
|
renderCall(args, theme, context) {
|
|
2476
2530
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2477
|
-
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
2531
|
+
text.setText(formatCrossWorkspaceCall("find_symbols_across_projects", args, theme));
|
|
2478
2532
|
return text;
|
|
2479
2533
|
},
|
|
2480
2534
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -2509,11 +2563,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
2509
2563
|
async execute(_toolCallId, params) {
|
|
2510
2564
|
const directories = params.directories.map((directory) => resolve(cwd, directory));
|
|
2511
2565
|
const results = await crossWorkspaceSearchOperations.searchText(params.query, directories, params.maxMatches, params.maxBytes, params.timeoutMs);
|
|
2512
|
-
return {
|
|
2566
|
+
return {
|
|
2567
|
+
content: [{ type: "text", text: formatSemanticModelContent(presentationTitle("search_code_across_projects"), results) }],
|
|
2568
|
+
details: { results },
|
|
2569
|
+
};
|
|
2513
2570
|
},
|
|
2514
2571
|
renderCall(args, theme, context) {
|
|
2515
2572
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
2516
|
-
text.setText(formatCrossWorkspaceCall(args, theme));
|
|
2573
|
+
text.setText(formatCrossWorkspaceCall("search_code_across_projects", args, theme));
|
|
2517
2574
|
return text;
|
|
2518
2575
|
},
|
|
2519
2576
|
renderResult(result, { expanded, isPartial }, theme, context) {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { LineEditOutcome } 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
|
export function formatLineEditCall(args: { path?: unknown; edits?: unknown }, theme: LectorTheme): string {
|
|
5
6
|
const path = typeof args.path === "string" ? args.path : "";
|
|
6
7
|
const count = Array.isArray(args.edits) ? args.edits.length : 0;
|
|
7
|
-
return `${theme.fg("toolTitle", theme.bold("line_edit"))} ${theme.fg("accent", path)} ${theme.fg("dim", `(${count} edit${count === 1 ? "" : "s"})`)}`;
|
|
8
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("line_edit")))} ${theme.fg("accent", path)} ${theme.fg("dim", `(${count} edit${count === 1 ? "" : "s"})`)}`;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function formatLineEditResult(result: LineEditOutcome | undefined, theme: LectorTheme): string {
|
|
@@ -2,6 +2,7 @@ import type { PackageSourceListEntry, PackageSourceOperationResult } from "@dany
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList, type TableColumn } 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_CANDIDATES = 5;
|
|
7
8
|
|
|
@@ -14,18 +15,18 @@ export function formatPackageSourceCall(
|
|
|
14
15
|
args: { action?: unknown; directory?: unknown; name?: unknown; version?: unknown; ecosystem?: unknown; resolvedVersion?: unknown; text?: unknown },
|
|
15
16
|
theme: LectorTheme,
|
|
16
17
|
): string {
|
|
17
|
-
const label = theme.fg("toolTitle", theme.bold("package_source"));
|
|
18
18
|
const action: PackageSourceAction = args.action === "list" || args.action === "remove" || args.action === "clean" ? args.action : "resolve";
|
|
19
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("package_source", action)));
|
|
19
20
|
if (action === "list") {
|
|
20
21
|
const text = typeof args.text === "string" && args.text.length > 0 ? ` ${theme.fg("dim", args.text)}` : "";
|
|
21
|
-
return `${label}
|
|
22
|
+
return `${label}${text}`;
|
|
22
23
|
}
|
|
23
24
|
if (action === "remove" || action === "clean") {
|
|
24
25
|
const name = typeof args.name === "string" ? args.name : "";
|
|
25
26
|
const version = typeof args.resolvedVersion === "string" ? `@${args.resolvedVersion}` : "";
|
|
26
27
|
const ecosystem = typeof args.ecosystem === "string" ? args.ecosystem : "";
|
|
27
28
|
const identity = name ? `${name}${version}` : ecosystem;
|
|
28
|
-
return `${label}
|
|
29
|
+
return `${label}${identity ? ` ${theme.fg("accent", identity)}` : ""}`;
|
|
29
30
|
}
|
|
30
31
|
const name = typeof args.name === "string" ? args.name : "";
|
|
31
32
|
const version = typeof args.version === "string" ? `@${args.version}` : "";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const DEFAULT_MODEL_CONTENT_BYTES = 32_768;
|
|
2
|
+
const MAX_COLLECTION_ENTRIES = 24;
|
|
3
|
+
const MAX_DEPTH = 4;
|
|
4
|
+
|
|
5
|
+
function scalarText(value: unknown): string | undefined {
|
|
6
|
+
if (value === null) return "none";
|
|
7
|
+
if (typeof value === "string") return value;
|
|
8
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendSemanticLines(lines: string[], value: unknown, path: string, depth: number): void {
|
|
13
|
+
const scalar = scalarText(value);
|
|
14
|
+
if (scalar !== undefined) {
|
|
15
|
+
lines.push(`${path}: ${scalar}`);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (depth >= MAX_DEPTH) {
|
|
19
|
+
lines.push(`${path}: [nested value omitted]`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
lines.push(`${path} (${value.length})`);
|
|
24
|
+
for (const [index, entry] of value.slice(0, MAX_COLLECTION_ENTRIES).entries()) appendSemanticLines(lines, entry, `${path}[${index}]`, depth + 1);
|
|
25
|
+
if (value.length > MAX_COLLECTION_ENTRIES) lines.push(`${path}: ${value.length - MAX_COLLECTION_ENTRIES} more entries omitted`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === "object" && value !== null) {
|
|
29
|
+
const entries = Object.entries(value).slice(0, MAX_COLLECTION_ENTRIES);
|
|
30
|
+
if (entries.length === 0) lines.push(`${path}: none`);
|
|
31
|
+
for (const [key, entry] of entries) appendSemanticLines(lines, entry, path ? `${path}.${key}` : key, depth + 1);
|
|
32
|
+
if (Object.keys(value).length > MAX_COLLECTION_ENTRIES) lines.push(`${path || "result"}: additional fields omitted`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
lines.push(`${path}: unavailable`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Bounds UTF-8 model-facing text independently from presentation details. */
|
|
39
|
+
export function boundModelContentText(full: string, maxBytes = DEFAULT_MODEL_CONTENT_BYTES): string {
|
|
40
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 64) throw new Error("model content maxBytes must be an integer of at least 64");
|
|
41
|
+
if (Buffer.byteLength(full, "utf8") <= maxBytes) return full;
|
|
42
|
+
const suffix = "\n[model content truncated]";
|
|
43
|
+
const budget = maxBytes - Buffer.byteLength(suffix, "utf8");
|
|
44
|
+
let bytes = Buffer.from(full, "utf8").subarray(0, Math.max(0, budget));
|
|
45
|
+
let prefix = "";
|
|
46
|
+
while (bytes.length > 0) {
|
|
47
|
+
try {
|
|
48
|
+
prefix = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
49
|
+
break;
|
|
50
|
+
} catch {
|
|
51
|
+
bytes = bytes.subarray(0, -1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `${prefix}${suffix}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Formats an operation outcome as bounded, semantic plain text for model consumption. */
|
|
58
|
+
export function formatSemanticModelContent(title: string, value: unknown, maxBytes = DEFAULT_MODEL_CONTENT_BYTES): string {
|
|
59
|
+
const lines = [title];
|
|
60
|
+
appendSemanticLines(lines, value, "", 0);
|
|
61
|
+
return boundModelContentText(lines.join("\n"), maxBytes);
|
|
62
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { JsonValue } from "@danypops/vehicle-core";
|
|
2
|
+
import type { AgentToolResult, AgentToolUpdateCallback, Theme, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import type { TSchema } from "typebox";
|
|
5
|
+
import { boundModelContentText, DEFAULT_MODEL_CONTENT_BYTES } from "./model-content.ts";
|
|
6
|
+
import { type PresentationFamily, presentationFamily } from "./tool-presentation.ts";
|
|
7
|
+
|
|
8
|
+
export const LECTOR_PRESENTATION_SCHEMA = "pi-lector.presentation/v1";
|
|
9
|
+
export const DEFAULT_LECTOR_PRESENTATION_MAX_BYTES = 128 * 1024;
|
|
10
|
+
|
|
11
|
+
interface LectorPresentationBase {
|
|
12
|
+
readonly schema: typeof LECTOR_PRESENTATION_SCHEMA;
|
|
13
|
+
readonly tool: string;
|
|
14
|
+
readonly action: string | null;
|
|
15
|
+
readonly payload: JsonValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Versioned presentation variants persisted independently from model-facing content. */
|
|
19
|
+
export type LectorToolPresentation = {
|
|
20
|
+
readonly [Family in PresentationFamily]: LectorPresentationBase & { readonly family: Family };
|
|
21
|
+
}[PresentationFamily];
|
|
22
|
+
|
|
23
|
+
export type LectorPresentationEnvelope = LectorToolPresentation;
|
|
24
|
+
|
|
25
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
30
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
31
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
32
|
+
if (Array.isArray(value)) return value.every(isJsonValue);
|
|
33
|
+
if (isRecord(value)) return Object.values(value).every(isJsonValue);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function serializedJsonValue(value: unknown): JsonValue {
|
|
38
|
+
if (value === undefined) return null;
|
|
39
|
+
let serialized: string;
|
|
40
|
+
try {
|
|
41
|
+
serialized = JSON.stringify(value, (_key, candidate: unknown) => {
|
|
42
|
+
if (typeof candidate === "number" && !Number.isFinite(candidate)) throw new TypeError("non-finite number");
|
|
43
|
+
if (typeof candidate === "bigint" || typeof candidate === "function" || typeof candidate === "symbol") {
|
|
44
|
+
throw new TypeError(`unsupported ${typeof candidate}`);
|
|
45
|
+
}
|
|
46
|
+
return candidate;
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new TypeError(`presentation details must be JSON serializable: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
}
|
|
51
|
+
const parsed: unknown = JSON.parse(serialized);
|
|
52
|
+
if (!isJsonValue(parsed)) throw new TypeError("presentation details must contain only JSON values");
|
|
53
|
+
return parsed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Projects one tool's renderer details into the versioned, serializable session boundary. */
|
|
57
|
+
export function projectLectorPresentation(
|
|
58
|
+
tool: string,
|
|
59
|
+
details: unknown,
|
|
60
|
+
maxBytes = DEFAULT_LECTOR_PRESENTATION_MAX_BYTES,
|
|
61
|
+
actionOverride?: string,
|
|
62
|
+
): LectorPresentationEnvelope {
|
|
63
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) throw new TypeError("presentation maxBytes must be a positive integer");
|
|
64
|
+
const action = actionOverride ?? (isRecord(details) && typeof details.action === "string" ? details.action : null);
|
|
65
|
+
const envelope: LectorPresentationEnvelope = {
|
|
66
|
+
schema: LECTOR_PRESENTATION_SCHEMA,
|
|
67
|
+
tool,
|
|
68
|
+
action,
|
|
69
|
+
family: presentationFamily(tool, action ?? undefined),
|
|
70
|
+
payload: serializedJsonValue(details),
|
|
71
|
+
};
|
|
72
|
+
const bytes = Buffer.byteLength(JSON.stringify(envelope), "utf8");
|
|
73
|
+
if (bytes > maxBytes) throw new RangeError(`presentation details exceed ${maxBytes} bytes (${bytes} observed)`);
|
|
74
|
+
return envelope;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Validates the shared envelope. Domain renderers retain responsibility for validating their payload variant. */
|
|
78
|
+
export function parseLectorPresentation(details: unknown, expectedTool: string): JsonValue | undefined {
|
|
79
|
+
if (!isRecord(details)) return undefined;
|
|
80
|
+
if (details.schema !== LECTOR_PRESENTATION_SCHEMA || details.tool !== expectedTool || !("payload" in details)) return undefined;
|
|
81
|
+
if (details.action !== null && typeof details.action !== "string") return undefined;
|
|
82
|
+
if (details.family !== presentationFamily(expectedTool, typeof details.action === "string" ? details.action : undefined)) return undefined;
|
|
83
|
+
try {
|
|
84
|
+
return serializedJsonValue(details.payload);
|
|
85
|
+
} catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fallbackText(result: AgentToolResult<unknown>, theme: Theme): Text {
|
|
91
|
+
const content = result.content
|
|
92
|
+
.filter((block): block is Extract<(typeof result.content)[number], { type: "text" }> => block.type === "text")
|
|
93
|
+
.map((block) => block.text)
|
|
94
|
+
.join("\n");
|
|
95
|
+
return new Text(theme.fg("toolOutput", content || "No result."), 0, 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface LectorPresentationOptions {
|
|
99
|
+
readonly maxBytes?: number;
|
|
100
|
+
readonly maxModelContentBytes?: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Wraps a production tool so persisted renderer details cross one validated, bounded envelope. */
|
|
104
|
+
export function withLectorPresentation<TParams extends TSchema, TDetails, TState>(
|
|
105
|
+
tool: ToolDefinition<TParams, TDetails, TState>,
|
|
106
|
+
options: LectorPresentationOptions = {},
|
|
107
|
+
): ToolDefinition<TParams, LectorPresentationEnvelope, TState> {
|
|
108
|
+
const maxBytes = options.maxBytes ?? DEFAULT_LECTOR_PRESENTATION_MAX_BYTES;
|
|
109
|
+
const maxModelContentBytes = options.maxModelContentBytes ?? DEFAULT_MODEL_CONTENT_BYTES;
|
|
110
|
+
return {
|
|
111
|
+
...tool,
|
|
112
|
+
async execute(toolCallId, params, signal, onUpdate, context) {
|
|
113
|
+
const parameterRecord = params as Record<string, unknown>;
|
|
114
|
+
const action =
|
|
115
|
+
typeof parameterRecord.action === "string"
|
|
116
|
+
? parameterRecord.action
|
|
117
|
+
: typeof parameterRecord.direction === "string"
|
|
118
|
+
? parameterRecord.direction
|
|
119
|
+
: undefined;
|
|
120
|
+
const boundedContent = (content: AgentToolResult<unknown>["content"]): AgentToolResult<unknown>["content"] =>
|
|
121
|
+
content.map((block) => (block.type === "text" ? { ...block, text: boundModelContentText(block.text, maxModelContentBytes) } : block));
|
|
122
|
+
const wrappedUpdate: AgentToolUpdateCallback<TDetails> | undefined = onUpdate
|
|
123
|
+
? (update) =>
|
|
124
|
+
onUpdate({
|
|
125
|
+
...update,
|
|
126
|
+
content: boundedContent(update.content),
|
|
127
|
+
details: projectLectorPresentation(tool.name, update.details, maxBytes, action),
|
|
128
|
+
})
|
|
129
|
+
: undefined;
|
|
130
|
+
const result = await tool.execute(toolCallId, params, signal, wrappedUpdate, context);
|
|
131
|
+
return {
|
|
132
|
+
...result,
|
|
133
|
+
content: boundedContent(result.content),
|
|
134
|
+
details: projectLectorPresentation(tool.name, result.details, maxBytes, action),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
renderResult(result, renderOptions, theme, context) {
|
|
138
|
+
const payload = parseLectorPresentation(result.details, tool.name);
|
|
139
|
+
if (payload === undefined && !renderOptions.isPartial && !context.isError) return fallbackText(result, theme);
|
|
140
|
+
if (!tool.renderResult) return fallbackText(result, theme);
|
|
141
|
+
// The envelope is the runtime validation boundary. Each existing domain renderer narrows
|
|
142
|
+
// its own payload variant; this is the single shared assertion that reconnects its generic.
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
144
|
+
const domainDetails = payload as TDetails;
|
|
145
|
+
return tool.renderResult({ ...result, details: domainDetails }, renderOptions, theme, context);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
export type PresentationFamily =
|
|
2
|
+
| "source"
|
|
3
|
+
| "markdown"
|
|
4
|
+
| "symbols"
|
|
5
|
+
| "locations"
|
|
6
|
+
| "diagnostics"
|
|
7
|
+
| "diff"
|
|
8
|
+
| "mutation"
|
|
9
|
+
| "status"
|
|
10
|
+
| "table"
|
|
11
|
+
| "tree"
|
|
12
|
+
| "candidates"
|
|
13
|
+
| "semantic-text";
|
|
14
|
+
|
|
15
|
+
interface PresentationPathSpec {
|
|
16
|
+
readonly title: string;
|
|
17
|
+
readonly family: PresentationFamily;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ToolPresentationSpec extends PresentationPathSpec {
|
|
21
|
+
readonly actions?: Readonly<Record<string, PresentationPathSpec>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const LECTOR_TOOL_PRESENTATION_SPECS: Readonly<Record<string, ToolPresentationSpec>> = {
|
|
25
|
+
read: { title: "Read File", family: "source" },
|
|
26
|
+
write: { title: "Write File", family: "mutation" },
|
|
27
|
+
edit: { title: "Edit File", family: "diff" },
|
|
28
|
+
find_symbols: { title: "Find Symbols", family: "symbols" },
|
|
29
|
+
localize_context: { title: "Localize Context", family: "candidates" },
|
|
30
|
+
go_to_definition: { title: "Go to Definition", family: "locations" },
|
|
31
|
+
go_to_implementation: { title: "Go to Implementation", family: "locations" },
|
|
32
|
+
find_references: { title: "Find References", family: "locations" },
|
|
33
|
+
hover: { title: "Hover", family: "markdown" },
|
|
34
|
+
document_symbols: { title: "Document Symbols", family: "tree" },
|
|
35
|
+
diagnostics: { title: "Diagnostics", family: "diagnostics" },
|
|
36
|
+
code_action_preview: { title: "Preview Code Actions", family: "candidates" },
|
|
37
|
+
code_action_apply: { title: "Apply Code Action", family: "mutation" },
|
|
38
|
+
diagnostic_delta: { title: "Diagnostic Delta", family: "diagnostics" },
|
|
39
|
+
call_hierarchy: {
|
|
40
|
+
title: "Call Hierarchy",
|
|
41
|
+
family: "tree",
|
|
42
|
+
actions: {
|
|
43
|
+
prepare: { title: "Prepare Call Hierarchy", family: "symbols" },
|
|
44
|
+
incoming: { title: "Incoming Calls", family: "tree" },
|
|
45
|
+
outgoing: { title: "Outgoing Calls", family: "tree" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
type_hierarchy: {
|
|
49
|
+
title: "Type Hierarchy",
|
|
50
|
+
family: "tree",
|
|
51
|
+
actions: {
|
|
52
|
+
prepare: { title: "Prepare Type Hierarchy", family: "symbols" },
|
|
53
|
+
supertypes: { title: "Supertypes", family: "tree" },
|
|
54
|
+
subtypes: { title: "Subtypes", family: "tree" },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
impact_analysis: { title: "Impact Analysis", family: "tree" },
|
|
58
|
+
reference_based_rename: { title: "Rename File by References", family: "mutation" },
|
|
59
|
+
rename: {
|
|
60
|
+
title: "Rename Symbol",
|
|
61
|
+
family: "mutation",
|
|
62
|
+
actions: {
|
|
63
|
+
prepare: { title: "Prepare Rename", family: "semantic-text" },
|
|
64
|
+
apply: { title: "Rename Symbol", family: "mutation" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
symbol_annotations: {
|
|
68
|
+
title: "Symbol Annotations",
|
|
69
|
+
family: "semantic-text",
|
|
70
|
+
actions: {
|
|
71
|
+
create: { title: "Create Symbol Annotation", family: "mutation" },
|
|
72
|
+
get: { title: "Show Symbol Annotation", family: "markdown" },
|
|
73
|
+
list: { title: "List Symbol Annotations", family: "table" },
|
|
74
|
+
refresh: { title: "Refresh Symbol Annotation", family: "mutation" },
|
|
75
|
+
scrub: { title: "Scrub Symbol Annotation", family: "mutation" },
|
|
76
|
+
restore: { title: "Restore Symbol Annotation", family: "mutation" },
|
|
77
|
+
contain: { title: "Contain Symbol Annotation", family: "mutation" },
|
|
78
|
+
uncontain: { title: "Uncontain Symbol Annotation", family: "mutation" },
|
|
79
|
+
tree: { title: "Annotation Tree", family: "tree" },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
reachable_from: { title: "Reachable Symbols", family: "tree" },
|
|
83
|
+
workspace_map: { title: "Workspace Map", family: "symbols" },
|
|
84
|
+
workspace_cache: {
|
|
85
|
+
title: "Workspace Cache",
|
|
86
|
+
family: "status",
|
|
87
|
+
actions: {
|
|
88
|
+
status: { title: "Workspace Cache Status", family: "status" },
|
|
89
|
+
populate: { title: "Populate Workspace Cache", family: "status" },
|
|
90
|
+
wait: { title: "Wait for Cache Job", family: "status" },
|
|
91
|
+
job_status: { title: "Cache Job Status", family: "status" },
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
git: {
|
|
95
|
+
title: "Git",
|
|
96
|
+
family: "semantic-text",
|
|
97
|
+
actions: {
|
|
98
|
+
status: { title: "Git Status", family: "status" },
|
|
99
|
+
log: { title: "Git Log", family: "table" },
|
|
100
|
+
diff: { title: "Git Diff", family: "diff" },
|
|
101
|
+
"compare-symbol": { title: "Compare Symbol", family: "diff" },
|
|
102
|
+
show: { title: "Show File at Git Ref", family: "source" },
|
|
103
|
+
"grep-ref": { title: "Search Git Ref", family: "locations" },
|
|
104
|
+
"grep-history": { title: "Search Git History", family: "locations" },
|
|
105
|
+
"ls-ref": { title: "List Files at Git Ref", family: "table" },
|
|
106
|
+
"is-ancestor": { title: "Check Git Ancestry", family: "semantic-text" },
|
|
107
|
+
"worktree-add": { title: "Create Git Worktree", family: "mutation" },
|
|
108
|
+
"worktree-remove": { title: "Remove Git Worktree", family: "mutation" },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
search_code: { title: "Search Code", family: "locations" },
|
|
112
|
+
find_files: { title: "Find Files", family: "table" },
|
|
113
|
+
line_edit: { title: "Edit Lines", family: "diff" },
|
|
114
|
+
apply_patch: { title: "Apply Patch", family: "diff" },
|
|
115
|
+
mutation_history: {
|
|
116
|
+
title: "Mutation History",
|
|
117
|
+
family: "table",
|
|
118
|
+
actions: {
|
|
119
|
+
list: { title: "Mutation History", family: "table" },
|
|
120
|
+
revert: { title: "Revert Mutation", family: "mutation" },
|
|
121
|
+
"revert-transaction": { title: "Revert Transaction", family: "mutation" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
package_source: {
|
|
125
|
+
title: "Package Source",
|
|
126
|
+
family: "semantic-text",
|
|
127
|
+
actions: {
|
|
128
|
+
resolve: { title: "Resolve Package Source", family: "semantic-text" },
|
|
129
|
+
list: { title: "List Package Sources", family: "table" },
|
|
130
|
+
remove: { title: "Remove Package Source", family: "mutation" },
|
|
131
|
+
clean: { title: "Clean Package Sources", family: "mutation" },
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
repo_cache: {
|
|
135
|
+
title: "Repository Cache",
|
|
136
|
+
family: "semantic-text",
|
|
137
|
+
actions: {
|
|
138
|
+
fetch: { title: "Fetch Repository", family: "status" },
|
|
139
|
+
list: { title: "List Repository Cache", family: "table" },
|
|
140
|
+
evict: { title: "Evict Repository", family: "mutation" },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
external_search: {
|
|
144
|
+
title: "External Search",
|
|
145
|
+
family: "candidates",
|
|
146
|
+
actions: {
|
|
147
|
+
github_repos: { title: "Search GitHub Repositories", family: "candidates" },
|
|
148
|
+
npm_packages: { title: "Search npm Packages", family: "candidates" },
|
|
149
|
+
sourcegraph_code: { title: "Search Public Code", family: "candidates" },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
find_symbols_across_projects: { title: "Find Symbols Across Projects", family: "symbols" },
|
|
153
|
+
search_code_across_projects: { title: "Search Code Across Projects", family: "locations" },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function presentationTitle(toolName: string, action?: string): string {
|
|
157
|
+
const spec = LECTOR_TOOL_PRESENTATION_SPECS[toolName];
|
|
158
|
+
if (!spec) throw new Error(`no presentation specification for ${toolName}`);
|
|
159
|
+
return (action ? spec.actions?.[action]?.title : undefined) ?? spec.title;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function presentationFamily(toolName: string, action?: string): PresentationFamily {
|
|
163
|
+
const spec = LECTOR_TOOL_PRESENTATION_SPECS[toolName];
|
|
164
|
+
if (!spec) throw new Error(`no presentation specification for ${toolName}`);
|
|
165
|
+
return (action ? spec.actions?.[action]?.family : undefined) ?? spec.family;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function presentationPathCount(): number {
|
|
169
|
+
return Object.values(LECTOR_TOOL_PRESENTATION_SPECS).reduce((count, spec) => count + (spec.actions ? Object.keys(spec.actions).length : 1), 0);
|
|
170
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { CachedRepositoryEntry, CachedRepositoryPage, RepoFetchResult } fro
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { TableColumn } 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
|
/** Table has no row-count bound of its own; a cache can grow arbitrarily large even though repo_cache's own `maxResults` bounds any one page, so the display itself still needs a cap independent of that. */
|
|
7
8
|
export const REPO_CACHE_VISIBLE_ROWS = 20;
|
|
@@ -17,16 +18,16 @@ export function formatRepoCacheCall(
|
|
|
17
18
|
args: { owner?: unknown; repo?: unknown; ref?: unknown; host?: unknown; text?: unknown },
|
|
18
19
|
theme: LectorTheme,
|
|
19
20
|
): string {
|
|
20
|
-
const label = theme.fg("toolTitle", theme.bold("repo_cache"));
|
|
21
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("repo_cache", action)));
|
|
21
22
|
if (action === "list") {
|
|
22
23
|
const filter = typeof args.text === "string" && args.text.length > 0 ? args.text : typeof args.repo === "string" ? args.repo : "";
|
|
23
|
-
return `${label}
|
|
24
|
+
return `${label}${filter ? ` ${theme.fg("accent", filter)}` : ""}`;
|
|
24
25
|
}
|
|
25
26
|
const host = typeof args.host === "string" && args.host.length > 0 ? args.host : "github.com";
|
|
26
27
|
const owner = typeof args.owner === "string" ? args.owner : "";
|
|
27
28
|
const repo = typeof args.repo === "string" ? args.repo : "";
|
|
28
29
|
const ref = typeof args.ref === "string" ? `@${args.ref}` : "";
|
|
29
|
-
return `${label} ${theme.fg("accent",
|
|
30
|
+
return `${label} ${theme.fg("accent", `${host}/${owner}/${repo}${ref}`)}`;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export function formatRepoFetchResult(result: (RepoFetchResult & { workspaceId: string }) | undefined, theme: LectorTheme): string {
|
|
@@ -2,13 +2,14 @@ import type { TextSearchResult } 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_MATCHES = 20;
|
|
7
8
|
|
|
8
9
|
export function formatSearchCall(args: { directory?: unknown; query?: unknown }, theme: LectorTheme): string {
|
|
9
10
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
10
11
|
const query = typeof args.query === "string" ? args.query : "";
|
|
11
|
-
return `${theme.fg("toolTitle", theme.bold("search_code"))} ${theme.fg("accent", `"${query}"`)} ${theme.fg("dim", directory)}`;
|
|
12
|
+
return `${theme.fg("toolTitle", theme.bold(presentationTitle("search_code")))} ${theme.fg("accent", `"${query}"`)} ${theme.fg("dim", directory)}`;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function formatSearchResult(result: TextSearchResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CacheResultCounts, JobSnapshot, PopulateSymbolGraphResult, WorkspaceCacheStatus } 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 WorkspaceCacheAction = "status" | "populate" | "wait" | "job_status";
|
|
5
6
|
|
|
@@ -8,10 +9,10 @@ export function formatWorkspaceCacheCall(
|
|
|
8
9
|
args: { directory?: unknown; maxFiles?: unknown; maxSymbolsPerFile?: unknown; jobId?: unknown },
|
|
9
10
|
theme: LectorTheme,
|
|
10
11
|
): string {
|
|
11
|
-
const label = theme.fg("toolTitle", theme.bold("workspace_cache"));
|
|
12
|
+
const label = theme.fg("toolTitle", theme.bold(presentationTitle("workspace_cache", action)));
|
|
12
13
|
if (action === "job_status" || action === "wait") {
|
|
13
14
|
const jobId = typeof args.jobId === "string" ? args.jobId : "";
|
|
14
|
-
return `${label} ${theme.fg("accent",
|
|
15
|
+
return `${label} ${theme.fg("accent", jobId)}`;
|
|
15
16
|
}
|
|
16
17
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
17
18
|
const maxFiles = typeof args.maxFiles === "number" ? String(args.maxFiles) : "default";
|
|
@@ -20,7 +21,7 @@ export function formatWorkspaceCacheCall(
|
|
|
20
21
|
action === "populate" && (typeof args.maxFiles === "number" || typeof args.maxSymbolsPerFile === "number")
|
|
21
22
|
? theme.fg("dim", ` (maxFiles=${maxFiles}, maxSymbolsPerFile=${maxSymbolsPerFile})`)
|
|
22
23
|
: "";
|
|
23
|
-
return `${label} ${theme.fg("accent",
|
|
24
|
+
return `${label} ${theme.fg("accent", directory)}${bounds}`;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
function formatResultCounts(result: CacheResultCounts): string {
|
package/package.json
CHANGED