@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,4 +1,4 @@
1
- import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
1
+ import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationInputs, OperationOutputs } from "@danypops/lector";
2
2
  import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
3
3
  import { invokeLectorVehicleOperation, type LectorVehicleCall } from "../vehicle-client.ts";
4
4
 
@@ -6,6 +6,8 @@ type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"
6
6
  type GitWorktreeAddResult = OperationOutputs["workspace.gitWorktreeAdd"];
7
7
  type GitWorktreeRemoveResult = OperationOutputs["workspace.gitWorktreeRemove"];
8
8
  type GitGrepResult = OperationOutputs["workspace.gitGrep"];
9
+ type GitHistoryGrepResult = OperationOutputs["workspace.gitGrepHistory"];
10
+ type GitHistoryGrepBounds = Omit<OperationInputs["workspace.gitGrepHistory"], "workspaceId" | "pattern" | "pathspecs">;
9
11
  type GitListFilesResult = OperationOutputs["workspace.gitListFiles"];
10
12
 
11
13
  /** Matches GIT_READ_PERMISSIONS' own declared value server-side (git/operation-registration.ts). */
@@ -42,6 +44,14 @@ export interface GitOperations {
42
44
  maxBytes: number,
43
45
  call: LectorVehicleCall,
44
46
  ): Promise<GitGrepResult>;
47
+ /** Bounded extended-regex search across commit trees reachable from every ref, with deterministic topological paging and exact-match deduplication. */
48
+ grepHistory(
49
+ directory: string,
50
+ pattern: string,
51
+ pathspecs: readonly string[] | undefined,
52
+ bounds: GitHistoryGrepBounds,
53
+ call: LectorVehicleCall,
54
+ ): Promise<GitHistoryGrepResult>;
45
55
  /** Every file path in `ref`'s own tree, no checkout -- pathspecs narrows the listing (prefix-based, not glob-based like grep's). */
46
56
  listFiles(directory: string, ref: string, pathspecs: readonly string[] | undefined, maxResults: number, call: LectorVehicleCall): Promise<GitListFilesResult>;
47
57
  /** True iff ancestorRef is a real ancestor of (or the exact same commit as) ref -- the backport/reachability check "was this fix ported to this branch" actually needs. */
@@ -139,6 +149,18 @@ export function createLectorGitOperations(): GitOperations {
139
149
  ),
140
150
  );
141
151
  },
