@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.5
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 +86 -43
- package/dist/cli.js +3540 -3521
- package/dist/types/advisor/advise-tool.d.ts +3 -0
- package/dist/types/advisor/runtime.d.ts +7 -1
- package/dist/types/async/job-manager.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/models-config-schema.d.ts +10 -0
- package/dist/types/config/models-config.d.ts +6 -0
- package/dist/types/dap/client.d.ts +10 -0
- package/dist/types/dap/types.d.ts +6 -5
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +16 -0
- package/dist/types/launch/spawn-options.d.ts +10 -0
- package/dist/types/launch/spawn-options.test.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +2 -3
- package/dist/types/modes/components/status-line/types.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -3
- package/dist/types/modes/components/tree-selector.d.ts +6 -2
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/print-mode.d.ts +4 -0
- package/dist/types/modes/rpc/rpc-input.d.ts +5 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +1 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/tools/browser/aria/aria-snapshot.d.ts +17 -7
- package/dist/types/tools/browser/cmux/socket-client.d.ts +2 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/gh.d.ts +5 -3
- package/dist/types/tools/hub/index.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +1 -0
- package/dist/types/tools/hub/types.d.ts +2 -0
- package/dist/types/tools/tool-timeouts.d.ts +1 -1
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/web/search/providers/codex.d.ts +1 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -0
- package/src/advisor/advise-tool.ts +4 -0
- package/src/advisor/runtime.ts +38 -14
- package/src/async/job-manager.ts +3 -0
- package/src/cli/bench-cli.ts +8 -2
- package/src/cli/dry-balance-cli.ts +1 -0
- package/src/config/model-registry.ts +89 -8
- package/src/config/model-resolver.ts +78 -14
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings.ts +3 -1
- package/src/dap/client.ts +168 -1
- package/src/dap/config.ts +51 -1
- package/src/dap/session.ts +575 -234
- package/src/dap/types.ts +6 -5
- package/src/discovery/agents.ts +2 -2
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/launch/broker.ts +34 -31
- package/src/launch/client.ts +7 -1
- package/src/launch/spawn-options.test.ts +31 -0
- package/src/launch/spawn-options.ts +17 -0
- package/src/lsp/types.ts +5 -1
- package/src/main.ts +17 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/status-line/component.ts +2 -2
- package/src/modes/components/status-line/segments.ts +20 -3
- package/src/modes/components/status-line/types.ts +3 -1
- package/src/modes/components/tool-execution.ts +5 -0
- package/src/modes/components/transcript-container.ts +23 -122
- package/src/modes/components/tree-selector.ts +10 -4
- package/src/modes/controllers/event-controller.ts +1 -2
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +40 -8
- package/src/modes/noninteractive-dispose.test.ts +12 -1
- package/src/modes/print-mode.ts +21 -9
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +7 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +3 -1
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +3 -2
- package/src/prompts/tools/browser.md +3 -2
- package/src/prompts/tools/debug.md +2 -7
- package/src/prompts/tools/github.md +6 -1
- package/src/prompts/tools/hub.md +4 -2
- package/src/prompts/tools/task-async-contract.md +1 -0
- package/src/prompts/tools/task.md +9 -2
- package/src/sdk.ts +38 -6
- package/src/session/agent-session.ts +395 -162
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +59 -33
- package/src/task/index.ts +46 -9
- package/src/task/worktree.ts +10 -0
- package/src/tools/browser/aria/aria-snapshot.ts +36 -8
- package/src/tools/browser/cmux/cmux-tab.ts +2 -1
- package/src/tools/browser/cmux/socket-client.ts +139 -3
- package/src/tools/browser/run-cancellation.ts +4 -0
- package/src/tools/browser/tab-protocol.ts +2 -0
- package/src/tools/browser/tab-supervisor.ts +21 -11
- package/src/tools/browser/tab-worker.ts +199 -33
- package/src/tools/debug.ts +3 -0
- package/src/tools/gh.ts +40 -2
- package/src/tools/hub/index.ts +4 -1
- package/src/tools/hub/jobs.ts +42 -1
- package/src/tools/hub/types.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -1
- package/src/utils/git.ts +237 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Markdown,
|
|
5
5
|
type MarkdownTheme,
|
|
6
6
|
matchesKey,
|
|
7
|
-
padding,
|
|
8
7
|
renderInlineMarkdown,
|
|
9
8
|
replaceTabs,
|
|
10
9
|
ScrollView,
|
|
@@ -13,7 +12,6 @@ import {
|
|
|
13
12
|
Text,
|
|
14
13
|
type TUI,
|
|
15
14
|
truncateToWidth,
|
|
16
|
-
visibleWidth,
|
|
17
15
|
wrapTextWithAnsi,
|
|
18
16
|
} from "@oh-my-pi/pi-tui";
|
|
19
17
|
import type {
|
|
@@ -23,8 +21,15 @@ import type {
|
|
|
23
21
|
} from "../../extensibility/extensions";
|
|
24
22
|
import { getTabBarTheme } from "../shared";
|
|
25
23
|
import { getMarkdownTheme, highlightCode, theme } from "../theme/theme";
|
|
26
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
matchesSelectCancel,
|
|
26
|
+
matchesSelectDown,
|
|
27
|
+
matchesSelectPageDown,
|
|
28
|
+
matchesSelectPageUp,
|
|
29
|
+
matchesSelectUp,
|
|
30
|
+
} from "../utils/keybinding-matchers";
|
|
27
31
|
import { CountdownTimer } from "./countdown-timer";
|
|
32
|
+
import { editorKey } from "./keybinding-hints";
|
|
28
33
|
import { bottomBorder, divider, row, topBorder } from "./overlay-box";
|
|
29
34
|
import { handleTabSwitchKey } from "./selector-helpers";
|
|
30
35
|
|
|
@@ -38,9 +43,6 @@ const SUBMIT_OPTION = "Submit";
|
|
|
38
43
|
const DIALOG_HEIGHT_RATIO = 0.7;
|
|
39
44
|
const MIN_DIALOG_ROWS = 12;
|
|
40
45
|
const MIN_BODY_ROWS = 5;
|
|
41
|
-
const PREVIEW_MIN_WIDTH = 40;
|
|
42
|
-
const SIDE_BY_SIDE_LIST_MIN_WIDTH = 30;
|
|
43
|
-
const SIDE_BY_SIDE_GAP_WIDTH = 3;
|
|
44
46
|
const MAX_HEADER_CHIP_WIDTH = 16;
|
|
45
47
|
/** Maximum number of title lines shown in the prompt editor overlay, so a
|
|
46
48
|
* long or multi-line question cannot push the input row off-screen. Mirrors
|
|
@@ -90,6 +92,7 @@ interface QuestionState {
|
|
|
90
92
|
noteRowKey: string | undefined;
|
|
91
93
|
cursorIndex: number;
|
|
92
94
|
scrollOffset: number;
|
|
95
|
+
manualScroll: boolean;
|
|
93
96
|
timedOut: boolean;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -114,6 +117,8 @@ interface PreviewSegment {
|
|
|
114
117
|
language: string | undefined;
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
type PreviewRenderCache = Map<string, Map<number, readonly string[]>>;
|
|
121
|
+
|
|
117
122
|
function clamp(value: number, min: number, max: number): number {
|
|
118
123
|
return Math.max(min, Math.min(value, max));
|
|
119
124
|
}
|
|
@@ -211,6 +216,31 @@ function renderPreviewContent(preview: string, width: number): string[] {
|
|
|
211
216
|
return out;
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
function renderCachedPreview(cache: PreviewRenderCache, preview: string, width: number): readonly string[] {
|
|
220
|
+
let byWidth = cache.get(preview);
|
|
221
|
+
if (!byWidth) {
|
|
222
|
+
byWidth = new Map();
|
|
223
|
+
cache.set(preview, byWidth);
|
|
224
|
+
}
|
|
225
|
+
let rendered = byWidth.get(width);
|
|
226
|
+
if (!rendered) {
|
|
227
|
+
rendered = renderPreviewContent(preview, width).map(line => ` ${theme.fg("border", "│")} ${line}`);
|
|
228
|
+
byWidth.set(width, rendered);
|
|
229
|
+
}
|
|
230
|
+
return rendered;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function pageKeysLabel(): string {
|
|
234
|
+
const pageUp = editorKey("tui.select.pageUp");
|
|
235
|
+
const pageDown = editorKey("tui.select.pageDown");
|
|
236
|
+
return `${pageUp === "pageup" ? "PgUp" : pageUp}/${pageDown === "pagedown" ? "PgDn" : pageDown}`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function cancelKeyLabel(): string {
|
|
240
|
+
const [key = ""] = editorKey("tui.select.cancel").split("/");
|
|
241
|
+
return key === "escape" ? "Esc" : key;
|
|
242
|
+
}
|
|
243
|
+
|
|
214
244
|
function normalizedInlineInput(input: string): string {
|
|
215
245
|
return replaceTabs(input).replace(/\s+/g, " ").trim();
|
|
216
246
|
}
|
|
@@ -260,6 +290,7 @@ function renderRowLabel(
|
|
|
260
290
|
state: QuestionState,
|
|
261
291
|
selected: boolean,
|
|
262
292
|
mdTheme: MarkdownTheme,
|
|
293
|
+
previewCache: PreviewRenderCache,
|
|
263
294
|
width: number,
|
|
264
295
|
): string[] {
|
|
265
296
|
const isOption = rowItem.kind === "option";
|
|
@@ -283,6 +314,10 @@ function renderRowLabel(
|
|
|
283
314
|
lines.push(` ${truncateToWidth(line, Math.max(1, width - 6), Ellipsis.Unicode)}`);
|
|
284
315
|
}
|
|
285
316
|
}
|
|
317
|
+
if (option?.preview?.trim()) {
|
|
318
|
+
const previewWidth = Math.max(1, width - 8);
|
|
319
|
+
lines.push(...renderCachedPreview(previewCache, option.preview, previewWidth));
|
|
320
|
+
}
|
|
286
321
|
}
|
|
287
322
|
if (isOther && state.customInput !== undefined) {
|
|
288
323
|
const preview = replaceTabs(state.customInput).replace(/\s+/g, " ").trim();
|
|
@@ -295,6 +330,8 @@ export class AskDialogComponent implements Component {
|
|
|
295
330
|
#states: QuestionState[];
|
|
296
331
|
#activeTabIndex = 0;
|
|
297
332
|
#submitScrollOffset = 0;
|
|
333
|
+
#bodyRows = MIN_BODY_ROWS;
|
|
334
|
+
#questionCanPage = false;
|
|
298
335
|
#remainingSeconds: number | undefined;
|
|
299
336
|
#countdown: CountdownTimer | undefined;
|
|
300
337
|
#promptActive = false;
|
|
@@ -302,6 +339,8 @@ export class AskDialogComponent implements Component {
|
|
|
302
339
|
#closed = false;
|
|
303
340
|
#tabBar: TabBar | undefined;
|
|
304
341
|
#stableHeight: { key: string; total: number } | undefined;
|
|
342
|
+
#previewCache: PreviewRenderCache = new Map();
|
|
343
|
+
#overflowLayouts = new WeakMap<ExtensionAskDialogQuestion, Set<string>>();
|
|
305
344
|
|
|
306
345
|
constructor(
|
|
307
346
|
private readonly questions: ExtensionAskDialogQuestion[],
|
|
@@ -318,6 +357,7 @@ export class AskDialogComponent implements Component {
|
|
|
318
357
|
noteRowKey: undefined,
|
|
319
358
|
cursorIndex: clamp(recommended ?? 0, 0, maxIndex),
|
|
320
359
|
scrollOffset: 0,
|
|
360
|
+
manualScroll: false,
|
|
321
361
|
timedOut: false,
|
|
322
362
|
};
|
|
323
363
|
});
|
|
@@ -335,6 +375,8 @@ export class AskDialogComponent implements Component {
|
|
|
335
375
|
|
|
336
376
|
invalidate(): void {
|
|
337
377
|
this.#stableHeight = undefined;
|
|
378
|
+
this.#previewCache.clear();
|
|
379
|
+
this.#overflowLayouts = new WeakMap();
|
|
338
380
|
this.#tabBar?.invalidate();
|
|
339
381
|
}
|
|
340
382
|
|
|
@@ -377,6 +419,7 @@ export class AskDialogComponent implements Component {
|
|
|
377
419
|
// (PRRT_kwDOQxs0bc6OFbDY).
|
|
378
420
|
const fixedRows = 1 + headerLines.length + 1 + 1 + 1 + 1;
|
|
379
421
|
const bodyRows = Math.max(MIN_BODY_ROWS, totalRows - fixedRows);
|
|
422
|
+
this.#bodyRows = bodyRows;
|
|
380
423
|
const bodyLines = this.#isSubmitTab()
|
|
381
424
|
? this.#renderSubmitBody(innerWidth, bodyRows)
|
|
382
425
|
: this.#renderQuestionBody(innerWidth, bodyRows);
|
|
@@ -419,22 +462,11 @@ export class AskDialogComponent implements Component {
|
|
|
419
462
|
const listRows = (listWidth: number): number => {
|
|
420
463
|
let total = 0;
|
|
421
464
|
for (const rowItem of rowItems) {
|
|
422
|
-
total += renderRowLabel(rowItem, question, state, false, mdTheme, listWidth).length;
|
|
465
|
+
total += renderRowLabel(rowItem, question, state, false, mdTheme, this.#previewCache, listWidth).length;
|
|
423
466
|
}
|
|
424
467
|
return total;
|
|
425
468
|
};
|
|
426
|
-
|
|
427
|
-
const previews = question.options.filter(option => option.preview?.trim());
|
|
428
|
-
const sideBySide = width >= SIDE_BY_SIDE_LIST_MIN_WIDTH + PREVIEW_MIN_WIDTH + SIDE_BY_SIDE_GAP_WIDTH;
|
|
429
|
-
if (previews.length > 0 && sideBySide) {
|
|
430
|
-
const previewWidth = Math.max(PREVIEW_MIN_WIDTH, Math.floor(width * 0.45));
|
|
431
|
-
const listWidth = Math.max(1, width - previewWidth - SIDE_BY_SIDE_GAP_WIDTH);
|
|
432
|
-
let pane = 0;
|
|
433
|
-
for (const option of previews) {
|
|
434
|
-
pane = Math.max(pane, renderPreviewContent(option.preview ?? "", Math.max(1, previewWidth - 2)).length);
|
|
435
|
-
}
|
|
436
|
-
body = Math.max(body, listRows(listWidth), pane);
|
|
437
|
-
}
|
|
469
|
+
const body = listRows(width);
|
|
438
470
|
needed = Math.max(needed, chrome + headerRows + Math.max(MIN_BODY_ROWS, body));
|
|
439
471
|
}
|
|
440
472
|
if (this.#hasSubmitTab()) {
|
|
@@ -499,14 +531,19 @@ export class AskDialogComponent implements Component {
|
|
|
499
531
|
}
|
|
500
532
|
|
|
501
533
|
#footerHintText(indicator: string): string {
|
|
502
|
-
const
|
|
534
|
+
const cancel = `${cancelKeyLabel()} cancel`;
|
|
503
535
|
if (this.#isSubmitTab()) {
|
|
504
|
-
|
|
536
|
+
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
537
|
+
return `Enter submit · ↑/↓ scroll ·${scroll} ${cancel}`;
|
|
505
538
|
}
|
|
506
539
|
const question = this.questions[this.#currentQuestionIndex()];
|
|
507
540
|
const action = question?.multi ? "Space/Enter toggle · n note" : "Enter select · n note";
|
|
508
|
-
const tabs = this.#hasSubmitTab() ? " · Tab/←/→
|
|
509
|
-
|
|
541
|
+
const tabs = this.#hasSubmitTab() ? " · Tab/←/→" : "";
|
|
542
|
+
if (this.#questionCanPage && indicator) {
|
|
543
|
+
return `${action} · ↑/↓${tabs} · ${cancel} · ${pageKeysLabel()} ${indicator}`;
|
|
544
|
+
}
|
|
545
|
+
const scroll = indicator ? ` ${indicator} scroll ·` : "";
|
|
546
|
+
return `${action} · ↑/↓ move${tabs} ·${scroll} ${cancel}`;
|
|
510
547
|
}
|
|
511
548
|
|
|
512
549
|
#questionRows(question: ExtensionAskDialogQuestion): QuestionRow[] {
|
|
@@ -536,13 +573,27 @@ export class AskDialogComponent implements Component {
|
|
|
536
573
|
if (!active) return;
|
|
537
574
|
const { question, state } = active;
|
|
538
575
|
const rows = this.#questionRows(question);
|
|
576
|
+
if (matchesSelectPageUp(keyData)) {
|
|
577
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - Math.max(1, this.#bodyRows - 1));
|
|
578
|
+
state.manualScroll = true;
|
|
579
|
+
this.#requestRender();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (matchesSelectPageDown(keyData)) {
|
|
583
|
+
state.scrollOffset += Math.max(1, this.#bodyRows - 1);
|
|
584
|
+
state.manualScroll = true;
|
|
585
|
+
this.#requestRender();
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
539
588
|
if (matchesSelectUp(keyData)) {
|
|
540
589
|
state.cursorIndex = clamp(state.cursorIndex - 1, 0, Math.max(0, rows.length - 1));
|
|
590
|
+
state.manualScroll = false;
|
|
541
591
|
this.#requestRender();
|
|
542
592
|
return;
|
|
543
593
|
}
|
|
544
594
|
if (matchesSelectDown(keyData)) {
|
|
545
595
|
state.cursorIndex = clamp(state.cursorIndex + 1, 0, Math.max(0, rows.length - 1));
|
|
596
|
+
state.manualScroll = false;
|
|
546
597
|
this.#requestRender();
|
|
547
598
|
return;
|
|
548
599
|
}
|
|
@@ -672,33 +723,7 @@ export class AskDialogComponent implements Component {
|
|
|
672
723
|
const { question, state } = active;
|
|
673
724
|
const rowItems = this.#questionRows(question);
|
|
674
725
|
state.cursorIndex = clamp(state.cursorIndex, 0, Math.max(0, rowItems.length - 1));
|
|
675
|
-
|
|
676
|
-
const preview =
|
|
677
|
-
selectedRow?.kind === "option" ? question.options[selectedRow.optionIndex ?? -1]?.preview : undefined;
|
|
678
|
-
// The preview pane exists only while the highlighted option carries a
|
|
679
|
-
// preview; otherwise the list takes the full dialog width.
|
|
680
|
-
if (!preview?.trim()) return this.#renderQuestionList(question, state, rowItems, width, maxRows);
|
|
681
|
-
const sideBySide = width >= SIDE_BY_SIDE_LIST_MIN_WIDTH + PREVIEW_MIN_WIDTH + SIDE_BY_SIDE_GAP_WIDTH;
|
|
682
|
-
if (sideBySide) {
|
|
683
|
-
const previewWidth = Math.max(PREVIEW_MIN_WIDTH, Math.floor(width * 0.45));
|
|
684
|
-
const listWidth = Math.max(1, width - previewWidth - SIDE_BY_SIDE_GAP_WIDTH);
|
|
685
|
-
const list = this.#renderQuestionList(question, state, rowItems, listWidth, maxRows);
|
|
686
|
-
const previewLines = this.#renderPreviewPane(preview, previewWidth, maxRows);
|
|
687
|
-
const lines: string[] = [];
|
|
688
|
-
for (let index = 0; index < maxRows; index++) {
|
|
689
|
-
const left = truncateToWidth(list.lines[index] ?? "", listWidth, Ellipsis.Unicode);
|
|
690
|
-
const right = truncateToWidth(previewLines[index] ?? "", previewWidth, Ellipsis.Unicode);
|
|
691
|
-
const gap = padding(Math.max(1, listWidth - visibleWidth(left)) + 1);
|
|
692
|
-
lines.push(`${left}${gap}${theme.fg("border", "│")} ${right}`);
|
|
693
|
-
}
|
|
694
|
-
return { lines, scrollOffset: list.scrollOffset, indicator: list.indicator };
|
|
695
|
-
}
|
|
696
|
-
const previewLines = this.#renderPreviewPane(preview, width, Math.max(3, Math.min(8, Math.floor(maxRows * 0.4))));
|
|
697
|
-
const listRows = Math.max(3, maxRows - previewLines.length - 1);
|
|
698
|
-
const list = this.#renderQuestionList(question, state, rowItems, width, listRows);
|
|
699
|
-
const lines = [...list.lines, theme.fg("border", "─".repeat(Math.max(1, width))), ...previewLines];
|
|
700
|
-
while (lines.length < maxRows) lines.push("");
|
|
701
|
-
return { lines: lines.slice(0, maxRows), scrollOffset: list.scrollOffset, indicator: list.indicator };
|
|
726
|
+
return this.#renderQuestionList(question, state, rowItems, width, maxRows);
|
|
702
727
|
}
|
|
703
728
|
|
|
704
729
|
#renderQuestionList(
|
|
@@ -709,16 +734,51 @@ export class AskDialogComponent implements Component {
|
|
|
709
734
|
rows: number,
|
|
710
735
|
): RenderedList {
|
|
711
736
|
const mdTheme = getMarkdownTheme();
|
|
712
|
-
const allLines: string[]
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
737
|
+
const renderRows = (contentWidth: number): { allLines: string[]; lineStartByRow: number[] } => {
|
|
738
|
+
const allLines: string[] = [];
|
|
739
|
+
const lineStartByRow: number[] = [];
|
|
740
|
+
for (let index = 0; index < rowItems.length; index++) {
|
|
741
|
+
lineStartByRow.push(allLines.length);
|
|
742
|
+
const rowItem = rowItems[index];
|
|
743
|
+
if (!rowItem) continue;
|
|
744
|
+
allLines.push(
|
|
745
|
+
...renderRowLabel(
|
|
746
|
+
rowItem,
|
|
747
|
+
question,
|
|
748
|
+
state,
|
|
749
|
+
index === state.cursorIndex,
|
|
750
|
+
mdTheme,
|
|
751
|
+
this.#previewCache,
|
|
752
|
+
contentWidth,
|
|
753
|
+
),
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return { allLines, lineStartByRow };
|
|
757
|
+
};
|
|
758
|
+
const layoutKey = `${width}:${rows}:${state.customInput === undefined ? 0 : 1}`;
|
|
759
|
+
let overflowLayouts = this.#overflowLayouts.get(question);
|
|
760
|
+
const knownOverflow = overflowLayouts?.has(layoutKey) ?? false;
|
|
761
|
+
let renderedRows = renderRows(knownOverflow && width > 1 ? width - 1 : width);
|
|
762
|
+
if (!knownOverflow && width > 1 && renderedRows.allLines.length > rows) {
|
|
763
|
+
if (!overflowLayouts) {
|
|
764
|
+
overflowLayouts = new Set();
|
|
765
|
+
this.#overflowLayouts.set(question, overflowLayouts);
|
|
766
|
+
}
|
|
767
|
+
overflowLayouts.add(layoutKey);
|
|
768
|
+
renderedRows = renderRows(width - 1);
|
|
719
769
|
}
|
|
770
|
+
const { allLines, lineStartByRow } = renderedRows;
|
|
720
771
|
const cursorStart = lineStartByRow[state.cursorIndex] ?? 0;
|
|
721
|
-
|
|
772
|
+
const cursorEnd = lineStartByRow[state.cursorIndex + 1] ?? allLines.length;
|
|
773
|
+
this.#questionCanPage = cursorEnd - cursorStart > rows;
|
|
774
|
+
state.scrollOffset = this.#scrollOffsetForCursor(
|
|
775
|
+
state.scrollOffset,
|
|
776
|
+
cursorStart,
|
|
777
|
+
cursorEnd,
|
|
778
|
+
rows,
|
|
779
|
+
allLines.length,
|
|
780
|
+
state.manualScroll,
|
|
781
|
+
);
|
|
722
782
|
const scrollView = new ScrollView(allLines, {
|
|
723
783
|
height: rows,
|
|
724
784
|
scrollbar: "auto",
|
|
@@ -734,15 +794,6 @@ export class AskDialogComponent implements Component {
|
|
|
734
794
|
};
|
|
735
795
|
}
|
|
736
796
|
|
|
737
|
-
#renderPreviewPane(preview: string, width: number, maxRows: number): string[] {
|
|
738
|
-
const bodyWidth = Math.max(1, width - 2);
|
|
739
|
-
const content = renderPreviewContent(preview, bodyWidth);
|
|
740
|
-
if (content.length <= maxRows) return content;
|
|
741
|
-
const visibleCount = Math.max(1, maxRows - 1);
|
|
742
|
-
const hidden = content.length - visibleCount;
|
|
743
|
-
return [...content.slice(0, visibleCount), theme.fg("dim", `… ${hidden} more lines`)];
|
|
744
|
-
}
|
|
745
|
-
|
|
746
797
|
#renderSubmitBody(width: number, rows: number): RenderedList {
|
|
747
798
|
const allLines: string[] = [];
|
|
748
799
|
const unanswered = this.#unansweredCount();
|
|
@@ -789,12 +840,25 @@ export class AskDialogComponent implements Component {
|
|
|
789
840
|
};
|
|
790
841
|
}
|
|
791
842
|
|
|
792
|
-
#scrollOffsetForCursor(
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
843
|
+
#scrollOffsetForCursor(
|
|
844
|
+
currentOffset: number,
|
|
845
|
+
cursorStart: number,
|
|
846
|
+
cursorEnd: number,
|
|
847
|
+
rows: number,
|
|
848
|
+
totalRows: number,
|
|
849
|
+
manualScroll: boolean,
|
|
850
|
+
): number {
|
|
851
|
+
const maxOffset = Math.max(0, totalRows - rows);
|
|
852
|
+
if (maxOffset === 0) return 0;
|
|
853
|
+
let nextOffset = clamp(currentOffset, 0, maxOffset);
|
|
854
|
+
const cursorRows = cursorEnd - cursorStart;
|
|
855
|
+
if (manualScroll && cursorRows > rows) {
|
|
856
|
+
// A page must not expose another option while Enter still targets this one.
|
|
857
|
+
nextOffset = clamp(nextOffset, cursorStart, cursorEnd - rows);
|
|
858
|
+
} else if (cursorStart < nextOffset || cursorEnd > nextOffset + rows) {
|
|
859
|
+
nextOffset = cursorRows <= rows ? cursorEnd - rows : cursorStart;
|
|
860
|
+
}
|
|
861
|
+
return clamp(nextOffset, 0, maxOffset);
|
|
798
862
|
}
|
|
799
863
|
|
|
800
864
|
#clipIndicator(offset: number, rows: number, totalRows: number): string {
|
|
@@ -273,7 +273,7 @@ export class StatusLineComponent implements Component {
|
|
|
273
273
|
*/
|
|
274
274
|
#activeMeters: WeakMap<AgentSession, ActiveMeter> = new WeakMap();
|
|
275
275
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
276
|
-
#loopModeStatus:
|
|
276
|
+
#loopModeStatus: SegmentContext["loopMode"] = null;
|
|
277
277
|
#goalModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
278
278
|
#vibeModeStatus: { enabled: boolean } | null = null;
|
|
279
279
|
#collabStatus: CollabStatus | null = null;
|
|
@@ -496,7 +496,7 @@ export class StatusLineComponent implements Component {
|
|
|
496
496
|
this.#planModeStatus = status ?? null;
|
|
497
497
|
}
|
|
498
498
|
|
|
499
|
-
setLoopModeStatus(status:
|
|
499
|
+
setLoopModeStatus(status: NonNullable<SegmentContext["loopMode"]> | undefined): void {
|
|
500
500
|
this.#loopModeStatus = status ?? null;
|
|
501
501
|
}
|
|
502
502
|
|
|
@@ -210,6 +210,19 @@ function renderGoalMode(ctx: SegmentContext, mode: { enabled: boolean; paused: b
|
|
|
210
210
|
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
function formatLoopLimit(limit: NonNullable<SegmentContext["loopMode"]>["limit"]): string | undefined {
|
|
214
|
+
if (!limit) return undefined;
|
|
215
|
+
if (limit.kind === "iterations") return `${limit.remaining}/${limit.initial}`;
|
|
216
|
+
|
|
217
|
+
const totalSeconds = Math.max(0, Math.ceil((limit.deadlineMs - Date.now()) / 1_000));
|
|
218
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
219
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
220
|
+
const seconds = totalSeconds % 60;
|
|
221
|
+
if (hours > 0) return `${hours}h${minutes > 0 ? `${minutes}m` : ""} left`;
|
|
222
|
+
if (minutes > 0) return `${minutes}m${seconds > 0 ? `${seconds}s` : ""} left`;
|
|
223
|
+
return `${seconds}s left`;
|
|
224
|
+
}
|
|
225
|
+
|
|
213
226
|
const modeSegment: StatusLineSegment = {
|
|
214
227
|
id: "mode",
|
|
215
228
|
render(ctx) {
|
|
@@ -241,9 +254,13 @@ const modeSegment: StatusLineSegment = {
|
|
|
241
254
|
}
|
|
242
255
|
|
|
243
256
|
const loop = ctx.loopMode;
|
|
244
|
-
if (loop
|
|
245
|
-
const
|
|
246
|
-
|
|
257
|
+
if (loop) {
|
|
258
|
+
const icon = loop.state === "paused" ? theme.icon.pause || theme.icon.loop : theme.icon.loop;
|
|
259
|
+
const color: ThemeColor = loop.state === "paused" ? "warning" : "customMessageLabel";
|
|
260
|
+
const parts = [withIcon(icon, `Loop ${loop.state}`)];
|
|
261
|
+
const limit = formatLoopLimit(loop.limit);
|
|
262
|
+
if (limit) parts.push(limit);
|
|
263
|
+
return { content: theme.fg(color, parts.join(" ")), visible: true };
|
|
247
264
|
}
|
|
248
265
|
|
|
249
266
|
return { content: "", visible: false };
|
|
@@ -2,6 +2,7 @@ import type { CollabSessionState } from "../../../collab/protocol";
|
|
|
2
2
|
import type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle } from "../../../config/settings-schema";
|
|
3
3
|
import type { AgentSession } from "../../../session/agent-session";
|
|
4
4
|
import type { ActiveRepoContext } from "../../../utils/active-repo-context";
|
|
5
|
+
import type { LoopLimitRuntime } from "../../loop-limit";
|
|
5
6
|
|
|
6
7
|
export type { StatusLinePreset, StatusLineSegmentId, StatusLineSeparatorStyle };
|
|
7
8
|
|
|
@@ -64,7 +65,8 @@ export interface SegmentContext {
|
|
|
64
65
|
enabled: boolean;
|
|
65
66
|
} | null;
|
|
66
67
|
loopMode: {
|
|
67
|
-
|
|
68
|
+
state: "waiting" | "running" | "paused";
|
|
69
|
+
limit?: LoopLimitRuntime;
|
|
68
70
|
} | null;
|
|
69
71
|
goalMode: {
|
|
70
72
|
enabled: boolean;
|
|
@@ -782,6 +782,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
|
|
|
782
782
|
return this.isTranscriptBlockFinalized() ? undefined : 0;
|
|
783
783
|
}
|
|
784
784
|
|
|
785
|
+
/** Keeps the in-flight `vibe_wait` TV wall out of immutable native scrollback. */
|
|
786
|
+
isNativeScrollbackLiveRegionPinned(): boolean {
|
|
787
|
+
return this.#toolName === "vibe_wait" && !this.isTranscriptBlockFinalized();
|
|
788
|
+
}
|
|
789
|
+
|
|
785
790
|
/**
|
|
786
791
|
* Whether this block has reached a terminal state for transcript freezing.
|
|
787
792
|
* Reports `false` while it can still visually change so the
|