@danypops/pi-lector 0.13.9 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,17 +2,20 @@ 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 {
15
- if (!result || result.matches.length === 0) return theme.fg("dim", "No matches found.");
16
+ if (!result) return theme.fg("dim", "No matches found.");
17
+ const provenance = result.provenance ? theme.fg("dim", `lexical via ${result.provenance.backend} (${result.provenance.indexState})`) : undefined;
18
+ if (result.matches.length === 0) return [provenance, theme.fg("dim", "No matches found.")].filter((line) => line !== undefined).join("\n");
16
19
  const lines = renderTruncatedList({
17
20
  items: result.matches,
18
21
  expanded,
@@ -22,5 +25,5 @@ export function formatSearchResult(result: TextSearchResult | undefined, expande
22
25
  moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
23
26
  truncationWarning: result.truncated ? theme.fg("warning", "(search itself was truncated by maxMatches/maxBytes -- results are incomplete)") : undefined,
24
27
  });
25
- return lines.join("\n");
28
+ return [provenance, ...lines].filter((line) => line !== undefined).join("\n");
26
29
  }
@@ -13,6 +13,8 @@ export interface AnnotationAnchorInput {
13
13
  readonly character: number;
14
14
  }
15
15
 
16
+ export type AnnotationAutoPopulationOptions = Pick<OperationInputs["workspace.createAnnotation"], "autoPopulate" | "maxFiles" | "maxSymbolsPerFile">;
17
+
16
18
  /**
17
19
  * Thin wrappers over Lector's annotation operations. Every operation resolves its workspace from
18
20
  * its own `path` parameter via workspaceForAnnotationPath -- a real project directory or an
@@ -29,6 +31,7 @@ export interface SymbolAnnotationOperations {
29
31
  body: string,
30
32
  anchors: readonly AnnotationAnchorInput[],
31
33
  call: LectorVehicleCall,
34
+ autoPopulation?: AnnotationAutoPopulationOptions,
32
35
  ): Promise<OperationOutputs["workspace.createAnnotation"]>;
33
36
  get(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.getAnnotation"]>;
34
37
  list(
@@ -44,6 +47,7 @@ export interface SymbolAnnotationOperations {
44
47
  body: string,
45
48
  anchors: readonly AnnotationAnchorInput[],
46
49
  call: LectorVehicleCall,
50
+ autoPopulation?: AnnotationAutoPopulationOptions,
47
51
  ): Promise<OperationOutputs["workspace.refreshAnnotation"]>;
48
52
  scrub(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.scrubAnnotation"]>;
49
53
  restore(path: string, id: string, call: LectorVehicleCall): Promise<OperationOutputs["workspace.restoreAnnotation"]>;
@@ -54,13 +58,13 @@ export interface SymbolAnnotationOperations {
54
58
 
55
59
  export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperations {
56
60
  return {
57
- async create(path, subtype, title, body, anchors, call) {
61
+ async create(path, subtype, title, body, anchors, call, autoPopulation) {
58
62
  return withWorkspace(
59
63
  () => workspaceForAnnotationPath(path),
60
64
  ({ workspaceId }) =>
61
65
  invokeLectorVehicleOperation<OperationOutputs["workspace.createAnnotation"]>(
62
66
  "workspace.createAnnotation",
63
- { workspaceId, subtype, title, body, anchors },
67
+ { workspaceId, subtype, title, body, anchors, ...autoPopulation },
64
68
  ANNOTATION_WRITE_PERMISSIONS,
65
69
  call,
66
70
  ),
@@ -90,13 +94,13 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
90
94
  ),
91
95
  );
92
96
  },
93
- async refresh(path, id, subtype, title, body, anchors, call) {
97
+ async refresh(path, id, subtype, title, body, anchors, call, autoPopulation) {
94
98
  return withWorkspace(
95
99
  () => workspaceForAnnotationPath(path),
96
100
  ({ workspaceId }) =>
97
101
  invokeLectorVehicleOperation<OperationOutputs["workspace.refreshAnnotation"]>(
98
102
  "workspace.refreshAnnotation",
99
- { workspaceId, id, subtype, title, body, anchors },
103
+ { workspaceId, id, subtype, title, body, anchors, ...autoPopulation },
100
104
  ANNOTATION_WRITE_PERMISSIONS,
101
105
  call,
102
106
  ),
@@ -29,9 +29,11 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
29
29
 
30
30
  type VehicleClientConnector = () => Promise<VehicleClient>;
31
31
 
32
- function connectLectorVehicleClient(): Promise<VehicleClient> {
32
+ async function connectLectorVehicleClient(): Promise<VehicleClient> {
33
33
  const { host, port, token } = resolveLectorDaemonConnection();
34
- return Promise.resolve(new RemoteVehicleClient({ baseUrl: `http://${host}:${port}`, token }));
34
+ const client = new RemoteVehicleClient({ baseUrl: `http://${host}:${port}`, token });
35
+ await client.negotiate({ minimumVersion: 1, maximumVersion: 1, requiredCapabilities: [], optionalCapabilities: [] });
36
+ return client;
35
37
  }
36
38
 
37
39
  function resolveLectorVehicleIdentity() {
@@ -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.9",
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",
@@ -22,9 +22,9 @@
22
22
  "@danypops/vehicle-client-pi": "^0.45.0"
23
23
  },
24
24
  "dependencies": {
25
- "@danypops/lector": "^0.20.0",
26
- "@danypops/vehicle-client": "^0.10.3",
27
- "@danypops/vehicle-core": "^0.17.1",
25
+ "@danypops/lector": "^0.20.4",
26
+ "@danypops/vehicle-client": "^0.10.8",
27
+ "@danypops/vehicle-core": "^0.19.1",
28
28
  "malevich-tui-components": "^0.32.1",
29
29
  "picomatch": "^4.0.5"
30
30
  },