@danypops/pi-lector 0.9.1 → 0.9.2

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,8 @@
1
- import type { GitDiffResult, GitLogEntry, GitStatusSummary } from "@danypops/lector";
1
+ import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
2
2
  import { lectorClient, withWorkspace, workspaceForDirectory } from "./lector-client.ts";
3
3
 
4
+ type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
5
+
4
6
  /**
5
7
  * Thin wrappers over Lector's read-only git operations. `directory` is
6
8
  * required, same convention as find_symbols -- no implicit "whatever the
@@ -10,6 +12,7 @@ export interface GitOperations {
10
12
  status(directory: string): Promise<GitStatusSummary>;
11
13
  log(directory: string, maxCount: number): Promise<readonly GitLogEntry[]>;
12
14
  diff(directory: string, ref: string | undefined, maxBytes: number): Promise<GitDiffResult>;
15
+ compareSymbol(directory: string, path: string, symbolName: string, fromRef: string, toRef: string | undefined, maxBytes: number): Promise<SymbolComparison>;
13
16
  }
14
17
 
15
18
  export function createLectorGitOperations(): GitOperations {
@@ -42,5 +45,14 @@ export function createLectorGitOperations(): GitOperations {
42
45
  },
43
46
  );
44
47
  },
48
+ async compareSymbol(directory, path, symbolName, fromRef, toRef, maxBytes) {
49
+ return withWorkspace(
50
+ () => workspaceForDirectory(directory),
51
+ async ({ workspaceId }) => {
52
+ const client = await lectorClient();
53
+ return client.call("workspace.compareSymbolAcrossVersions", { workspaceId, path, symbolName, fromRef, toRef, maxBytes });
54
+ },
55
+ );
56
+ },
45
57
  };
46
58
  }
@@ -1,4 +1,4 @@
1
- import type { GitDiffResult, GitLogEntry, GitStatusSummary } from "@danypops/lector";
1
+ import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
2
2
  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";
@@ -11,18 +11,31 @@ const DEFAULT_VISIBLE_FILES = 20;
11
11
  const DEFAULT_VISIBLE_COMMITS = 10;
12
12
  const DEFAULT_VISIBLE_DIFF_LINES = 60;
13
13
 
14
- export type GitAction = "status" | "log" | "diff";
14
+ export type GitAction = "status" | "log" | "diff" | "compare-symbol";
15
+
16
+ type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
15
17
 
16
18
  export interface GitToolDetails {
17
19
  readonly action: GitAction;
18
20
  readonly summary?: GitStatusSummary;
19
21
  readonly entries?: readonly GitLogEntry[];
20
22
  readonly result?: GitDiffResult;
23
+ readonly comparison?: SymbolComparison;
21
24
  }
22
25
 
23
- export function formatGitCall(args: { action?: unknown; directory?: unknown; ref?: unknown }, theme: LectorTheme): string {
26
+ export function formatGitCall(
27
+ args: { action?: unknown; directory?: unknown; ref?: unknown; path?: unknown; symbol?: unknown; fromRef?: unknown; toRef?: unknown },
28
+ theme: LectorTheme,
29
+ ): string {
24
30
  const action = typeof args.action === "string" ? args.action : "";
25
31
  const directory = typeof args.directory === "string" ? args.directory : "";
32
+ if (action === "compare-symbol") {
33
+ const path = typeof args.path === "string" ? args.path : "";
34
+ const symbol = typeof args.symbol === "string" ? args.symbol : "";
35
+ const fromRef = typeof args.fromRef === "string" ? args.fromRef : "";
36
+ const toRef = typeof args.toRef === "string" ? ` -> ${args.toRef}` : "";
37
+ return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", `${directory}/${path}`)} (${symbol}) ${fromRef}${toRef}`;
38
+ }
26
39
  const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
27
40
  return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)}${ref}`;
28
41
  }
@@ -31,6 +44,7 @@ export function formatGitResult(details: GitToolDetails | undefined, expanded: b
31
44
  if (!details) return theme.fg("dim", "No result.");
32
45
  if (details.action === "status") return formatGitStatusResult(details.summary, expanded, theme);
33
46
  if (details.action === "log") return formatGitLogResult(details.entries, expanded, theme);
47
+ if (details.action === "compare-symbol") return formatCompareSymbolResult(details.comparison, expanded, theme);
34
48
  return formatGitDiffResult(details.result, expanded, theme);
35
49
  }
36
50
 
@@ -90,11 +104,11 @@ function formatGitLogResult(entries: readonly GitLogEntry[] | undefined, expande
90
104
  * is the still-open "render file and Git diffs as bounded native Pi
91
105
  * visuals" follow-up.
92
106
  */
