@oh-my-pi/pi-coding-agent 15.10.2 → 15.10.4
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/CHANGELOG.md +66 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +1 -0
- package/dist/types/eval/bridge-timeout.d.ts +1 -1
- package/dist/types/eval/{llm-bridge.d.ts → completion-bridge.d.ts} +8 -8
- package/dist/types/eval/idle-timeout.d.ts +1 -1
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/late-diagnostics-message.d.ts +20 -0
- package/dist/types/modes/components/read-tool-group.d.ts +6 -0
- package/dist/types/modes/components/session-selector.d.ts +16 -7
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +5 -1
- package/dist/types/tools/read.d.ts +2 -1
- package/dist/types/tools/render-utils.d.ts +3 -1
- package/dist/types/tools/renderers.d.ts +0 -15
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tui/code-cell.d.ts +0 -2
- package/dist/types/tui/hyperlink.d.ts +5 -7
- package/dist/types/tui/output-block.d.ts +0 -18
- package/package.json +9 -9
- package/src/cli/gallery-cli.ts +4 -0
- package/src/cli/gallery-fixtures/codeintel.ts +0 -1
- package/src/cli/gallery-fixtures/fs.ts +68 -1
- package/src/cli/gallery-fixtures/types.ts +8 -1
- package/src/commit/agentic/agent.ts +1 -0
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/agent-bridge.test.ts +13 -0
- package/src/eval/__tests__/{llm-bridge.test.ts → completion-bridge.test.ts} +60 -54
- package/src/eval/__tests__/js-context-manager.test.ts +241 -0
- package/src/eval/agent-bridge.ts +6 -1
- package/src/eval/bridge-timeout.ts +1 -1
- package/src/eval/{llm-bridge.ts → completion-bridge.ts} +30 -27
- package/src/eval/idle-timeout.ts +1 -1
- package/src/eval/js/context-manager.ts +66 -6
- package/src/eval/js/shared/prelude.txt +28 -12
- package/src/eval/js/tool-bridge.ts +3 -3
- package/src/eval/js/worker-entry.ts +6 -0
- package/src/eval/py/prelude.py +3 -3
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/lsp/index.ts +128 -52
- package/src/main.ts +54 -14
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/plan-review-overlay.ts +26 -5
- package/src/modes/components/read-tool-group.ts +415 -35
- package/src/modes/components/session-selector.ts +89 -35
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/tool-execution.ts +7 -49
- package/src/modes/components/transcript-container.ts +108 -32
- package/src/modes/controllers/event-controller.ts +6 -1
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system.md +16 -3
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/sdk.ts +59 -1
- package/src/session/agent-session.ts +5 -3
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +2 -2
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +1 -0
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +0 -7
- package/src/tools/eval-render.ts +6 -25
- package/src/tools/eval.ts +1 -1
- package/src/tools/find.ts +148 -106
- package/src/tools/index.ts +32 -0
- package/src/tools/path-utils.ts +19 -22
- package/src/tools/read.ts +16 -8
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +3 -12
- package/src/tui/code-cell.ts +1 -6
- package/src/tui/hyperlink.ts +13 -23
- package/src/tui/output-block.ts +2 -97
- package/src/utils/title-generator.ts +2 -2
- /package/dist/types/eval/__tests__/{llm-bridge.test.d.ts → completion-bridge.test.d.ts} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
2
3
|
import { Container, Text } from "@oh-my-pi/pi-tui";
|
|
3
4
|
import { InternalUrlRouter } from "../../internal-urls";
|
|
4
5
|
import { getLanguageFromPath, theme } from "../../modes/theme/theme";
|
|
5
|
-
import { splitPathAndSel } from "../../tools/path-utils";
|
|
6
|
+
import { parseLineRanges, selectorLineRanges, splitPathAndSel } from "../../tools/path-utils";
|
|
6
7
|
import { PREVIEW_LIMITS, shortenPath } from "../../tools/render-utils";
|
|
7
|
-
import { renderCodeCell } from "../../tui";
|
|
8
|
+
import { fileHyperlink, renderCodeCell, tryResolveInternalUrlSync } from "../../tui";
|
|
8
9
|
import type { ToolExecutionHandle } from "./tool-execution";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -46,11 +47,19 @@ type ReadToolSuffixResolution = {
|
|
|
46
47
|
};
|
|
47
48
|
|
|
48
49
|
type ReadToolResultDetails = {
|
|
50
|
+
resolvedPath?: string;
|
|
49
51
|
suffixResolution?: {
|
|
50
52
|
from?: string;
|
|
51
53
|
to?: string;
|
|
52
54
|
};
|
|
53
55
|
conflictCount?: number;
|
|
56
|
+
displayReadTargets?: unknown;
|
|
57
|
+
meta?: {
|
|
58
|
+
source?: {
|
|
59
|
+
type?: string;
|
|
60
|
+
value?: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
54
63
|
};
|
|
55
64
|
|
|
56
65
|
type ReadToolGroupOptions = {
|
|
@@ -67,6 +76,8 @@ function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadTo
|
|
|
67
76
|
type ReadEntry = {
|
|
68
77
|
toolCallId: string;
|
|
69
78
|
path: string;
|
|
79
|
+
displayPaths?: string[];
|
|
80
|
+
linkPath?: string;
|
|
70
81
|
status: "pending" | "success" | "warning" | "error";
|
|
71
82
|
correctedFrom?: string;
|
|
72
83
|
contentText?: string;
|
|
@@ -76,6 +87,197 @@ type ReadEntry = {
|
|
|
76
87
|
/** Number of code lines to show in collapsed preview mode */
|
|
77
88
|
const COLLAPSED_PREVIEW_LINES = PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
78
89
|
|
|
90
|
+
type ReadDisplayTarget = {
|
|
91
|
+
entry: ReadEntry;
|
|
92
|
+
targetPath: string;
|
|
93
|
+
basePath: string;
|
|
94
|
+
linkPath?: string;
|
|
95
|
+
selector?: string;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
type ReadSummaryRow = {
|
|
99
|
+
targetPath: string;
|
|
100
|
+
basePath: string;
|
|
101
|
+
targets: ReadDisplayTarget[];
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const READ_STATUS_RANK: Record<ReadEntry["status"], number> = {
|
|
105
|
+
success: 0,
|
|
106
|
+
pending: 1,
|
|
107
|
+
warning: 2,
|
|
108
|
+
error: 3,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const URL_LIKE_RE = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
112
|
+
|
|
113
|
+
function getDisplayReadTargets(details: ReadToolResultDetails | undefined): string[] | undefined {
|
|
114
|
+
if (!Array.isArray(details?.displayReadTargets)) return undefined;
|
|
115
|
+
const targets = details.displayReadTargets
|
|
116
|
+
.filter((target): target is string => typeof target === "string")
|
|
117
|
+
.map(target => target.trim())
|
|
118
|
+
.filter(target => target.length > 0);
|
|
119
|
+
return targets.length > 0 ? targets : undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function displayPathWithSuffixResolution(currentPath: string, suffixResolution: ReadToolSuffixResolution): string {
|
|
123
|
+
const currentSelector = splitPathAndSel(currentPath).sel;
|
|
124
|
+
if (!currentSelector || splitPathAndSel(suffixResolution.to).sel) return suffixResolution.to;
|
|
125
|
+
return `${suffixResolution.to}:${currentSelector}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function readSourceFsPath(details: ReadToolResultDetails | undefined): string | undefined {
|
|
129
|
+
const source = details?.meta?.source;
|
|
130
|
+
return source?.type === "path" && typeof source.value === "string" ? source.value : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readResultLinkPath(details: ReadToolResultDetails | undefined): string | undefined {
|
|
134
|
+
return typeof details?.resolvedPath === "string" ? details.resolvedPath : readSourceFsPath(details);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readTargetLinkPath(basePath: string, entryLinkPath: string | undefined): string | undefined {
|
|
138
|
+
if (entryLinkPath) return entryLinkPath;
|
|
139
|
+
const resolvedInternalPath = tryResolveInternalUrlSync(basePath);
|
|
140
|
+
if (resolvedInternalPath) return resolvedInternalPath;
|
|
141
|
+
return path.isAbsolute(basePath) ? basePath : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function firstSelectorLine(selector: string | undefined): number | undefined {
|
|
145
|
+
try {
|
|
146
|
+
return selectorLineRanges(selector)?.[0].startLine;
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function firstSelectorLineForTargets(targets: ReadDisplayTarget[]): number | undefined {
|
|
153
|
+
let line: number | undefined;
|
|
154
|
+
for (const target of targets) {
|
|
155
|
+
const targetLine = firstSelectorLine(target.selector);
|
|
156
|
+
if (targetLine === undefined) continue;
|
|
157
|
+
if (line === undefined || targetLine < line) line = targetLine;
|
|
158
|
+
}
|
|
159
|
+
return line;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function linkPathForTargets(targets: ReadDisplayTarget[]): string | undefined {
|
|
163
|
+
for (const target of targets) {
|
|
164
|
+
if (target.linkPath) return target.linkPath;
|
|
165
|
+
}
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function selectorChunkIsLineRangeList(chunk: string): boolean {
|
|
170
|
+
const trimmed = chunk.trim();
|
|
171
|
+
if (!trimmed) return false;
|
|
172
|
+
try {
|
|
173
|
+
return parseLineRanges(trimmed) !== null;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function nextTopLevelToken(input: string, start: number): string {
|
|
180
|
+
let braceDepth = 0;
|
|
181
|
+
for (let i = start; i < input.length; i++) {
|
|
182
|
+
const ch = input[i];
|
|
183
|
+
if (ch === "\\" && i + 1 < input.length) {
|
|
184
|
+
i++;
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ch === "{") {
|
|
188
|
+
braceDepth++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (ch === "}") {
|
|
192
|
+
if (braceDepth > 0) braceDepth--;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (braceDepth === 0 && (ch === "," || ch === ";")) {
|
|
196
|
+
return input.slice(start, i);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return input.slice(start);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function commaContinuesLineRangeSelector(input: string, partStart: number, commaIndex: number): boolean {
|
|
203
|
+
const currentPart = input.slice(partStart, commaIndex).trim();
|
|
204
|
+
if (!splitPathAndSel(currentPart).sel) return false;
|
|
205
|
+
return selectorChunkIsLineRangeList(nextTopLevelToken(input, commaIndex + 1));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function splitReadDisplayPathSpecs(rawPath: string): string[] {
|
|
209
|
+
const normalized = rawPath.trim();
|
|
210
|
+
if (!normalized || URL_LIKE_RE.test(normalized)) return [rawPath];
|
|
211
|
+
|
|
212
|
+
const parts: string[] = [];
|
|
213
|
+
let braceDepth = 0;
|
|
214
|
+
let partStart = 0;
|
|
215
|
+
for (let i = 0; i < normalized.length; i++) {
|
|
216
|
+
const ch = normalized[i];
|
|
217
|
+
if (ch === "\\" && i + 1 < normalized.length) {
|
|
218
|
+
i++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (ch === "{") {
|
|
222
|
+
braceDepth++;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (ch === "}") {
|
|
226
|
+
if (braceDepth > 0) braceDepth--;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (braceDepth !== 0 || (ch !== "," && ch !== ";")) continue;
|
|
230
|
+
if (ch === "," && commaContinuesLineRangeSelector(normalized, partStart, i)) continue;
|
|
231
|
+
parts.push(normalized.slice(partStart, i).trim());
|
|
232
|
+
partStart = i + 1;
|
|
233
|
+
}
|
|
234
|
+
parts.push(normalized.slice(partStart).trim());
|
|
235
|
+
|
|
236
|
+
const cleanParts = parts.filter(part => part.length > 0);
|
|
237
|
+
if (cleanParts.length <= 1) return [rawPath];
|
|
238
|
+
return cleanParts.every(part => splitPathAndSel(part).sel !== undefined) ? cleanParts : [rawPath];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitSelectorDisplayParts(sel: string | undefined): Array<string | undefined> {
|
|
242
|
+
if (!sel) return [undefined];
|
|
243
|
+
const chunks = sel.split(":");
|
|
244
|
+
if (chunks.length === 1) {
|
|
245
|
+
if (!selectorChunkIsLineRangeList(sel) || !sel.includes(",")) return [sel];
|
|
246
|
+
return sel
|
|
247
|
+
.split(",")
|
|
248
|
+
.map(chunk => chunk.trim())
|
|
249
|
+
.filter(chunk => chunk.length > 0);
|
|
250
|
+
}
|
|
251
|
+
if (chunks.length === 2) {
|
|
252
|
+
const [left, right] = chunks as [string, string];
|
|
253
|
+
const leftIsRange = selectorChunkIsLineRangeList(left);
|
|
254
|
+
const rightIsRange = selectorChunkIsLineRangeList(right);
|
|
255
|
+
if (leftIsRange && left.includes(",")) {
|
|
256
|
+
return left
|
|
257
|
+
.split(",")
|
|
258
|
+
.map(chunk => chunk.trim())
|
|
259
|
+
.filter(chunk => chunk.length > 0)
|
|
260
|
+
.map(chunk => `${chunk}:${right}`);
|
|
261
|
+
}
|
|
262
|
+
if (rightIsRange && right.includes(",")) {
|
|
263
|
+
return right
|
|
264
|
+
.split(",")
|
|
265
|
+
.map(chunk => chunk.trim())
|
|
266
|
+
.filter(chunk => chunk.length > 0)
|
|
267
|
+
.map(chunk => `${left}:${chunk}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [sel];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatMergedSelectorParts(selectors: string[]): string {
|
|
274
|
+
if (selectors.length <= 3) return selectors.join(",");
|
|
275
|
+
const first = selectors[0]!;
|
|
276
|
+
const second = selectors[1]!;
|
|
277
|
+
const last = selectors[selectors.length - 1]!;
|
|
278
|
+
return `${first},${second},…,${last}`;
|
|
279
|
+
}
|
|
280
|
+
|
|
79
281
|
export class ReadToolGroupComponent extends Container implements ToolExecutionHandle {
|
|
80
282
|
#entries = new Map<string, ReadEntry>();
|
|
81
283
|
#text: Text;
|
|
@@ -89,6 +291,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
89
291
|
// (see TranscriptContainer / NativeScrollbackLiveRegion). The controller calls
|
|
90
292
|
// `finalize()` once the run breaks so the block can commit to native scrollback.
|
|
91
293
|
#finalized = false;
|
|
294
|
+
// Forced terminal even with a still-pending entry: the turn ended (abort or
|
|
295
|
+
// completion) so no late result is coming. Set via `seal()`.
|
|
296
|
+
#sealed = false;
|
|
92
297
|
|
|
93
298
|
constructor(options: ReadToolGroupOptions = {}) {
|
|
94
299
|
super();
|
|
@@ -99,13 +304,36 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
99
304
|
}
|
|
100
305
|
|
|
101
306
|
isTranscriptBlockFinalized(): boolean {
|
|
102
|
-
|
|
307
|
+
if (this.#sealed) return true;
|
|
308
|
+
if (!this.#finalized) return false;
|
|
309
|
+
// Closed to new entries, but a still-pending entry means its result is in
|
|
310
|
+
// flight — parallel reads can finalize the group (a sibling tool starts and
|
|
311
|
+
// breaks the run) before a read's `tool_execution_end` lands. Stay live so
|
|
312
|
+
// the late result repaints instead of freezing the pending preview into
|
|
313
|
+
// native scrollback on ED3-risk terminals (#issue: stuck "Read <path>").
|
|
314
|
+
return !this.#hasPendingEntries();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#hasPendingEntries(): boolean {
|
|
318
|
+
for (const entry of this.#entries.values()) {
|
|
319
|
+
if (entry.status === "pending") return true;
|
|
320
|
+
}
|
|
321
|
+
return false;
|
|
103
322
|
}
|
|
104
323
|
|
|
105
324
|
finalize(): void {
|
|
106
325
|
this.#finalized = true;
|
|
107
326
|
}
|
|
108
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Force the group terminal even if an entry never received its result (the
|
|
330
|
+
* turn aborted or ended). Lets it freeze and stop pinning the transcript live
|
|
331
|
+
* region instead of lingering on a pending preview until the next thaw.
|
|
332
|
+
*/
|
|
333
|
+
seal(): void {
|
|
334
|
+
this.#sealed = true;
|
|
335
|
+
}
|
|
336
|
+
|
|
109
337
|
updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
|
|
110
338
|
if (!toolCallId) return;
|
|
111
339
|
const basePath = args.file_path || args.path || "";
|
|
@@ -131,11 +359,15 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
131
359
|
if (isPartial) return;
|
|
132
360
|
const details = result.details as ReadToolResultDetails | undefined;
|
|
133
361
|
const suffixResolution = getSuffixResolution(details);
|
|
362
|
+
const displayPaths = getDisplayReadTargets(details);
|
|
363
|
+
entry.linkPath = readResultLinkPath(details);
|
|
134
364
|
if (suffixResolution) {
|
|
135
|
-
entry.path = suffixResolution
|
|
365
|
+
entry.path = displayPathWithSuffixResolution(entry.path, suffixResolution);
|
|
136
366
|
entry.correctedFrom = suffixResolution.from;
|
|
367
|
+
entry.displayPaths = undefined;
|
|
137
368
|
} else {
|
|
138
369
|
entry.correctedFrom = undefined;
|
|
370
|
+
entry.displayPaths = displayPaths;
|
|
139
371
|
}
|
|
140
372
|
const conflictCount =
|
|
141
373
|
typeof details?.conflictCount === "number" && details.conflictCount > 0 ? details.conflictCount : undefined;
|
|
@@ -164,42 +396,42 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
164
396
|
|
|
165
397
|
#updateDisplay(): void {
|
|
166
398
|
const entries = [...this.#entries.values()];
|
|
399
|
+
const displayTargets = this.#displayTargetsForEntries(entries);
|
|
400
|
+
const displayRows = this.#buildSummaryRows(displayTargets);
|
|
167
401
|
|
|
168
402
|
// Clear previous children and rebuild the summary and preview blocks.
|
|
169
403
|
this.clear();
|
|
170
404
|
this.#text = new Text("", 0, 0);
|
|
171
405
|
|
|
172
|
-
if (
|
|
406
|
+
if (displayRows.length === 0) {
|
|
173
407
|
this.#text.setText(` ${theme.format.bullet} ${theme.fg("toolTitle", theme.bold("Read"))}`);
|
|
174
408
|
this.addChild(this.#text);
|
|
175
409
|
return;
|
|
176
410
|
}
|
|
177
411
|
|
|
178
|
-
if (
|
|
179
|
-
const
|
|
180
|
-
if (!this.#
|
|
181
|
-
const statusSymbol = this.#formatStatus(
|
|
182
|
-
const pathDisplay = this.#
|
|
412
|
+
if (displayRows.length === 1) {
|
|
413
|
+
const row = displayRows[0]!;
|
|
414
|
+
if (!this.#shouldRenderPreviewRow(row)) {
|
|
415
|
+
const statusSymbol = this.#formatStatus(this.#statusForTargets(row.targets));
|
|
416
|
+
const pathDisplay = this.#formatRowPath(row);
|
|
183
417
|
this.#text.setText(
|
|
184
418
|
` ${statusSymbol} ${theme.fg("toolTitle", theme.bold("Read"))} ${pathDisplay}`.trimEnd(),
|
|
185
419
|
);
|
|
186
420
|
this.addChild(this.#text);
|
|
187
421
|
}
|
|
188
|
-
|
|
422
|
+
for (const entry of this.#previewEntriesForRow(row)) {
|
|
189
423
|
this.#addContentPreview(entry);
|
|
190
424
|
}
|
|
191
425
|
return;
|
|
192
426
|
}
|
|
193
427
|
|
|
194
|
-
const header = `${theme.fg("toolTitle", theme.bold("Read"))}${theme.fg("dim", ` (${
|
|
428
|
+
const header = `${theme.fg("toolTitle", theme.bold("Read"))}${theme.fg("dim", ` (${displayRows.length})`)}`;
|
|
195
429
|
const lines = [` ${theme.format.bullet} ${header}`];
|
|
196
430
|
const entriesWithoutPreview = entries.filter(entry => !this.#shouldRenderPreview(entry));
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const pathDisplay = this.#formatPath(entry);
|
|
202
|
-
lines.push(` ${theme.fg("dim", connector)} ${statusPrefix}${pathDisplay}`.trimEnd());
|
|
431
|
+
const summaryTargets = this.#displayTargetsForEntries(entriesWithoutPreview);
|
|
432
|
+
const rows = this.#buildSummaryRows(summaryTargets);
|
|
433
|
+
for (const [index, row] of rows.entries()) {
|
|
434
|
+
this.#appendSummaryRow(lines, row, index, rows.length);
|
|
203
435
|
}
|
|
204
436
|
|
|
205
437
|
this.#text.setText(lines.join("\n"));
|
|
@@ -212,16 +444,177 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
212
444
|
}
|
|
213
445
|
}
|
|
214
446
|
|
|
447
|
+
#displayTargetsForEntries(entries: ReadEntry[]): ReadDisplayTarget[] {
|
|
448
|
+
const targets: ReadDisplayTarget[] = [];
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
const pathSpecs = entry.displayPaths ?? splitReadDisplayPathSpecs(entry.path);
|
|
451
|
+
const useEntryLinkPath = pathSpecs.length === 1;
|
|
452
|
+
for (const pathSpec of pathSpecs) {
|
|
453
|
+
const split = splitPathAndSel(pathSpec);
|
|
454
|
+
const linkPath = readTargetLinkPath(split.path, useEntryLinkPath ? entry.linkPath : undefined);
|
|
455
|
+
for (const selector of splitSelectorDisplayParts(split.sel)) {
|
|
456
|
+
targets.push({
|
|
457
|
+
entry,
|
|
458
|
+
targetPath: selector ? `${split.path}:${selector}` : pathSpec,
|
|
459
|
+
basePath: split.path,
|
|
460
|
+
linkPath,
|
|
461
|
+
selector,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return targets;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
#buildSummaryRows(targets: ReadDisplayTarget[]): ReadSummaryRow[] {
|
|
470
|
+
const selectorTargetsByBasePath = new Map<string, ReadDisplayTarget[]>();
|
|
471
|
+
for (const target of targets) {
|
|
472
|
+
if (!target.selector) continue;
|
|
473
|
+
const existing = selectorTargetsByBasePath.get(target.basePath);
|
|
474
|
+
if (existing) existing.push(target);
|
|
475
|
+
else selectorTargetsByBasePath.set(target.basePath, [target]);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const mergeableBasePaths = new Set<string>();
|
|
479
|
+
for (const [basePath, baseTargets] of selectorTargetsByBasePath) {
|
|
480
|
+
if (basePath && baseTargets.length > 1) {
|
|
481
|
+
mergeableBasePaths.add(basePath);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const emittedMergedRows = new Set<string>();
|
|
486
|
+
const rows: ReadSummaryRow[] = [];
|
|
487
|
+
for (const target of targets) {
|
|
488
|
+
if (target.selector && mergeableBasePaths.has(target.basePath)) {
|
|
489
|
+
if (!emittedMergedRows.has(target.basePath)) {
|
|
490
|
+
const mergedTargets = selectorTargetsByBasePath.get(target.basePath) ?? [target];
|
|
491
|
+
rows.push({
|
|
492
|
+
targetPath: `${target.basePath}:${formatMergedSelectorParts(
|
|
493
|
+
mergedTargets
|
|
494
|
+
.map(mergedTarget => mergedTarget.selector)
|
|
495
|
+
.filter(selector => selector !== undefined),
|
|
496
|
+
)}`,
|
|
497
|
+
basePath: target.basePath,
|
|
498
|
+
targets: mergedTargets,
|
|
499
|
+
});
|
|
500
|
+
emittedMergedRows.add(target.basePath);
|
|
501
|
+
}
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
rows.push({ targetPath: target.targetPath, basePath: target.basePath, targets: [target] });
|
|
505
|
+
}
|
|
506
|
+
return rows;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
#appendSummaryRow(lines: string[], row: ReadSummaryRow, index: number, total: number): void {
|
|
510
|
+
const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
|
|
511
|
+
lines.push(` ${theme.fg("dim", connector)} ${this.#formatRow(row)}`.trimEnd());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#formatRow(row: ReadSummaryRow): string {
|
|
515
|
+
const status = this.#statusForTargets(row.targets);
|
|
516
|
+
const statusPrefix = status === "success" ? "" : `${this.#formatStatus(status)} `;
|
|
517
|
+
return `${statusPrefix}${this.#formatRowPath(row)}`;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
#formatRowPath(row: ReadSummaryRow): string {
|
|
521
|
+
return this.#formatPathValue(row.targetPath, {
|
|
522
|
+
correctedFrom: this.#correctedFromForTargets(row.targets),
|
|
523
|
+
conflictCount: this.#conflictCountForTargets(row.targets),
|
|
524
|
+
line: firstSelectorLineForTargets(row.targets),
|
|
525
|
+
linkPath: linkPathForTargets(row.targets),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#statusForTargets(targets: ReadDisplayTarget[]): ReadEntry["status"] {
|
|
530
|
+
let status: ReadEntry["status"] = "success";
|
|
531
|
+
for (const target of targets) {
|
|
532
|
+
if (READ_STATUS_RANK[target.entry.status] > READ_STATUS_RANK[status]) {
|
|
533
|
+
status = target.entry.status;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return status;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
#correctedFromForTargets(targets: ReadDisplayTarget[]): string | undefined {
|
|
540
|
+
for (const target of targets) {
|
|
541
|
+
if (target.entry.correctedFrom) return target.entry.correctedFrom;
|
|
542
|
+
}
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
#conflictCountForTargets(targets: ReadDisplayTarget[]): number | undefined {
|
|
547
|
+
let conflictCount = 0;
|
|
548
|
+
for (const target of targets) {
|
|
549
|
+
if (target.entry.conflictCount && target.entry.conflictCount > conflictCount) {
|
|
550
|
+
conflictCount = target.entry.conflictCount;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return conflictCount > 0 ? conflictCount : undefined;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
#previewEntriesForRow(row: ReadSummaryRow): ReadEntry[] {
|
|
557
|
+
const entries: ReadEntry[] = [];
|
|
558
|
+
const seen = new Set<string>();
|
|
559
|
+
for (const target of row.targets) {
|
|
560
|
+
if (seen.has(target.entry.toolCallId) || !this.#shouldRenderPreview(target.entry)) continue;
|
|
561
|
+
entries.push(target.entry);
|
|
562
|
+
seen.add(target.entry.toolCallId);
|
|
563
|
+
}
|
|
564
|
+
return entries;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#shouldRenderPreviewRow(row: ReadSummaryRow): boolean {
|
|
568
|
+
return this.#previewEntriesForRow(row).length > 0;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
#formatPathValue(
|
|
572
|
+
value: string,
|
|
573
|
+
options: { correctedFrom?: string; conflictCount?: number; line?: number; linkPath?: string } = {},
|
|
574
|
+
): string {
|
|
575
|
+
const split = splitPathAndSel(value);
|
|
576
|
+
const selectorSuffix = split.sel ? `:${split.sel}` : "";
|
|
577
|
+
const baseValue = split.sel ? split.path : value;
|
|
578
|
+
const filePath = shortenPath(baseValue);
|
|
579
|
+
let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", "…");
|
|
580
|
+
if (filePath && options.linkPath) {
|
|
581
|
+
const linkOptions = options.line !== undefined ? { line: options.line } : undefined;
|
|
582
|
+
pathDisplay = fileHyperlink(options.linkPath, pathDisplay, linkOptions);
|
|
583
|
+
}
|
|
584
|
+
if (selectorSuffix) {
|
|
585
|
+
pathDisplay += theme.fg("accent", selectorSuffix);
|
|
586
|
+
}
|
|
587
|
+
if (options.correctedFrom) {
|
|
588
|
+
pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(options.correctedFrom)})`);
|
|
589
|
+
}
|
|
590
|
+
pathDisplay += this.#formatConflictBadge(options.conflictCount);
|
|
591
|
+
return pathDisplay;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
#formatConflictBadge(conflictCount: number | undefined): string {
|
|
595
|
+
if (!conflictCount || conflictCount <= 0) return "";
|
|
596
|
+
const n = conflictCount;
|
|
597
|
+
return ` ${theme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
598
|
+
}
|
|
599
|
+
|
|
215
600
|
/**
|
|
216
601
|
* Add a code-cell content preview below the entry summary.
|
|
217
602
|
* When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with a "… N more lines ⟨<key>: Expand⟩" hint.
|
|
218
603
|
* When expanded: shows full content.
|
|
219
604
|
*/
|
|
220
605
|
#addContentPreview(entry: ReadEntry): void {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const
|
|
606
|
+
const split = splitPathAndSel(entry.path);
|
|
607
|
+
const lang = getLanguageFromPath(split.path);
|
|
608
|
+
const pathValue = shortenPath(entry.path);
|
|
609
|
+
const pathDisplay = pathValue
|
|
610
|
+
? this.#formatPathValue(entry.path, {
|
|
611
|
+
correctedFrom: entry.correctedFrom,
|
|
612
|
+
conflictCount: entry.conflictCount,
|
|
613
|
+
line: firstSelectorLine(split.sel),
|
|
614
|
+
linkPath: readTargetLinkPath(split.path, entry.linkPath),
|
|
615
|
+
})
|
|
616
|
+
: "";
|
|
617
|
+
const title = pathDisplay ? `Read ${pathDisplay}` : "Read";
|
|
225
618
|
let cachedWidth: number | undefined;
|
|
226
619
|
let cachedLines: string[] | undefined;
|
|
227
620
|
const expanded = this.#expanded;
|
|
@@ -255,19 +648,6 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
|
|
255
648
|
return this.#showContentPreview && entry.contentText !== undefined;
|
|
256
649
|
}
|
|
257
650
|
|
|
258
|
-
#formatPath(entry: ReadEntry): string {
|
|
259
|
-
const filePath = shortenPath(entry.path);
|
|
260
|
-
let pathDisplay = filePath ? theme.fg("accent", filePath) : theme.fg("toolOutput", "…");
|
|
261
|
-
if (entry.correctedFrom) {
|
|
262
|
-
pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(entry.correctedFrom)})`);
|
|
263
|
-
}
|
|
264
|
-
if (entry.conflictCount && entry.conflictCount > 0) {
|
|
265
|
-
const n = entry.conflictCount;
|
|
266
|
-
pathDisplay += ` ${theme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
|
|
267
|
-
}
|
|
268
|
-
return pathDisplay;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
651
|
#formatStatus(status: ReadEntry["status"]): string {
|
|
272
652
|
if (status === "success") {
|
|
273
653
|
return theme.fg("text", theme.status.enabled);
|