@danypops/pi-lector 0.9.0 → 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(
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/extension/src/index.ts
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
type ExtensionAPI,
|
|
35
35
|
} from "@earendil-works/pi-coding-agent";
|
|
36
36
|
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
37
|
-
import {
|
|
37
|
+
import { renderBoundedTable, type TextMeasure } from "malevich-tui-components";
|
|
38
38
|
import { Type } from "typebox";
|
|
39
39
|
|
|
40
40
|
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
@@ -99,6 +99,8 @@ import {
|
|
|
99
99
|
formatRepoCacheListResult,
|
|
100
100
|
formatRepoFetchResult,
|
|
101
101
|
REPO_CACHE_TABLE_COLUMNS,
|
|
102
|
+
REPO_CACHE_VISIBLE_ROWS,
|
|
103
|
+
repoCacheMoreLine,
|
|
102
104
|
} from "./repo-cache-rendering.ts";
|
|
103
105
|
import { createLectorRepoFetchOperations } from "./repo-fetch-operations.ts";
|
|
104
106
|
import { createLectorSearchOperations } from "./search-operations.ts";
|
|
@@ -1031,17 +1033,26 @@ export default function (pi: ExtensionAPI) {
|
|
|
1031
1033
|
name: "git",
|
|
1032
1034
|
label: "Git",
|
|
1033
1035
|
description:
|
|
1034
|
-
"Working tree status, recent commit log, and
|
|
1035
|
-
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",
|
|
1036
1038
|
promptGuidelines: [
|
|
1037
|
-
"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.",
|
|
1038
1041
|
],
|
|
1039
1042
|
parameters: Type.Object({
|
|
1040
|
-
action: Type.String({ description: "status | log | diff" }),
|
|
1043
|
+
action: Type.String({ description: "status | log | diff | compare-symbol" }),
|
|
1041
1044
|
directory: Type.String({ description: "Directory inside the repository to check, absolute or relative to the current working directory" }),
|
|
1042
1045
|
maxCount: Type.Optional(Type.Number({ description: "Maximum number of commits to return, most recent first -- required for action=log" })),
|
|
1043
1046
|
ref: Type.Optional(Type.String({ description: "Ref to diff against; defaults to HEAD -- only used for action=diff" })),
|
|
1044
|
-
maxBytes: Type.Optional(
|
|
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
|
+
),
|
|
1045
1056
|
}),
|
|
1046
1057
|
async execute(_toolCallId, params) {
|
|
1047
1058
|
const directory = resolve(cwd, params.directory);
|
|
@@ -1064,6 +1075,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1064
1075
|
const details: GitToolDetails = { action: "diff", result };
|
|
1065
1076
|
return { content: [{ type: "text", text: result.diff.length === 0 ? "No differences." : result.diff }], details };
|
|
1066
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
|
+
}
|
|
1067
1085
|
throw new Error(`unknown git action: ${String(params.action)}`);
|
|
1068
1086
|
},
|
|
1069
1087
|
renderCall(args, theme, context) {
|
|
@@ -1447,7 +1465,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1447
1465
|
text.setText(formatRepoCacheCall(action, args, theme));
|
|
1448
1466
|
return text;
|
|
1449
1467
|
},
|
|
1450
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
1468
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
1451
1469
|
if (isPartial) return new Text(theme.fg("warning", "Working on repo cache..."), 0, 0);
|
|
1452
1470
|
if (context.isError) {
|
|
1453
1471
|
const errorText = result.content
|
|
@@ -1457,16 +1475,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1457
1475
|
return new Text(theme.fg("error", errorText || "repo_cache failed"), 0, 0);
|
|
1458
1476
|
}
|
|
1459
1477
|
const details = result.details as RepoCacheToolDetails | undefined;
|
|
1460
|
-
// A non-empty list renders as a real Table -- the human channel actually shows
|
|
1461
|
-
// host/owner/repo/ref/registered/size/fetched, not just a bare count
|
|
1462
|
-
//
|
|
1478
|
+
// A non-empty list renders as a real, bounded Table -- the human channel actually shows
|
|
1479
|
+
// host/owner/repo/ref/registered/size/fetched, not just a bare count, capped at
|
|
1480
|
+
// REPO_CACHE_VISIBLE_ROWS since a cache can grow arbitrarily large even though maxResults
|
|
1481
|
+
// bounds any one page. Every other branch (empty list, fetch, evict) stays a plain Text line.
|
|
1463
1482
|
if (details?.action === "list" && details.page.entries.length > 0) {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1483
|
+
// Rebuilt fresh on every call (not reused via context.lastComponent) since expanded is
|
|
1484
|
+
// fixed at BoundedTable construction, matching every other truncated-list renderer in this
|
|
1485
|
+
// codebase (they all call renderTruncatedList fresh with the current expanded each time).
|
|
1486
|
+
return renderBoundedTable({
|
|
1487
|
+
columns: REPO_CACHE_TABLE_COLUMNS,
|
|
1488
|
+
rows: buildRepoCacheTableRows(details.page.entries),
|
|
1489
|
+
expanded,
|
|
1490
|
+
visibleRowCount: REPO_CACHE_VISIBLE_ROWS,
|
|
1491
|
+
moreLine: repoCacheMoreLine(theme),
|
|
1492
|
+
measure: tableMeasure,
|
|
1493
|
+
headerStyle: (s) => theme.fg("muted", theme.bold(s)),
|
|
1494
|
+
});
|
|
1470
1495
|
}
|
|
1471
1496
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1472
1497
|
if (details?.action === "list") text.setText(formatRepoCacheListResult(details.page, theme));
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import type { CachedRepositoryEntry, CachedRepositoryPage, RepoFetchResult } from "@danypops/lector";
|
|
2
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import type { TableColumn } from "malevich-tui-components";
|
|
3
4
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
4
5
|
|
|
6
|
+
/** 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
|
+
export const REPO_CACHE_VISIBLE_ROWS = 20;
|
|
8
|
+
|
|
9
|
+
export function repoCacheMoreLine(theme: LectorTheme): (hiddenCount: number) => string {
|
|
10
|
+
return (hiddenCount) => theme.fg("dim", `... ${hiddenCount} more (${keyHint("app.tools.expand", "to expand")})`);
|
|
11
|
+
}
|
|
12
|
+
|
|
5
13
|
type RepoCacheAction = "fetch" | "list" | "evict";
|
|
6
14
|
|
|
7
15
|
export function formatRepoCacheCall(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.9.
|
|
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,8 +19,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/vehicle-client": "^0.1.1",
|
|
22
|
-
"@danypops/lector": "^0.
|
|
23
|
-
"malevich-tui-components": "^0.
|
|
22
|
+
"@danypops/lector": "^0.11.0",
|
|
23
|
+
"malevich-tui-components": "^0.19.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@earendil-works/pi-ai": "^0.81.1",
|