93
- function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolean, theme: LectorTheme): string {
94
- if (!result || result.diff.length === 0) return theme.fg("dim", "No differences.");
107
+ /** Shared by formatGitDiffResult and formatCompareSymbolResult -- both display a real unified-diff string, styled and bounded the same way. */
108
+ function renderStyledDiffLines(diff: string, truncatedUpstream: boolean, expanded: boolean, theme: LectorTheme): string {
95
109
  const styledLines = renderDiffLines(
96
110
  Number.MAX_SAFE_INTEGER,
97
- result.diff,
111
+ diff,
98
112
  {
99
113
  add: (s) => theme.fg("success", s),
100
114
  remove: (s) => theme.fg("error", s),
@@ -110,7 +124,20 @@ function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolea
110
124
  visibleCount: DEFAULT_VISIBLE_DIFF_LINES,
111
125
  formatItem: (line) => line,
112
126
  moreLine: (hidden) => theme.fg("dim", `... ${hidden} more line${hidden === 1 ? "" : "s"} (${keyHint("app.tools.expand", "to expand")})`),
113
- truncationWarning: result.truncated ? theme.fg("warning", "(diff output itself was truncated by maxBytes)") : undefined,
127
+ truncationWarning: truncatedUpstream ? theme.fg("warning", "(diff output itself was truncated by maxBytes)") : undefined,
114
128
  });
115
129
  return lines.join("\n");
116
130
  }
131
+
132
+ function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolean, theme: LectorTheme): string {
133
+ if (!result || result.diff.length === 0) return theme.fg("dim", "No differences.");
134
+ return renderStyledDiffLines(result.diff, result.truncated, expanded, theme);
135
+ }
136
+
137
+ function formatCompareSymbolResult(comparison: SymbolComparison | undefined, expanded: boolean, theme: LectorTheme): string {
138
+ if (!comparison) return theme.fg("dim", "No result.");
139
+ const header = theme.fg("accent", `${comparison.path} (${comparison.symbolName}) -- ${comparison.fromRef} -> ${comparison.toRef}`);
140
+ if (comparison.status === "both-missing") return `${header}\n${theme.fg("dim", "symbol found at neither version")}`;
141
+ if (comparison.status === "unchanged") return `${header}\n${theme.fg("dim", "unchanged")}`;
142
+ return `${header}\n${renderStyledDiffLines(comparison.diff, comparison.truncated, expanded, theme)}`;
143
+ }
@@ -1033,17 +1033,26 @@ export default function (pi: ExtensionAPI) {
1033
1033
  name: "git",
1034
1034
  label: "Git",
1035
1035
  description:
1036
- "Working tree status, recent commit log, and unified diff for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes).",
1037
- promptSnippet: "Show a repository's status, log, or diff",
1036
+ "Working tree status, recent commit log, unified diff, and one symbol's own declaration diff across two versions, for a real git repository, in one tool. Fails clearly if `directory` is not inside a git repository. ACTIONS: status (working tree state, ahead/behind tracking), log (recent commits, bounded by maxCount), diff (unified diff against `ref`, defaulting to HEAD, bounded by maxBytes), compare-symbol (a named symbol's own declaration text diffed between fromRef and toRef, or fromRef and the current working tree when toRef is omitted -- tree-sitter syntactic tier only, TypeScript/JavaScript files only, no project-aware cross-reference resolution).",
1037
+ promptSnippet: "Show a repository's status, log, diff, or one symbol's diff across versions",
1038
1038
  promptGuidelines: [
1039
- "maxCount is required for action=log; maxBytes is required for action=diff -- every bounded query needs its bound stated explicitly, never defaulted silently.",
1039
+ "maxCount is required for action=log; maxBytes is required for action=diff/compare-symbol -- every bounded query needs its bound stated explicitly, never defaulted silently.",
1040
+ "path, symbol, and fromRef are required for action=compare-symbol; toRef is optional and means 'the current working tree' when omitted.",
1040
1041
  ],
1041
1042
  parameters: Type.Object({
1042
- action: Type.String({ description: "status | log | diff" }),
1043
+ action: Type.String({ description: "status | log | diff | compare-symbol" }),
1043
1044
  directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
1044
1045
  maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
1045
1046
  ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD -- only used for action=diff" })),
1046
- maxBytes: Type.Optional(Type.Number({ description: "Maximum diff size in bytes before truncating -- required for action=diff" })),
1047
+ maxBytes: Type.Optional(
1048
+ Type.Number({ description: "Maximum diff/comparison size in bytes before truncating -- required for action=diff/compare-symbol" }),
1049
+ ),
1050
+ path: Type.Optional(Type.String({ description: "File path (relative to directory) containing the symbol -- required for action=compare-symbol" })),
1051
+ symbol: Type.Optional(Type.String({ description: "Exact symbol name to compare -- required for action=compare-symbol" })),
1052
+ fromRef: Type.Optional(Type.String({ description: "Git ref for the 'before' version -- required for action=compare-symbol" })),
1053
+ toRef: Type.Optional(
1054
+ Type.String({ description: "Git ref for the 'after' version; omit to compare against the current working tree -- action=compare-symbol only" }),
1055
+ ),
1047
1056
  }),
1048
1057
  async execute(_toolCallId, params) {
1049
1058
  const directory = resolve(cwd, params.directory);
@@ -1066,6 +1075,13 @@ export default function (pi: ExtensionAPI) {
1066
1075
  const details: GitToolDetails = { action: "diff", result };
1067
1076
  return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details };
1068
1077
  }
1078
+ if (params.action === "compare-symbol") {
1079
+ if (!params.path || !params.symbol || !params.fromRef) throw new Error("git action=compare-symbol requires path, symbol, and fromRef");
1080
+ if (params.maxBytes === undefined) throw new Error("git action=compare-symbol requires maxBytes");
1081
+ const comparison = await gitOperations.compareSymbol(directory, params.path, params.symbol, params.fromRef, params.toRef, params.maxBytes);
1082
+ const details: GitToolDetails = { action: "compare-symbol", comparison };
1083
+ return { content: [{ type: "text", text: JSON.stringify(comparison) }], details };
1084
+ }
1069
1085
  throw new Error(`unknown git action: ${String(params.action)}`);
1070
1086
  },
1071
1087
  renderCall(args, theme, context) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
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",
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@danypops/vehicle-client": "^0.1.1",
22
- "@danypops/lector": "^0.10.0",
22
+ "@danypops/lector": "^0.11.0",
23
23
  "malevich-tui-components": "^0.19.0"
24
24
  },
25
25
  "devDependencies": {