@oh-my-pi/pi-coding-agent 17.0.4 → 17.0.6
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 +209 -0
- package/dist/cli.js +3616 -3676
- 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/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/extensions/load-errors.d.ts +3 -0
- 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/extensibility/plugins/legacy-pi-compat.d.ts +8 -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 +10 -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/components/usage-row.d.ts +1 -1
- 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-client.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-frame.d.ts +9 -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/setup-wizard/index.d.ts +1 -1
- package/dist/types/modes/setup-wizard/scenes/model.d.ts +3 -0
- 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 +42 -6
- package/dist/types/session/messages.d.ts +6 -0
- package/dist/types/session/session-paths.d.ts +21 -4
- 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/tools/xdev.d.ts +5 -3
- package/dist/types/utils/git.d.ts +33 -0
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +1 -0
- package/dist/types/vibe/runtime.d.ts +23 -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/exec/bash-executor.ts +68 -8
- package/src/extensibility/extensions/load-errors.ts +13 -0
- package/src/extensibility/extensions/runner.ts +6 -4
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +26 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +41 -6
- 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 +25 -4
- package/src/mcp/transports/stdio.test.ts +9 -1
- package/src/modes/components/ask-dialog.ts +137 -73
- package/src/modes/components/chat-transcript-builder.ts +10 -1
- package/src/modes/components/status-line/component.ts +57 -3
- 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/components/usage-row.ts +17 -1
- package/src/modes/controllers/command-controller.ts +41 -1
- package/src/modes/controllers/event-controller.ts +7 -3
- package/src/modes/controllers/input-controller.ts +1 -1
- package/src/modes/controllers/selector-controller.ts +29 -8
- package/src/modes/interactive-mode.ts +102 -60
- package/src/modes/noninteractive-dispose.test.ts +14 -1
- package/src/modes/print-mode.ts +81 -9
- package/src/modes/rpc/rpc-client.ts +76 -35
- package/src/modes/rpc/rpc-frame.ts +156 -0
- package/src/modes/rpc/rpc-input.ts +38 -0
- package/src/modes/rpc/rpc-mode.ts +11 -4
- package/src/modes/setup-wizard/index.ts +2 -0
- package/src/modes/setup-wizard/scenes/model.ts +134 -0
- 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 +9 -3
- package/src/prompts/system/workflow-notice.md +89 -74
- 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 +484 -188
- package/src/session/messages.test.ts +91 -0
- package/src/session/messages.ts +248 -110
- package/src/session/session-manager.ts +34 -3
- package/src/session/session-paths.ts +38 -9
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/task/executor.ts +104 -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/tools/xdev.ts +11 -4
- package/src/tools/yield.ts +4 -1
- package/src/utils/git.ts +237 -0
- package/src/{modes/components/status-line → utils}/token-rate.ts +6 -0
- package/src/vibe/__tests__/token-rate.test.ts +96 -0
- package/src/vibe/runtime.ts +50 -0
- package/src/web/search/index.ts +9 -5
- package/src/web/search/providers/codex.ts +195 -99
- /package/dist/types/{modes/components/status-line → utils}/token-rate.d.ts +0 -0
|
@@ -153,13 +153,21 @@ describe.skipIf(process.platform === "win32")("StdioTransport request write stal
|
|
|
153
153
|
}, 8000);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
+
// `kill(pid, 0)` succeeds for a zombie too: a grandchild whose parent (the
|
|
157
|
+
// killed leader) is gone sits as <defunct> until whatever reaps orphans
|
|
158
|
+
// (init/subreaper) gets around to it — which can lag on some hosts. A
|
|
159
|
+
// zombie already received and honored the group SIGKILL; it is just not
|
|
160
|
+
// harvested yet, so treating it as "still alive" would make the group-kill
|
|
161
|
+
// assertions below flaky rather than testing what they claim to test.
|
|
156
162
|
function processExists(pid: number): boolean {
|
|
157
163
|
try {
|
|
158
164
|
process.kill(pid, 0);
|
|
159
|
-
return true;
|
|
160
165
|
} catch {
|
|
161
166
|
return false;
|
|
162
167
|
}
|
|
168
|
+
const result = Bun.spawnSync(["ps", "-o", "stat=", "-p", String(pid)]);
|
|
169
|
+
const state = result.stdout.toString().trim();
|
|
170
|
+
return result.exitCode === 0 && state.length > 0 && !state.startsWith("Z");
|
|
163
171
|
}
|
|
164
172
|
|
|
165
173
|
// Regression for #5578: `close()` used a bare `this.#process.kill()` (direct
|
|
@@ -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 {
|
|
@@ -85,6 +85,7 @@ export class ChatTranscriptBuilder {
|
|
|
85
85
|
#pendingUsage: Usage | undefined;
|
|
86
86
|
#pendingUsageDuration: number | undefined;
|
|
87
87
|
#pendingUsageTtft: number | undefined;
|
|
88
|
+
#pendingUsageTimestamp: number | undefined;
|
|
88
89
|
#lastAssistantUsage: Usage | undefined;
|
|
89
90
|
#waitingPoll: ToolExecutionComponent | null = null;
|
|
90
91
|
#todoSnapshot: ToolExecutionComponent | null = null;
|
|
@@ -133,6 +134,7 @@ export class ChatTranscriptBuilder {
|
|
|
133
134
|
this.#pendingUsage = undefined;
|
|
134
135
|
this.#pendingUsageDuration = undefined;
|
|
135
136
|
this.#pendingUsageTtft = undefined;
|
|
137
|
+
this.#pendingUsageTimestamp = undefined;
|
|
136
138
|
this.#lastAssistantUsage = undefined;
|
|
137
139
|
this.#waitingPoll = null;
|
|
138
140
|
this.#todoSnapshot = null;
|
|
@@ -201,11 +203,17 @@ export class ChatTranscriptBuilder {
|
|
|
201
203
|
this.#readGroup?.seal();
|
|
202
204
|
this.#readGroup = null;
|
|
203
205
|
this.container.addChild(
|
|
204
|
-
createUsageRowBlock(
|
|
206
|
+
createUsageRowBlock(
|
|
207
|
+
this.#pendingUsage,
|
|
208
|
+
this.#pendingUsageDuration,
|
|
209
|
+
this.#pendingUsageTtft,
|
|
210
|
+
this.#pendingUsageTimestamp,
|
|
211
|
+
),
|
|
205
212
|
);
|
|
206
213
|
this.#pendingUsage = undefined;
|
|
207
214
|
this.#pendingUsageDuration = undefined;
|
|
208
215
|
this.#pendingUsageTtft = undefined;
|
|
216
|
+
this.#pendingUsageTimestamp = undefined;
|
|
209
217
|
}
|
|
210
218
|
|
|
211
219
|
#appendChatMessage(message: AgentMessage): void {
|
|
@@ -381,6 +389,7 @@ export class ChatTranscriptBuilder {
|
|
|
381
389
|
settings.get("display.showTokenUsage") && assistantUsageIsBilled(message.usage) ? message.usage : undefined;
|
|
382
390
|
this.#pendingUsageDuration = message.duration;
|
|
383
391
|
this.#pendingUsageTtft = message.ttft;
|
|
392
|
+
this.#pendingUsageTimestamp = message.timestamp;
|
|
384
393
|
}
|
|
385
394
|
|
|
386
395
|
#appendToolResult(message: Extract<AgentMessage, { role: "toolResult" }>): void {
|
|
@@ -11,13 +11,13 @@ import { limitMatchesActiveAccount } from "../../../slash-commands/helpers/activ
|
|
|
11
11
|
import { type ActiveRepoContext, resolveActiveRepoContextSync } from "../../../utils/active-repo-context";
|
|
12
12
|
import * as git from "../../../utils/git";
|
|
13
13
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../../../utils/session-color";
|
|
14
|
+
import { calculateTokensPerSecond } from "../../../utils/token-rate";
|
|
14
15
|
import { sanitizeStatusText } from "../../shared";
|
|
15
16
|
import { theme } from "../../theme/theme";
|
|
16
17
|
import { canReuseCachedPr, createPrCacheContext, isSamePrCacheContext, type PrCacheContext } from "./git-utils";
|
|
17
18
|
import { getPreset } from "./presets";
|
|
18
19
|
import { renderSegment, type SegmentContext } from "./segments";
|
|
19
20
|
import { getSeparator } from "./separators";
|
|
20
|
-
import { calculateTokensPerSecond } from "./token-rate";
|
|
21
21
|
import type {
|
|
22
22
|
CollabStatus,
|
|
23
23
|
EffectiveStatusLineSettings,
|
|
@@ -273,9 +273,16 @@ 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
|
+
/**
|
|
280
|
+
* Injected aggregator that returns the aggregate tok/s of this session's
|
|
281
|
+
* live vibe worker sessions, or null when no workers are streaming. Kept as
|
|
282
|
+
* a callback so the render layer doesn't import the heavy vibe/task
|
|
283
|
+
* dependency graph; interactive-mode wires it to VibeSessionRegistry.
|
|
284
|
+
*/
|
|
285
|
+
#vibeWorkerTokenRate: (() => number | null) | null = null;
|
|
279
286
|
#collabStatus: CollabStatus | null = null;
|
|
280
287
|
#focusedAgentId: string | undefined;
|
|
281
288
|
#activeRepoCache: ActiveRepoCache | undefined;
|
|
@@ -496,7 +503,7 @@ export class StatusLineComponent implements Component {
|
|
|
496
503
|
this.#planModeStatus = status ?? null;
|
|
497
504
|
}
|
|
498
505
|
|
|
499
|
-
setLoopModeStatus(status:
|
|
506
|
+
setLoopModeStatus(status: NonNullable<SegmentContext["loopMode"]> | undefined): void {
|
|
500
507
|
this.#loopModeStatus = status ?? null;
|
|
501
508
|
}
|
|
502
509
|
|
|
@@ -508,6 +515,17 @@ export class StatusLineComponent implements Component {
|
|
|
508
515
|
this.#vibeModeStatus = status ?? null;
|
|
509
516
|
}
|
|
510
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Inject the aggregator that returns the aggregate tok/s of this session's
|
|
520
|
+
* live vibe worker sessions (null when no workers are streaming). Wired by
|
|
521
|
+
* interactive-mode, which owns the VibeSessionRegistry coupling, so the
|
|
522
|
+
* render layer stays off the heavy vibe/task dependency graph. Pass
|
|
523
|
+
* `undefined` to clear.
|
|
524
|
+
*/
|
|
525
|
+
setVibeWorkerTokenRateProvider(provider: (() => number | null) | undefined): void {
|
|
526
|
+
this.#vibeWorkerTokenRate = provider ?? null;
|
|
527
|
+
}
|
|
528
|
+
|
|
511
529
|
setCollabStatus(status: CollabStatus | null): void {
|
|
512
530
|
this.#collabStatus = status;
|
|
513
531
|
}
|
|
@@ -747,6 +765,31 @@ export class StatusLineComponent implements Component {
|
|
|
747
765
|
}
|
|
748
766
|
|
|
749
767
|
#getTokensPerSecond(): number | null {
|
|
768
|
+
// Aggregate tok/s across the main session AND every live vibe worker.
|
|
769
|
+
// In vibe mode the director is often idle while workers stream, so the
|
|
770
|
+
// main session's own rate alone would show a stale/zero value while
|
|
771
|
+
// parallel work is actively generating tokens.
|
|
772
|
+
const workerRate = this.#getVibeWorkerTokensPerSecond();
|
|
773
|
+
if (workerRate !== null) {
|
|
774
|
+
// At least one worker is streaming — add the director's live rate
|
|
775
|
+
// only when it is itself streaming (a finalized last-turn rate would
|
|
776
|
+
// double-count and overstate throughput).
|
|
777
|
+
const mainRate = this.session.isStreaming ? calculateTokensPerSecond(this.session.state.messages, true) : 0;
|
|
778
|
+
return (mainRate ?? 0) + workerRate;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// No workers streaming — fall back to the main session's own rate with
|
|
782
|
+
// its sticky per-assistant-message cache so the badge doesn't flicker
|
|
783
|
+
// off in the brief gap between stream end and the finalized message.
|
|
784
|
+
return this.#getMainSessionTokensPerSecond();
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Main session's tok/s with sticky caching keyed on the last assistant
|
|
789
|
+
* message timestamp. Preserves the pre-aggregation behavior when no vibe
|
|
790
|
+
* workers are active.
|
|
791
|
+
*/
|
|
792
|
+
#getMainSessionTokensPerSecond(): number | null {
|
|
750
793
|
let lastAssistantTimestamp: number | null = null;
|
|
751
794
|
for (let i = this.session.state.messages.length - 1; i >= 0; i--) {
|
|
752
795
|
const message = this.session.state.messages[i];
|
|
@@ -776,6 +819,17 @@ export class StatusLineComponent implements Component {
|
|
|
776
819
|
return null;
|
|
777
820
|
}
|
|
778
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Aggregate tok/s across every live vibe worker session owned by this
|
|
824
|
+
* session. Returns null when no workers are streaming (so the main
|
|
825
|
+
* session's own rate shines through unchanged). The aggregation itself is
|
|
826
|
+
* injected via {@link setVibeWorkerTokenRateProvider} to keep this render
|
|
827
|
+
* layer off the heavy vibe/task dependency graph.
|
|
828
|
+
*/
|
|
829
|
+
#getVibeWorkerTokensPerSecond(): number | null {
|
|
830
|
+
return this.#vibeWorkerTokenRate?.() ?? null;
|
|
831
|
+
}
|
|
832
|
+
|
|
779
833
|
#getUsageContextKey(session: AgentSession): string {
|
|
780
834
|
const activeProvider = session.state.model?.provider ?? session.model?.provider ?? "";
|
|
781
835
|
if (!activeProvider) return "";
|
|
@@ -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
|