152
+ async grepHistory(directory, pattern, pathspecs, bounds, call) {
153
+ return withWorkspace(
154
+ () => workspaceForDirectory(directory),
155
+ ({ workspaceId }) =>
156
+ invokeLectorVehicleOperation<GitHistoryGrepResult>(
157
+ "workspace.gitGrepHistory",
158
+ { workspaceId, pattern, pathspecs, ...bounds },
159
+ GIT_READ_PERMISSIONS,
160
+ call,
161
+ ),
162
+ );
163
+ },
142
164
  async listFiles(directory, ref, pathspecs, maxResults, call) {
143
165
  return withWorkspace(
144
166
  () => workspaceForDirectory(directory),
@@ -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 };
@@ -11,12 +12,24 @@ const DEFAULT_VISIBLE_FILES = 20;
11
12
  const DEFAULT_VISIBLE_COMMITS = 10;
12
13
  const DEFAULT_VISIBLE_DIFF_LINES = 60;
13
14
 
14
- export type GitAction = "status" | "log" | "diff" | "compare-symbol" | "worktree-add" | "worktree-remove" | "show" | "grep-ref" | "ls-ref" | "is-ancestor";
15
+ export type GitAction =
16
+ | "status"
17
+ | "log"
18
+ | "diff"
19
+ | "compare-symbol"
20
+ | "worktree-add"
21
+ | "worktree-remove"
22
+ | "show"
23
+ | "grep-ref"
24
+ | "grep-history"
25
+ | "ls-ref"
26
+ | "is-ancestor";
15
27
 
16
28
  type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
17
29
  type GitWorktreeAddResult = OperationOutputs["workspace.gitWorktreeAdd"];
18
30
  type GitWorktreeRemoveResult = OperationOutputs["workspace.gitWorktreeRemove"];
19
31
  type GitGrepResult = OperationOutputs["workspace.gitGrep"];
32
+ type GitHistoryGrepResult = OperationOutputs["workspace.gitGrepHistory"];
20
33
  type GitListFilesResult = OperationOutputs["workspace.gitListFiles"];
21
34
 
22
35
  export interface GitToolDetails {
@@ -29,6 +42,7 @@ export interface GitToolDetails {
29
42
  readonly worktreeRemove?: GitWorktreeRemoveResult;
30
43
  readonly showFile?: { readonly ref: string; readonly path: string; readonly content: string | undefined };
31
44
  readonly grep?: GitGrepResult;
45
+ readonly historyGrep?: GitHistoryGrepResult;
32
46
  readonly listFiles?: GitListFilesResult;
33
47
  readonly isAncestor?: { readonly ancestorRef: string; readonly ref: string; readonly result: boolean };
34
48
  }
@@ -48,31 +62,32 @@ export function formatGitCall(
48
62
  theme: LectorTheme,
49
63
  ): string {
50
64
  const action = typeof args.action === "string" ? args.action : "";
65
+ const label = theme.fg("toolTitle", theme.bold(presentationTitle("git", action)));
51
66
  const directory = typeof args.directory === "string" ? args.directory : "";
52
67
  if (action === "compare-symbol") {
53
68
  const path = typeof args.path === "string" ? args.path : "";
54
69
  const symbol = typeof args.symbol === "string" ? args.symbol : "";
55
70
  const fromRef = typeof args.fromRef === "string" ? args.fromRef : "";
56
71
  const toRef = typeof args.toRef === "string" ? ` -> ${args.toRef}` : "";
57
- 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}`;
58
73
  }
59
74
  if (action === "is-ancestor") {
60
75
  const ancestorRef = typeof args.ancestorRef === "string" ? args.ancestorRef : "";
61
76
  const ref = typeof args.ref === "string" ? args.ref : "";
62
- 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}`;
63
78
  }
64
- if (action === "grep-ref") {
65
- const ref = typeof args.ref === "string" ? args.ref : "";
79
+ if (action === "grep-ref" || action === "grep-history") {
80
+ const ref = action === "grep-ref" && typeof args.ref === "string" ? `${args.ref} ` : "";
66
81
  const pattern = typeof args.pattern === "string" ? args.pattern : "";
67
- 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}"`;
68
83
  }
69
84
  if (action === "show") {
70
85
  const ref = typeof args.ref === "string" ? args.ref : "";
71
86
  const path = typeof args.path === "string" ? args.path : "";
72
- 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}`;
73
88
  }
74
89
  const ref = typeof args.ref === "string" ? ` ${args.ref}` : "";
75
- return `${theme.fg("toolTitle", theme.bold("git"))} ${theme.fg("muted", action)} ${theme.fg("accent", directory)}${ref}`;
90
+ return `${label} ${theme.fg("accent", directory)}${ref}`;
76
91
  }
77
92
 
78
93
  function formatGitWorktreeAddResult(result: GitWorktreeAddResult | undefined, theme: LectorTheme): string {
@@ -105,6 +120,30 @@ function formatGitGrepResult(result: GitGrepResult | undefined, expanded: boolea
105
120
  return lines.join("\n");
106
121
  }
107
122
 
123
+ function formatGitHistoryGrepResult(result: GitHistoryGrepResult | undefined, expanded: boolean, theme: LectorTheme): string {
124
+ if (!result || result.matches.length === 0) {
125
+ if (result?.deadlineReached) return theme.fg("warning", "No matches before the deadline.");
126
+ return theme.fg("dim", "No historical matches.");
127
+ }
128
+ const lines = renderTruncatedList({
129
+ items: result.matches,
130
+ expanded,
131
+ visibleCount: DEFAULT_VISIBLE_FILES,
132
+ formatItem: (match) => {
133
+ const occurrences = match.occurrences > 1 ? ` (${match.occurrences} commits)` : "";
134
+ return `${theme.fg("muted", match.commit.slice(0, 8))} ${theme.fg("accent", `${match.path}:${match.line}`)}:${match.text}${occurrences}`;
135
+ },
136
+ moreLine: moreLine(theme),
137
+ truncationWarning: result.deadlineReached
138
+ ? theme.fg("warning", "(deadline reached)")
139
+ : result.truncated
140
+ ? theme.fg("warning", "(bounded by maxMatches/maxBytes)")
141
+ : undefined,
142
+ });
143
+ if (result.nextCommitOffset !== undefined) lines.push(theme.fg("muted", `next commit offset: ${result.nextCommitOffset}`));
144
+ return lines.join("\n");
145
+ }
146
+
108
147
  function formatGitListFilesResult(result: GitListFilesResult | undefined, expanded: boolean, theme: LectorTheme): string {
109
148
  if (!result || result.paths.length === 0) return theme.fg("dim", "No files.");
110
149
  const lines = renderTruncatedList({
@@ -133,6 +172,7 @@ export function formatGitResult(details: GitToolDetails | undefined, expanded: b
133
172
  if (details.action === "worktree-remove") return formatGitWorktreeRemoveResult(details.worktreeRemove, theme);
134
173
  if (details.action === "show") return formatGitShowFileResult(details.showFile, theme);
135
174
  if (details.action === "grep-ref") return formatGitGrepResult(details.grep, expanded, theme);
175
+ if (details.action === "grep-history") return formatGitHistoryGrepResult(details.historyGrep, expanded, theme);
136
176
  if (details.action === "ls-ref") return formatGitListFilesResult(details.listFiles, expanded, theme);
137
177
  if (details.action === "is-ancestor") return formatGitIsAncestorResult(details.isAncestor, theme);
138
178
  return formatGitDiffResult(details.result, expanded, theme);