@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.
@@ -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
- return `${formatPositionalCall("type_hierarchy", args, theme)} ${theme.fg("muted", typeof args.direction === "string" ? args.direction : "")}`;
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"))} ${theme.fg("muted", direction)} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
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(args: { directories?: unknown; query?: unknown }, theme: LectorTheme): string {
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", action)} ${theme.fg("dim", query)}`;
10
+ return `${label} ${theme.fg("accent", `"${query}"`)}`;
10
11
  }
11
12
 
12
- export function formatGithubRepoSearchResult(result: GithubRepoSearchResult | undefined, theme: LectorTheme): string {
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
- const count = result.candidates.length;
15
- const summary = count === 0 ? theme.fg("dim", "no repositories matched") : theme.fg("success", `${count} repositor${count === 1 ? "y" : "ies"}`);
16
- return result.authenticated ? summary : `${summary} ${theme.fg("warning", "(unauthenticated -- lower rate limit)")}`;
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(result: { candidates: readonly NpmPackageCandidate[] } | undefined, theme: LectorTheme): string {
20
- const count = result?.candidates.length ?? 0;
21
- return count === 0 ? theme.fg("dim", "no packages matched") : theme.fg("success", `${count} package${count === 1 ? "" : "s"}`);
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(result: { candidates: readonly SourcegraphCodeCandidate[] } | undefined, theme: LectorTheme): string {
25
- const count = result?.candidates.length ?? 0;
26
- return count === 0 ? theme.fg("dim", "no code matches") : theme.fg("success", `${count} match${count === 1 ? "" : "es"}`);
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 `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", `${directory}/${path}`)} (${symbol}) ${fromRef}${toRef}`;
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 `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)} ${ancestorRef} -> ${ref}`;
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 `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)} ${ref}"${pattern}"`;
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 `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", `${directory}/${path}`)} @ ${ref}`;
87
+ return `${label} ${theme.fg("accent", `${directory}/${path}`)} @ ${ref}`;
86
88
  }
87
89
  const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
88
- return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)}${ref}`;
90
+ return `${label} ${theme.fg("accent", directory)}${ref}`;
89
91
  }
90
92
 
91
93
  function formatGitWorktreeAddResult(result: GitWorktreeAddResult | undefined, theme: LectorTheme): string {
@@ -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(theme.fg("toolTitle", `localize ${typeof args.query === "string" ? args.query : "context"}`), 0, 0);
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 details = result.details as ContextBundleResult | undefined;
486
- return new Text(details ? `${details.candidates.length} candidates · graph ${details.completeness.graph}` : "Localization complete", 0, 0);
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"))} ${theme.fg("dim", action)} ${theme.fg("accent", path)}`);
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"))} ${theme.fg("accent", action)}${theme.fg("dim", id)}`);
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 { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "job_status", job } };
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 { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "wait", job } };
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 { content: [{ type: "text", text: JSON.stringify(status) }], details: { action: "status", status } };
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 { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "populate", job } };
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: JSON.stringify(summary) }], details };
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: JSON.stringify(comparison) }], details };
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: JSON.stringify(worktreeAdd) }], details };
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: JSON.stringify(worktreeRemove) }], details };
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: JSON.stringify(grep) }], details };
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: JSON.stringify(historyGrep) }], details };
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: JSON.stringify(listFiles) }], details };
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: JSON.stringify({ isAncestor: result }) }], details };
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"))} ${theme.fg("accent", action)} ${theme.fg("dim", path)}`);
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 { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "remove", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "clean", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "resolve", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "fetch", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "evict", result } };
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 { content: [{ type: "text", text: JSON.stringify(page) }], details: { action: "list", page } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "github_repos", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "npm_packages", result } };
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 { content: [{ type: "text", text: JSON.stringify(result) }], details: { action: "sourcegraph_code", result } };
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 { content: [{ type: "text", text: JSON.stringify(results) }], details: { results } };
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 { content: [{ type: "text", text: JSON.stringify(results) }], details: { results } };
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} ${theme.fg("accent", "list")}${text}`;
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} ${theme.fg("accent", action)}${identity ? ` ${theme.fg("dim", identity)}` : ""}`;
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} ${theme.fg("accent", "list")}${filter ? ` ${theme.fg("dim", filter)}` : ""}`;
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", action)} ${theme.fg("dim", `${host}/${owner}/${repo}${ref}`)}`;
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", action)} ${theme.fg("dim", jobId)}`;
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", action)} ${theme.fg("dim", directory)}${bounds}`;
24
+ return `${label} ${theme.fg("accent", directory)}${bounds}`;
24
25
  }
25
26
 
26
27
  function formatResultCounts(result: CacheResultCounts): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.13.11",
3
+ "version": "0.14.0",
4
4
  "description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
5
5
  "license": "MIT",
6
6
  "type": "module",