@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.3
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 +113 -1
- package/dist/types/cli/gallery-fixtures/types.d.ts +7 -1
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/edit/index.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/lsp/index.d.ts +0 -5
- package/dist/types/main.d.ts +14 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +0 -9
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- 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/status-line.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +0 -18
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/messages.d.ts +11 -8
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/session/yield-queue.d.ts +10 -1
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval-render.d.ts +0 -1
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/index.d.ts +31 -0
- package/dist/types/tools/path-utils.d.ts +13 -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/search.d.ts +2 -2
- package/dist/types/tools/write.d.ts +0 -2
- package/dist/types/tools/yield.d.ts +8 -0
- 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/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- 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/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/agentic/agent.ts +1 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/diff.ts +86 -0
- package/src/edit/hashline/execute.ts +14 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/edit/index.ts +31 -17
- package/src/edit/renderer.ts +116 -31
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/js/shared/prelude.txt +26 -10
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +189 -61
- package/src/main.ts +144 -78
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/assistant-message.ts +3 -15
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/late-diagnostics-message.ts +60 -0
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- 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/status-line.ts +19 -4
- 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/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +32 -1
- package/src/modes/controllers/input-controller.ts +56 -9
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +7 -0
- package/src/modes/utils/ui-helpers.ts +26 -5
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/manual-continue.md +7 -0
- package/src/prompts/system/plan-mode-active.md +56 -72
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/lsp-late-diagnostic.md +8 -0
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +85 -10
- package/src/session/agent-session.ts +42 -15
- package/src/session/auth-storage.ts +2 -0
- package/src/session/messages.ts +21 -14
- package/src/session/session-manager.ts +98 -25
- package/src/session/yield-queue.ts +20 -2
- package/src/task/executor.ts +72 -36
- package/src/task/render.ts +3 -4
- package/src/tiny/title-client.ts +6 -1
- package/src/tools/bash.ts +7 -7
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval-render.ts +4 -23
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +148 -99
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +47 -24
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +41 -20
- package/src/tools/render-utils.ts +3 -1
- package/src/tools/renderers.ts +0 -15
- package/src/tools/search.ts +38 -3
- package/src/tools/ssh.ts +0 -1
- package/src/tools/todo.ts +1 -0
- package/src/tools/write.ts +5 -14
- package/src/tools/yield.ts +10 -1
- 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/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
|
@@ -141,6 +141,9 @@ export class PlanReviewOverlay implements Component {
|
|
|
141
141
|
#bodyClickRows = new Set<number>();
|
|
142
142
|
/** 1-based column at/under which a region-row click targets the sidebar. */
|
|
143
143
|
#sidebarClickMaxCol = 0;
|
|
144
|
+
/** Option index the pointer is currently hovering, or undefined. Updated from
|
|
145
|
+
* motion mouse reports and cleared when the pointer leaves the option rows. */
|
|
146
|
+
#hoveredOption: number | undefined;
|
|
144
147
|
|
|
145
148
|
#annotating = false;
|
|
146
149
|
#input: Input;
|
|
@@ -315,9 +318,10 @@ export class PlanReviewOverlay implements Component {
|
|
|
315
318
|
* Hit-test an SGR mouse report (`\x1b[<b;x;yM/m`) against the click maps the
|
|
316
319
|
* last render recorded. Returns true when consumed. The fullscreen overlay
|
|
317
320
|
* paints from screen row 0, so a 1-based mouse row maps directly to the
|
|
318
|
-
* rendered-line index. Wheel scrolls the body;
|
|
319
|
-
*
|
|
320
|
-
* the body column focuses
|
|
321
|
+
* rendered-line index. Wheel scrolls the body; pointer motion lights up the
|
|
322
|
+
* hovered option row; a left click on an option activates it (select +
|
|
323
|
+
* confirm), on a ToC row jumps to that section, and on the body column focuses
|
|
324
|
+
* the body.
|
|
321
325
|
*/
|
|
322
326
|
#handleMouse(data: string): boolean {
|
|
323
327
|
const match = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/.exec(data);
|
|
@@ -331,7 +335,13 @@ export class PlanReviewOverlay implements Component {
|
|
|
331
335
|
return true;
|
|
332
336
|
}
|
|
333
337
|
if (match[4] !== "M") return true; // release
|
|
334
|
-
if (button & 32)
|
|
338
|
+
if (button & 32) {
|
|
339
|
+
// Motion (hover or drag): light up the option row under the pointer so a
|
|
340
|
+
// mouse user gets the same affordance the keyboard cursor gives. Any
|
|
341
|
+
// non-option row clears the highlight.
|
|
342
|
+
this.#setHoveredOption(this.#optionClickRows.get(row));
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
335
345
|
if ((button & 3) !== 0) return true; // not the left button
|
|
336
346
|
const optionIndex = this.#optionClickRows.get(row);
|
|
337
347
|
if (optionIndex !== undefined) {
|
|
@@ -355,6 +365,12 @@ export class PlanReviewOverlay implements Component {
|
|
|
355
365
|
return true;
|
|
356
366
|
}
|
|
357
367
|
|
|
368
|
+
/** Set the hovered option from a hit-tested row, ignoring disabled rows and
|
|
369
|
+
* non-option rows (both clear the highlight). */
|
|
370
|
+
#setHoveredOption(index: number | undefined): void {
|
|
371
|
+
this.#hoveredOption = index !== undefined && !this.#disabled.has(index) ? index : undefined;
|
|
372
|
+
}
|
|
373
|
+
|
|
358
374
|
#cycleRegion(direction: number): void {
|
|
359
375
|
// Sidebar is skipped from the cycle when it is not shown.
|
|
360
376
|
const regions: Focus[] = this.#sidebarShown ? ["toc", "body", "actions"] : ["body", "actions"];
|
|
@@ -611,14 +627,19 @@ export class PlanReviewOverlay implements Component {
|
|
|
611
627
|
return this.#options.map((label, i) => {
|
|
612
628
|
const selected = i === this.#selectedIndex;
|
|
613
629
|
const isDisabled = this.#disabled.has(i);
|
|
630
|
+
const hovered = !isDisabled && i === this.#hoveredOption;
|
|
614
631
|
// The cursor marks the selected option; it dims when actions are not the
|
|
615
632
|
// focused region so the active region's highlight stays unambiguous.
|
|
616
633
|
const cursor = selected ? theme.fg(active ? "accent" : "dim", `${theme.nav.cursor} `) : " ";
|
|
617
|
-
|
|
634
|
+
let text = isDisabled
|
|
618
635
|
? theme.fg("dim", label)
|
|
619
636
|
: selected && active
|
|
620
637
|
? theme.bold(theme.fg("accent", label))
|
|
621
638
|
: theme.fg("text", label);
|
|
639
|
+
// A pointer hovering an option paints a highlight band behind its label,
|
|
640
|
+
// distinct from the keyboard selection (cursor glyph + bold accent) which
|
|
641
|
+
// stays where it is. One space of padding gives the band a button shape.
|
|
642
|
+
if (hovered) text = theme.bg("selectedBg", ` ${text} `);
|
|
622
643
|
return cursor + text;
|
|
623
644
|
});
|
|
624
645
|
}
|
|
@@ -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);
|