@danypops/pi-lector 0.8.1 → 0.9.1
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.
- package/extension/src/apply-patch-rendering.ts +39 -2
- package/extension/src/code-intelligence-rendering.ts +95 -62
- package/extension/src/cross-workspace-search-rendering.ts +22 -13
- package/extension/src/find-files-rendering.ts +9 -5
- package/extension/src/find-symbols-rendering.ts +8 -10
- package/extension/src/git-rendering.ts +65 -20
- package/extension/src/index.ts +36 -4
- package/extension/src/package-source-rendering.ts +12 -5
- package/extension/src/repo-cache-rendering.ts +41 -1
- package/extension/src/search-rendering.ts +9 -5
- package/package.json +3 -2
|
@@ -1,9 +1,46 @@
|
|
|
1
1
|
import type { EditOutcome } from "@danypops/lector";
|
|
2
|
+
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
2
3
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
+
/**
|
|
6
|
+
* The applied result (EditOutcome) only carries a hash transition, not the
|
|
7
|
+
* diff text itself -- the model already has what it submitted, so there's
|
|
8
|
+
* nothing to render as a Diff on the result side. The call's own patchText
|
|
9
|
+
* argument is the real diff content, so the colored preview lives here
|
|
10
|
+
* instead of formatApplyPatchResult, unlike git_diff's coloring (which
|
|
11
|
+
* lives on the result side because that's where GitDiffResult's diff text
|
|
12
|
+
* actually is).
|
|
13
|
+
*/
|
|
14
|
+
const DEFAULT_VISIBLE_PATCH_LINES = 12;
|
|
15
|
+
|
|
16
|
+
export function formatApplyPatchCall(args: { path?: unknown; patchText?: unknown }, theme: LectorTheme, measure?: TextMeasure): string {
|
|
5
17
|
const path = typeof args.path === "string" ? args.path : "";
|
|
6
|
-
|
|
18
|
+
const header = `${theme.fg("toolTitle", theme.bold("apply_patch"))} ${theme.fg("accent", path)}`;
|
|
19
|
+
if (typeof args.patchText !== "string" || args.patchText.length === 0) return header;
|
|
20
|
+
|
|
21
|
+
const styledLines = renderDiffLines(
|
|
22
|
+
Number.MAX_SAFE_INTEGER,
|
|
23
|
+
args.patchText,
|
|
24
|
+
{
|
|
25
|
+
add: (s) => theme.fg("success", s),
|
|
26
|
+
remove: (s) => theme.fg("error", s),
|
|
27
|
+
context: (s) => theme.fg("dim", s),
|
|
28
|
+
hunk: (s) => theme.fg("accent", s),
|
|
29
|
+
header: (s) => theme.fg("muted", s),
|
|
30
|
+
},
|
|
31
|
+
measure,
|
|
32
|
+
);
|
|
33
|
+
// No expand affordance here -- renderCall has no `expanded` option (that's
|
|
34
|
+
// renderResult-only), so a call-time preview is always just a hard cap,
|
|
35
|
+
// never an "expand to see more" invitation the call slot can't honor.
|
|
36
|
+
const preview = renderTruncatedList({
|
|
37
|
+
items: styledLines,
|
|
38
|
+
expanded: false,
|
|
39
|
+
visibleCount: DEFAULT_VISIBLE_PATCH_LINES,
|
|
40
|
+
formatItem: (line) => line,
|
|
41
|
+
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more line${hidden === 1 ? "" : "s"}`),
|
|
42
|
+
});
|
|
43
|
+
return [header, ...preview].join("\n");
|
|
7
44
|
}
|
|
8
45
|
|
|
9
46
|
export function formatApplyPatchResult(result: EditOutcome | undefined, theme: LectorTheme): string {
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
WorkspaceMapResult,
|
|
12
12
|
} from "@danypops/lector";
|
|
13
13
|
import { keyHint, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
14
15
|
import { colorForKind, formatLocation, type LectorTheme } from "./lector-tui-theme.ts";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -41,16 +42,23 @@ function formatPositionalCall(toolName: string, args: { path?: unknown; line?: u
|
|
|
41
42
|
return `${theme.fg("toolTitle", theme.bold(toolName))} ${theme.fg("accent", `${path}:${line}:${character}`)}`;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function moreLine(theme: LectorTheme): (hidden: number) => string {
|
|
46
|
+
return (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`);
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
function formatLocationList(locations: readonly WorkspaceLocation[] | undefined, emptyMessage: string, expanded: boolean, theme: LectorTheme): string {
|
|
45
50
|
if (!locations || locations.length === 0) return theme.fg("dim", emptyMessage);
|
|
46
51
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const lines = [
|
|
53
|
+
theme.fg("muted", `${locations.length} location${locations.length === 1 ? "" : "s"}:`),
|
|
54
|
+
...renderTruncatedList({
|
|
55
|
+
items: locations,
|
|
56
|
+
expanded,
|
|
57
|
+
visibleCount: DEFAULT_VISIBLE_LOCATIONS,
|
|
58
|
+
formatItem: (location) => ` ${formatLocation(theme, location.path, location.line, location.character)}`,
|
|
59
|
+
moreLine: moreLine(theme),
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
54
62
|
return lines.join("\n");
|
|
55
63
|
}
|
|
56
64
|
|
|
@@ -88,8 +96,14 @@ export function formatHoverResult(hover: Hover | undefined, expanded: boolean, t
|
|
|
88
96
|
if (!hover) return theme.fg("dim", "No hover information available.");
|
|
89
97
|
const lines = hover.contents.split("\n");
|
|
90
98
|
if (expanded || lines.length <= HOVER_COLLAPSED_LINE_COUNT) return hover.contents;
|
|
91
|
-
const
|
|
92
|
-
|
|
99
|
+
const body = renderTruncatedList({
|
|
100
|
+
items: lines,
|
|
101
|
+
expanded: false,
|
|
102
|
+
visibleCount: HOVER_COLLAPSED_LINE_COUNT,
|
|
103
|
+
formatItem: (line) => line,
|
|
104
|
+
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more line${hidden === 1 ? "" : "s"} (${keyHint("app.tools.expand", "to expand")})`),
|
|
105
|
+
});
|
|
106
|
+
return body.join("\n");
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
export function formatDocumentSymbolsCall(args: { path?: unknown }, theme: LectorTheme): string {
|
|
@@ -112,19 +126,22 @@ export function formatDocumentSymbolsResult(symbols: readonly DocumentSymbolEntr
|
|
|
112
126
|
|
|
113
127
|
const flattened = flattenSymbols(symbols);
|
|
114
128
|
const kindColumnWidth = Math.max(...flattened.map(({ entry }) => entry.kind.length));
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const lines = [
|
|
130
|
+
theme.fg("muted", `${flattened.length} symbol${flattened.length === 1 ? "" : "s"}:`),
|
|
131
|
+
...renderTruncatedList({
|
|
132
|
+
items: flattened,
|
|
133
|
+
expanded,
|
|
134
|
+
visibleCount: DEFAULT_VISIBLE_SYMBOLS,
|
|
135
|
+
formatItem: ({ depth, entry }) => {
|
|
136
|
+
const indent = " ".repeat(depth + 1);
|
|
137
|
+
const kind = theme.fg(colorForKind(entry.kind), entry.kind.padEnd(kindColumnWidth));
|
|
138
|
+
const name = theme.fg("text", theme.bold(entry.name));
|
|
139
|
+
const location = formatLocation(theme, entry.range.path, entry.selectionRange.start.line, entry.selectionRange.start.character);
|
|
140
|
+
return `${indent}${kind} ${name} ${location}`;
|
|
141
|
+
},
|
|
142
|
+
moreLine: moreLine(theme),
|
|
143
|
+
}),
|
|
144
|
+
];
|
|
128
145
|
return lines.join("\n");
|
|
129
146
|
}
|
|
130
147
|
|
|
@@ -136,18 +153,21 @@ export function formatDiagnosticsCall(args: { path?: unknown }, theme: LectorThe
|
|
|
136
153
|
export function formatDiagnosticsResult(diagnostics: readonly Diagnostic[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
137
154
|
if (!diagnostics || diagnostics.length === 0) return theme.fg("success", "No diagnostics.");
|
|
138
155
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
const lines = [
|
|
157
|
+
theme.fg("muted", `${diagnostics.length} diagnostic${diagnostics.length === 1 ? "" : "s"}:`),
|
|
158
|
+
...renderTruncatedList({
|
|
159
|
+
items: diagnostics,
|
|
160
|
+
expanded,
|
|
161
|
+
visibleCount: DEFAULT_VISIBLE_DIAGNOSTICS,
|
|
162
|
+
formatItem: (diagnostic) => {
|
|
163
|
+
const severity = theme.fg(DIAGNOSTIC_SEVERITY_COLOR[diagnostic.severity], theme.bold(diagnostic.severity));
|
|
164
|
+
const location = formatLocation(theme, diagnostic.range.path, diagnostic.range.start.line, diagnostic.range.start.character);
|
|
165
|
+
const origin = diagnostic.source ? theme.fg("dim", ` (${diagnostic.source}${diagnostic.code !== undefined ? ` ${diagnostic.code}` : ""})`) : "";
|
|
166
|
+
return ` ${severity} ${location} -- ${diagnostic.message}${origin}`;
|
|
167
|
+
},
|
|
168
|
+
moreLine: moreLine(theme),
|
|
169
|
+
}),
|
|
170
|
+
];
|
|
151
171
|
return lines.join("\n");
|
|
152
172
|
}
|
|
153
173
|
|
|
@@ -183,24 +203,32 @@ function formatPrepareCallHierarchyResult(items: readonly CallHierarchyEntry[] |
|
|
|
183
203
|
function formatIncomingCallsResult(calls: readonly IncomingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
184
204
|
if (!calls || calls.length === 0) return theme.fg("dim", "No incoming calls found.");
|
|
185
205
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
206
|
+
const lines = [
|
|
207
|
+
theme.fg("muted", `${calls.length} caller${calls.length === 1 ? "" : "s"}:`),
|
|
208
|
+
...renderTruncatedList({
|
|
209
|
+
items: calls,
|
|
210
|
+
expanded,
|
|
211
|
+
visibleCount: DEFAULT_VISIBLE_CALLS,
|
|
212
|
+
formatItem: (call) => ` ${formatCallHierarchyEntry(call.from, theme)}`,
|
|
213
|
+
moreLine: moreLine(theme),
|
|
214
|
+
}),
|
|
215
|
+
];
|
|
192
216
|
return lines.join("\n");
|
|
193
217
|
}
|
|
194
218
|
|
|
195
219
|
function formatOutgoingCallsResult(calls: readonly OutgoingCall[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
196
220
|
if (!calls || calls.length === 0) return theme.fg("dim", "No outgoing calls found.");
|
|
197
221
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
222
|
+
const lines = [
|
|
223
|
+
theme.fg("muted", `${calls.length} callee${calls.length === 1 ? "" : "s"}:`),
|
|
224
|
+
...renderTruncatedList({
|
|
225
|
+
items: calls,
|
|
226
|
+
expanded,
|
|
227
|
+
visibleCount: DEFAULT_VISIBLE_CALLS,
|
|
228
|
+
formatItem: (call) => ` ${formatCallHierarchyEntry(call.to, theme)}`,
|
|
229
|
+
moreLine: moreLine(theme),
|
|
230
|
+
}),
|
|
231
|
+
];
|
|
204
232
|
return lines.join("\n");
|
|
205
233
|
}
|
|
206
234
|
|
|
@@ -221,12 +249,16 @@ export function formatReachableFromResult(symbols: readonly SymbolNode[] | undef
|
|
|
221
249
|
if (!symbols || symbols.length === 0)
|
|
222
250
|
return theme.fg("dim", "Nothing reachable at this position (the workspace's symbol graph may still be populating in the background -- retry shortly).");
|
|
223
251
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
const lines = [
|
|
253
|
+
theme.fg("muted", `${symbols.length} reachable symbol${symbols.length === 1 ? "" : "s"}:`),
|
|
254
|
+
...renderTruncatedList({
|
|
255
|
+
items: symbols,
|
|
256
|
+
expanded,
|
|
257
|
+
visibleCount: DEFAULT_VISIBLE_CALLS,
|
|
258
|
+
formatItem: (symbol) => ` ${formatCallHierarchyEntry(symbol, theme)}`,
|
|
259
|
+
moreLine: moreLine(theme),
|
|
260
|
+
}),
|
|
261
|
+
];
|
|
230
262
|
return lines.join("\n");
|
|
231
263
|
}
|
|
232
264
|
|
|
@@ -240,21 +272,22 @@ export function formatWorkspaceMapResult(result: WorkspaceMapResult | undefined,
|
|
|
240
272
|
if (!result || result.entries.length === 0)
|
|
241
273
|
return theme.fg("dim", "No ranked symbols (the workspace's symbol graph may still be populating in the background -- retry shortly).");
|
|
242
274
|
|
|
243
|
-
const displayCount = expanded ? result.entries.length : Math.min(result.entries.length, DEFAULT_VISIBLE_SYMBOLS);
|
|
244
275
|
const lines = [
|
|
245
276
|
theme.fg(
|
|
246
277
|
"muted",
|
|
247
278
|
`${result.entries.length} of ${result.totalRanked} ranked symbol${result.totalRanked === 1 ? "" : "s"}, most structurally central first:`,
|
|
248
279
|
),
|
|
280
|
+
...renderTruncatedList({
|
|
281
|
+
items: result.entries,
|
|
282
|
+
expanded,
|
|
283
|
+
visibleCount: DEFAULT_VISIBLE_SYMBOLS,
|
|
284
|
+
formatItem: (entry) => {
|
|
285
|
+
const signature = entry.signature ? ` -- ${entry.signature}` : "";
|
|
286
|
+
return ` ${theme.fg(colorForKind(entry.kind), entry.kind)} ${theme.bold(entry.name)} ${formatLocation(theme, entry.path, entry.line, entry.character)}${signature}`;
|
|
287
|
+
},
|
|
288
|
+
moreLine: moreLine(theme),
|
|
289
|
+
truncationWarning: result.truncated ? theme.fg("warning", "budget-truncated -- raise --max-entries/--max-bytes for more") : undefined,
|
|
290
|
+
}),
|
|
249
291
|
];
|
|
250
|
-
for (const entry of result.entries.slice(0, displayCount)) {
|
|
251
|
-
const signature = entry.signature ? ` -- ${entry.signature}` : "";
|
|
252
|
-
lines.push(
|
|
253
|
-
` ${theme.fg(colorForKind(entry.kind), entry.kind)} ${theme.bold(entry.name)} ${formatLocation(theme, entry.path, entry.line, entry.character)}${signature}`,
|
|
254
|
-
);
|
|
255
|
-
}
|
|
256
|
-
const remaining = displayCount - result.entries.length;
|
|
257
|
-
if (remaining < 0) lines.push(theme.fg("dim", `... ${-remaining} more (${keyHint("app.tools.expand", "to expand")})`));
|
|
258
|
-
if (result.truncated) lines.push(theme.fg("warning", "budget-truncated -- raise --max-entries/--max-bytes for more"));
|
|
259
292
|
return lines.join("\n");
|
|
260
293
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SymbolSearchResult, TextSearchResult, WorkspaceQueryOutcome } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
3
4
|
import { describeFindSymbolSources } from "./find-symbols-rendering.ts";
|
|
4
5
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
5
6
|
|
|
@@ -35,12 +36,15 @@ export function formatFindSymbolsAcrossProjectsResult(
|
|
|
35
36
|
lines.push(theme.fg("dim", " no symbols matched"));
|
|
36
37
|
continue;
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
lines.push(
|
|
40
|
+
...renderTruncatedList({
|
|
41
|
+
items: outcome.result.symbols,
|
|
42
|
+
expanded,
|
|
43
|
+
visibleCount: DEFAULT_VISIBLE_PER_WORKSPACE,
|
|
44
|
+
formatItem: (symbol) => ` ${symbol.kind} ${symbol.name} -- ${symbol.location.path}:${symbol.location.line}:${symbol.location.character}`,
|
|
45
|
+
moreLine: (hidden) => theme.fg("dim", ` ... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
44
48
|
}
|
|
45
49
|
return lines.join("\n");
|
|
46
50
|
}
|
|
@@ -59,13 +63,18 @@ export function formatSearchTextAcrossProjectsResult(
|
|
|
59
63
|
lines.push(theme.fg("dim", " no matches"));
|
|
60
64
|
continue;
|
|
61
65
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
lines.push(
|
|
67
|
+
...renderTruncatedList({
|
|
68
|
+
items: outcome.result.matches,
|
|
69
|
+
expanded,
|
|
70
|
+
visibleCount: DEFAULT_VISIBLE_PER_WORKSPACE,
|
|
71
|
+
formatItem: (match) => ` ${match.path}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}`,
|
|
72
|
+
moreLine: (hidden) => theme.fg("dim", ` ... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
73
|
+
truncationWarning: outcome.result.truncated
|
|
74
|
+
? theme.fg("warning", " (this workspace's search was itself truncated by maxMatches/maxBytes)")
|
|
75
|
+
: undefined,
|
|
76
|
+
}),
|
|
77
|
+
);
|
|
69
78
|
}
|
|
70
79
|
return lines.join("\n");
|
|
71
80
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FindFilesResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
3
4
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_VISIBLE_PATHS = 40;
|
|
@@ -12,10 +13,13 @@ export function formatFindFilesCall(args: { directory?: unknown; patterns?: unkn
|
|
|
12
13
|
|
|
13
14
|
export function formatFindFilesResult(result: FindFilesResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
14
15
|
if (!result || result.paths.length === 0) return theme.fg("dim", "No files found.");
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const lines = renderTruncatedList({
|
|
17
|
+
items: result.paths,
|
|
18
|
+
expanded,
|
|
19
|
+
visibleCount: DEFAULT_VISIBLE_PATHS,
|
|
20
|
+
formatItem: (path) => theme.fg("accent", path),
|
|
21
|
+
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
22
|
+
truncationWarning: result.truncated ? theme.fg("warning", "(listing itself was truncated by maxResults/maxBytes -- results are incomplete)") : undefined,
|
|
23
|
+
});
|
|
20
24
|
return lines.join("\n");
|
|
21
25
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SymbolSearchResult, WorkspaceSymbol } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
3
4
|
import { colorForKind, formatLocation, type LectorTheme } from "./lector-tui-theme.ts";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -52,21 +53,18 @@ export function formatFindSymbolsResult(result: SymbolSearchResult | undefined,
|
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
const kindColumnWidth = Math.max(...symbols.map((symbol) => symbol.kind.length));
|
|
55
|
-
const displayCount = expanded ? symbols.length : Math.min(symbols.length, DEFAULT_VISIBLE_RESULTS);
|
|
56
56
|
const lines = [
|
|
57
57
|
theme.fg("muted", source),
|
|
58
58
|
...sourceLines,
|
|
59
59
|
theme.fg("muted", `${symbols.length} symbol${symbols.length === 1 ? "" : "s"} matching "${query}":`),
|
|
60
|
+
...renderTruncatedList({
|
|
61
|
+
items: symbols,
|
|
62
|
+
expanded,
|
|
63
|
+
visibleCount: DEFAULT_VISIBLE_RESULTS,
|
|
64
|
+
formatItem: (symbol) => formatSymbolLine(symbol, theme, kindColumnWidth),
|
|
65
|
+
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
66
|
+
}),
|
|
60
67
|
];
|
|
61
68
|
|
|
62
|
-
for (const symbol of symbols.slice(0, displayCount)) {
|
|
63
|
-
lines.push(formatSymbolLine(symbol, theme, kindColumnWidth));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const remaining = symbols.length - displayCount;
|
|
67
|
-
if (remaining > 0) {
|
|
68
|
-
lines.push(theme.fg("dim", `... ${remaining} more (${keyHint("app.tools.expand", "to expand")})`));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
69
|
return lines.join("\n");
|
|
72
70
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { GitDiffResult, GitLogEntry, GitStatusSummary } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
3
5
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
4
6
|
|
|
7
|
+
/** Real ANSI-aware measurement, not Malevich's own ASCII-only default -- every diff line renderDiffLines receives is already theme.fg-styled. */
|
|
8
|
+
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
9
|
+
|
|
5
10
|
const DEFAULT_VISIBLE_FILES = 20;
|
|
6
11
|
const DEFAULT_VISIBLE_COMMITS = 10;
|
|
7
12
|
const DEFAULT_VISIBLE_DIFF_LINES = 60;
|
|
@@ -29,6 +34,10 @@ export function formatGitResult(details: GitToolDetails | undefined, expanded: b
|
|
|
29
34
|
return formatGitDiffResult(details.result, expanded, theme);
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
function moreLine(theme: LectorTheme): (hidden: number) => string {
|
|
38
|
+
return (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
function formatGitStatusResult(summary: GitStatusSummary | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
33
42
|
if (!summary) return theme.fg("dim", "No status available.");
|
|
34
43
|
const branch = summary.current ?? "(detached)";
|
|
@@ -38,34 +47,70 @@ function formatGitStatusResult(summary: GitStatusSummary | undefined, expanded:
|
|
|
38
47
|
lines.push(theme.fg("dim", "working tree clean"));
|
|
39
48
|
return lines.join("\n");
|
|
40
49
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
lines.push(
|
|
51
|
+
...renderTruncatedList({
|
|
52
|
+
items: summary.files,
|
|
53
|
+
expanded,
|
|
54
|
+
visibleCount: DEFAULT_VISIBLE_FILES,
|
|
55
|
+
formatItem: (file) => {
|
|
56
|
+
const code = `${file.indexStatus}${file.workingDirStatus}`;
|
|
57
|
+
return file.renamedFrom ? `${code} ${file.renamedFrom} -> ${file.path}` : `${code} ${file.path}`;
|
|
58
|
+
},
|
|
59
|
+
moreLine: moreLine(theme),
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
48
62
|
return lines.join("\n");
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
function formatGitLogResult(entries: readonly GitLogEntry[] | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
52
66
|
if (!entries || entries.length === 0) return theme.fg("dim", "No commits found.");
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const lines = renderTruncatedList({
|
|
68
|
+
items: entries,
|
|
69
|
+
expanded,
|
|
70
|
+
visibleCount: DEFAULT_VISIBLE_COMMITS,
|
|
71
|
+
formatItem: (entry) => `${theme.fg("accent", entry.sha.slice(0, 8))} ${entry.authoredAt} ${entry.authorName} -- ${entry.message}`,
|
|
72
|
+
moreLine: moreLine(theme),
|
|
73
|
+
});
|
|
59
74
|
return lines.join("\n");
|
|
60
75
|
}
|
|
61
76
|
|
|
77
|
+
/**
|
|
78
|
+
* formatGitDiffResult returns a plain string (like every other renderer in
|
|
79
|
+
* this file), fed into a Text component by index.ts -- Text word-wraps to
|
|
80
|
+
* whatever width the host renders at, same as before this change. A real
|
|
81
|
+
* per-line width-aware ellipsis-truncation (the way Diff is meant to be
|
|
82
|
+
* used against a real terminal width, matching how Table already truncates
|
|
83
|
+
* an oversized cell) isn't available at this string-building stage, since
|
|
84
|
+
* renderResult's context carries no terminal width. Number.MAX_SAFE_INTEGER
|
|
85
|
+
* here means renderDiffLines' own truncation never fires -- a
|
|
86
|
+
* pathologically long single line still gets word-wrapped by Text rather
|
|
87
|
+
* than ellipsis-truncated, exactly as it did before this migration. This
|
|
88
|
+
* migration's actual scope is real +/- coloring; rendering git_diff as a
|
|
89
|
+
* genuine width-aware Component (fixing the wrap-vs-truncate tradeoff too)
|
|
90
|
+
* is the still-open "render file and Git diffs as bounded native Pi
|
|
91
|
+
* visuals" follow-up.
|
|
92
|
+
*/
|
|
62
93
|
function formatGitDiffResult(result: GitDiffResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
63
94
|
if (!result || result.diff.length === 0) return theme.fg("dim", "No differences.");
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
95
|
+
const styledLines = renderDiffLines(
|
|
96
|
+
Number.MAX_SAFE_INTEGER,
|
|
97
|
+
result.diff,
|
|
98
|
+
{
|
|
99
|
+
add: (s) => theme.fg("success", s),
|
|
100
|
+
remove: (s) => theme.fg("error", s),
|
|
101
|
+
context: (s) => theme.fg("dim", s),
|
|
102
|
+
hunk: (s) => theme.fg("accent", s),
|
|
103
|
+
header: (s) => theme.fg("muted", s),
|
|
104
|
+
},
|
|
105
|
+
measure,
|
|
106
|
+
);
|
|
107
|
+
const lines = renderTruncatedList({
|
|
108
|
+
items: styledLines,
|
|
109
|
+
expanded,
|
|
110
|
+
visibleCount: DEFAULT_VISIBLE_DIFF_LINES,
|
|
111
|
+
formatItem: (line) => line,
|
|
112
|
+
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,
|
|
114
|
+
});
|
|
115
|
+
return lines.join("\n");
|
|
71
116
|
}
|
package/extension/src/index.ts
CHANGED
|
@@ -33,8 +33,13 @@ import {
|
|
|
33
33
|
createWriteToolDefinition,
|
|
34
34
|
type ExtensionAPI,
|
|
35
35
|
} from "@earendil-works/pi-coding-agent";
|
|
36
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
36
|
+
import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
37
|
+
import { renderBoundedTable, type TextMeasure } from "malevich-tui-components";
|
|
37
38
|
import { Type } from "typebox";
|
|
39
|
+
|
|
40
|
+
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
41
|
+
const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
42
|
+
|
|
38
43
|
import { createLectorApplyPatchOperations } from "./apply-patch-operations.ts";
|
|
39
44
|
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch-rendering.ts";
|
|
40
45
|
import { createLectorCodeIntelligenceOperations } from "./code-intelligence-operations.ts";
|
|
@@ -87,7 +92,16 @@ import { createReferenceBasedRenameOperations } from "./reference-based-rename-o
|
|
|
87
92
|
import { createRenameOperations } from "./rename-operations.ts";
|
|
88
93
|
import { createRepoCacheEvictOperations } from "./repo-cache-evict-operations.ts";
|
|
89
94
|
import { createRepoCacheListOperations } from "./repo-cache-list-operations.ts";
|
|
90
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
buildRepoCacheTableRows,
|
|
97
|
+
formatRepoCacheCall,
|
|
98
|
+
formatRepoCacheEvictResult,
|
|
99
|
+
formatRepoCacheListResult,
|
|
100
|
+
formatRepoFetchResult,
|
|
101
|
+
REPO_CACHE_TABLE_COLUMNS,
|
|
102
|
+
REPO_CACHE_VISIBLE_ROWS,
|
|
103
|
+
repoCacheMoreLine,
|
|
104
|
+
} from "./repo-cache-rendering.ts";
|
|
91
105
|
import { createLectorRepoFetchOperations } from "./repo-fetch-operations.ts";
|
|
92
106
|
import { createLectorSearchOperations } from "./search-operations.ts";
|
|
93
107
|
import { formatSearchCall, formatSearchResult } from "./search-rendering.ts";
|
|
@@ -1261,7 +1275,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1261
1275
|
},
|
|
1262
1276
|
renderCall(args, theme, context) {
|
|
1263
1277
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1264
|
-
text.setText(formatApplyPatchCall(args, theme));
|
|
1278
|
+
text.setText(formatApplyPatchCall(args, theme, tableMeasure));
|
|
1265
1279
|
return text;
|
|
1266
1280
|
},
|
|
1267
1281
|
renderResult(result, { isPartial }, theme, context) {
|
|
@@ -1435,7 +1449,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1435
1449
|
text.setText(formatRepoCacheCall(action, args, theme));
|
|
1436
1450
|
return text;
|
|
1437
1451
|
},
|
|
1438
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
1452
|
+
renderResult(result, { isPartial, expanded }, theme, context) {
|
|
1439
1453
|
if (isPartial) return new Text(theme.fg("warning", "Working on repo cache..."), 0, 0);
|
|
1440
1454
|
if (context.isError) {
|
|
1441
1455
|
const errorText = result.content
|
|
@@ -1445,6 +1459,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
1445
1459
|
return new Text(theme.fg("error", errorText || "repo_cache failed"), 0, 0);
|
|
1446
1460
|
}
|
|
1447
1461
|
const details = result.details as RepoCacheToolDetails | undefined;
|
|
1462
|
+
// A non-empty list renders as a real, bounded Table -- the human channel actually shows
|
|
1463
|
+
// host/owner/repo/ref/registered/size/fetched, not just a bare count, capped at
|
|
1464
|
+
// REPO_CACHE_VISIBLE_ROWS since a cache can grow arbitrarily large even though maxResults
|
|
1465
|
+
// bounds any one page. Every other branch (empty list, fetch, evict) stays a plain Text line.
|
|
1466
|
+
if (details?.action === "list" && details.page.entries.length > 0) {
|
|
1467
|
+
// Rebuilt fresh on every call (not reused via context.lastComponent) since expanded is
|
|
1468
|
+
// fixed at BoundedTable construction, matching every other truncated-list renderer in this
|
|
1469
|
+
// codebase (they all call renderTruncatedList fresh with the current expanded each time).
|
|
1470
|
+
return renderBoundedTable({
|
|
1471
|
+
columns: REPO_CACHE_TABLE_COLUMNS,
|
|
1472
|
+
rows: buildRepoCacheTableRows(details.page.entries),
|
|
1473
|
+
expanded,
|
|
1474
|
+
visibleRowCount: REPO_CACHE_VISIBLE_ROWS,
|
|
1475
|
+
moreLine: repoCacheMoreLine(theme),
|
|
1476
|
+
measure: tableMeasure,
|
|
1477
|
+
headerStyle: (s) => theme.fg("muted", theme.bold(s)),
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1448
1480
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1449
1481
|
if (details?.action === "list") text.setText(formatRepoCacheListResult(details.page, theme));
|
|
1450
1482
|
else if (details?.action === "evict") text.setText(formatRepoCacheEvictResult(details.result, theme));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PackageSourceOperationResult } from "@danypops/lector";
|
|
2
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
2
3
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
3
4
|
|
|
4
5
|
const DEFAULT_VISIBLE_CANDIDATES = 5;
|
|
@@ -21,11 +22,17 @@ export function formatPackageSourceResult(result: PackageSourceOperationResult |
|
|
|
21
22
|
].join("\n");
|
|
22
23
|
}
|
|
23
24
|
if (outcome.status === "ambiguous") {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
const lines = [
|
|
26
|
+
theme.fg("warning", `Ambiguous package source (${outcome.code})`),
|
|
27
|
+
...renderTruncatedList({
|
|
28
|
+
items: outcome.candidates,
|
|
29
|
+
expanded,
|
|
30
|
+
visibleCount: DEFAULT_VISIBLE_CANDIDATES,
|
|
31
|
+
formatItem: (candidate) => `${candidate.version} -- ${candidate.source}`,
|
|
32
|
+
moreLine: (hidden) => theme.fg("dim", `… ${hidden} more`),
|
|
33
|
+
truncationWarning: outcome.truncated ? theme.fg("dim", "More candidates were truncated by the daemon.") : undefined,
|
|
34
|
+
}),
|
|
35
|
+
];
|
|
29
36
|
return lines.join("\n");
|
|
30
37
|
}
|
|
31
38
|
if (outcome.status === "unauthenticated") {
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import type { CachedRepositoryPage, RepoFetchResult } from "@danypops/lector";
|
|
1
|
+
import type { CachedRepositoryEntry, CachedRepositoryPage, RepoFetchResult } from "@danypops/lector";
|
|
2
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TableColumn } from "malevich-tui-components";
|
|
2
4
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
3
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
|
+
|
|
4
13
|
type RepoCacheAction = "fetch" | "list" | "evict";
|
|
5
14
|
|
|
6
15
|
export function formatRepoCacheCall(
|
|
@@ -31,11 +40,42 @@ export function formatRepoFetchResult(result: (RepoFetchResult & { workspaceId:
|
|
|
31
40
|
return lines.join("\n");
|
|
32
41
|
}
|
|
33
42
|
|
|
43
|
+
/** Empty-state fallback only -- a non-empty page renders as a real Table (see REPO_CACHE_TABLE_COLUMNS/buildRepoCacheTableRows) so the human channel actually shows what's cached, not just a bare count. */
|
|
34
44
|
export function formatRepoCacheListResult(page: CachedRepositoryPage | undefined, theme: LectorTheme): string {
|
|
35
45
|
const count = page?.entries.length ?? 0;
|
|
36
46
|
return count === 0 ? theme.fg("dim", "no cached repositories") : theme.fg("success", `${count} cached repositor${count === 1 ? "y" : "ies"}`);
|
|
37
47
|
}
|
|
38
48
|
|
|
49
|
+
export const REPO_CACHE_TABLE_COLUMNS: TableColumn[] = [
|
|
50
|
+
{ header: "Repository", key: "repo" },
|
|
51
|
+
{ header: "Ref", key: "ref" },
|
|
52
|
+
{ header: "Registered", key: "registered" },
|
|
53
|
+
{ header: "Size", key: "size" },
|
|
54
|
+
{ header: "Fetched", key: "fetched" },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
/** Powers of 1024, one decimal past the first -- "cache size" is always at least a git checkout, so bytes/KB granularity is never useful here. */
|
|
58
|
+
function formatCacheSize(bytes: number): string {
|
|
59
|
+
const units = ["B", "KB", "MB", "GB", "TB"] as const;
|
|
60
|
+
let value = bytes;
|
|
61
|
+
let unitIndex = 0;
|
|
62
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
63
|
+
value /= 1024;
|
|
64
|
+
unitIndex++;
|
|
65
|
+
}
|
|
66
|
+
return `${unitIndex === 0 ? value : value.toFixed(1)} ${units[unitIndex]}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildRepoCacheTableRows(entries: readonly CachedRepositoryEntry[]): Record<string, string>[] {
|
|
70
|
+
return entries.map((entry) => ({
|
|
71
|
+
repo: `${entry.host}/${entry.owner}/${entry.repo}`,
|
|
72
|
+
ref: entry.requestedRef === entry.resolvedRef ? entry.resolvedRef : `${entry.requestedRef} -> ${entry.resolvedRef}`,
|
|
73
|
+
registered: entry.registeredWorkspaceId ?? "no",
|
|
74
|
+
size: formatCacheSize(entry.cacheSizeBytes),
|
|
75
|
+
fetched: new Date(entry.fetchedAt).toISOString(),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
export function formatRepoCacheEvictResult(result: { evicted: boolean } | undefined, theme: LectorTheme): string {
|
|
40
80
|
if (!result) return theme.fg("dim", "No result.");
|
|
41
81
|
return result.evicted ? theme.fg("success", "evicted") : theme.fg("dim", "nothing cached for that reference");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TextSearchResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { renderTruncatedList } from "malevich-tui-components";
|
|
3
4
|
import type { LectorTheme } from "./lector-tui-theme.ts";
|
|
4
5
|
|
|
5
6
|
const DEFAULT_VISIBLE_MATCHES = 20;
|
|
@@ -12,10 +13,13 @@ export function formatSearchCall(args: { directory?: unknown; query?: unknown },
|
|
|
12
13
|
|
|
13
14
|
export function formatSearchResult(result: TextSearchResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
14
15
|
if (!result || result.matches.length === 0) return theme.fg("dim", "No matches found.");
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const lines = renderTruncatedList({
|
|
17
|
+
items: result.matches,
|
|
18
|
+
expanded,
|
|
19
|
+
visibleCount: DEFAULT_VISIBLE_MATCHES,
|
|
20
|
+
formatItem: (match) => `${theme.fg("accent", match.path)}:${match.lineNumber}: ${match.line.replace(/\n$/, "")}`,
|
|
21
|
+
moreLine: (hidden) => theme.fg("dim", `... ${hidden} more (${keyHint("app.tools.expand", "to expand")})`),
|
|
22
|
+
truncationWarning: result.truncated ? theme.fg("warning", "(search itself was truncated by maxMatches/maxBytes -- results are incomplete)") : undefined,
|
|
23
|
+
});
|
|
20
24
|
return lines.join("\n");
|
|
21
25
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
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,8 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/vehicle-client": "^0.1.1",
|
|
22
|
-
"@danypops/lector": "^0.10.0"
|
|
22
|
+
"@danypops/lector": "^0.10.0",
|
|
23
|
+
"malevich-tui-components": "^0.19.0"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@earendil-works/pi-ai": "^0.81.1",
|