@oh-my-pi/pi-tui 18.13.0 → 18.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -32
- package/dist/types/overlays/ask-dialog.d.ts +3 -1
- package/dist/types/overlays/pause-screen.d.ts +13 -3
- package/dist/types/status-line/component.d.ts +2 -2
- package/dist/types/tools/ask.d.ts +2 -0
- package/package.json +9 -9
- package/src/overlays/ask-dialog.ts +67 -20
- package/src/overlays/pause-screen.ts +166 -37
- package/src/status-line/component.ts +6 -4
- package/src/tools/ask.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [18.13.1] - 2026-10-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- ANSI and TSP `/pause` screens show unsettled turns and jobs, hide settled counters, and preserve counts and resume controls on short terminals ([#17073](https://github.com/can1357/oh-my-pi/pull/17073) by [@anatoli-tsinovoy](https://github.com/anatoli-tsinovoy)).
|
|
10
|
+
- Added `noteOption` to `ExtensionAskDialogResultItem` and `QuestionResult`: the label of the option an ask note is on (unset for a note on the custom answer) ([#17126](https://github.com/can1357/oh-my-pi/pull/17126) by [@H4vC](https://github.com/H4vC))
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `StatusLineComponent.setRunningSubagents()` and `setSubagentTreeCost()` now report whether their state changed, allowing callers to skip redundant redraws ([#17131](https://github.com/can1357/oh-my-pi/pull/17131) by [@iliaal](https://github.com/iliaal)).
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Fixed the ask dialog's Note (button or `n`) right after a pick moved to the next question annotating that question's highlighted row instead of the answer just picked ([#17126](https://github.com/can1357/oh-my-pi/pull/17126) by [@H4vC](https://github.com/H4vC))
|
|
19
|
+
- Fixed the ask dialog's Next button reopening the custom-answer editor after Other was cancelled instead of moving on to the next question or Review tab
|
|
20
|
+
|
|
5
21
|
## [18.13.0] - 2026-10-26
|
|
6
22
|
|
|
7
23
|
### Breaking Changes
|
|
@@ -2453,35 +2469,4 @@
|
|
|
2453
2469
|
|
|
2454
2470
|
- Fixed native Windows + Windows Terminal scrollback being yanked to the top when a streaming response triggered a TUI full redraw. Under ConPTY the `kernel32` `GetConsoleScreenBufferInfo` probe answers about the pseudo-console (always at the buffer tail) and not about WT's host scrollback, so `isNativeViewportAtBottom()` falsely returned `true` while the user was scrolled up and the shrink-across-viewport branch issued a destructive `historyRebuild` (`\x1b[2J\x1b[H\x1b[3J`). The probe now short-circuits to `undefined` whenever `WT_SESSION` is set, letting the existing deferred-rebuild path keep streaming-time mutations non-destructive and reconcile native history at the next prompt-submit checkpoint. ([#1635](https://github.com/can1357/oh-my-pi/issues/1635))
|
|
2455
2471
|
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
### Added
|
|
2459
|
-
|
|
2460
|
-
- Added `overflowSearch` to `SelectListLayoutOptions` to let consumers enable or disable type-to-filter search and search-status rendering per SelectList instance
|
|
2461
|
-
- Added fuzzy type-to-filter search to overflowing `SelectList` pickers, with search status and result counts.
|
|
2462
|
-
- Added `TUI.setEagerNativeScrollbackRebuild(enabled)` — while enabled, live render frames rebuild native scrollback on offscreen/structural changes even when the viewport position is unobservable (POSIX), instead of deferring to a non-destructive repaint. Trades the anti-yank guarantee for clean, duplicate-free history; intended for windows where output above the fold is actively re-laying out (e.g. a tool whose result is still streaming). A terminal that reports a known-scrolled viewport still defers.
|
|
2463
|
-
|
|
2464
|
-
### Changed
|
|
2465
|
-
|
|
2466
|
-
- Disabled interactive search filtering for editor autocomplete and slash-command `SelectList`s by passing `overflowSearch: false` in their layout options
|
|
2467
|
-
|
|
2468
|
-
### Fixed
|
|
2469
|
-
|
|
2470
|
-
- Preserved hidden tmux overlays in the live viewport by removing overlay content from view when an overlay was hidden while keeping pane history intact
|
|
2471
|
-
- Preserved native scrollback when forced TUI renders coalesce with content growth, and deferred pure tail appends while readers are scrolled into history.
|
|
2472
|
-
- Preserved existing terminal scrollback during forced and structural TUI renders so preexisting shell lines remained visible after component mutations
|
|
2473
|
-
- Rebuilt native scrollback for safe bottom-anchored offscreen edits and high-water preview collapses instead of repainting only the viewport, preventing stale or duplicated rows above the live viewport.
|
|
2474
|
-
- Stripped internal cursor marker sentinels from all rendered lines so offscreen focus markers no longer leak into terminal output
|
|
2475
|
-
- Truncated all painted lines to terminal width during viewport repaints and append-tail updates so long content no longer overflows or wraps unexpectedly
|
|
2476
|
-
- Fixed `tui.select.cancel` handling in `SelectList` so pressing Escape or Ctrl+C closes the list even when no matches are currently shown
|
|
2477
|
-
- Fixed native scrollback corruption when an offscreen row edit and repeated-tail append land in one render frame; ambiguous appended tails now rebuild history instead of splicing stale rows into the buffer.
|
|
2478
|
-
- Fixed scrolled-up readers being yanked back to the tail whenever streaming content arrived on POSIX terminals (macOS/Linux). Native viewport position is unobservable there (`isNativeViewportAtBottom()` returns `undefined`), and the planner optimistically treated "unknown" as "at bottom", so every offscreen streaming edit ran a destructive `historyRebuild` that cleared scrollback and snapped the view to the bottom. Live render frames now treat an unknown viewport as unsafe for a destructive rebuild — they defer to a non-destructive viewport repaint and reconcile native scrollback at the next explicit checkpoint (prompt submit). Resize and checkpoint replays keep the prior behavior.
|
|
2479
|
-
- Fixed native scrollback not rewrapping when the terminal widens on POSIX. A width increase reflows the transcript to fewer lines, which the shrink-across-boundary branch intercepted and (after the unknown-viewport deferral) repainted only the viewport — leaving committed history wrapped at the old width and duplicated above the live viewport. Width changes now rebuild native scrollback at the new geometry even when the viewport position is unknown (a yank is acceptable on an explicit resize); a terminal that can report a scrolled viewport still defers.
|
|
2480
|
-
|
|
2481
|
-
## [15.7.0] - 2026-05-31
|
|
2482
|
-
|
|
2483
|
-
### Fixed
|
|
2484
|
-
|
|
2485
|
-
- Fixed slash-command autocomplete repainting when a Windows Terminal session cannot report native scrollback position; live input renders can now bypass the unknown-viewport deferral without weakening background scrollback protection. ([#1550](https://github.com/can1357/oh-my-pi/issues/1550))
|
|
2486
|
-
|
|
2487
|
-
Older entries are archived in [packages/tui/CHANGELOG.md@539f05ae39f8](https://github.com/can1357/oh-my-pi/blob/539f05ae39f81319dc75d650757dad3c76482343/packages/tui/CHANGELOG.md).
|
|
2472
|
+
Older entries are archived in [packages/tui/CHANGELOG.md@3d45d8557cd7](https://github.com/can1357/oh-my-pi/blob/3d45d8557cd7493221b69c7ed3c6b6a2307f46e7/packages/tui/CHANGELOG.md).
|
|
@@ -31,6 +31,8 @@ export interface ExtensionAskDialogResultItem {
|
|
|
31
31
|
customInputImages?: ImageContent[];
|
|
32
32
|
note?: string;
|
|
33
33
|
noteImages?: ImageContent[];
|
|
34
|
+
/** Label of the option the note is on; unset when it is on the custom answer. */
|
|
35
|
+
noteOption?: string;
|
|
34
36
|
timedOut?: boolean;
|
|
35
37
|
}
|
|
36
38
|
/** Answers submitted from the dialog. */
|
|
@@ -116,7 +118,7 @@ export declare class AskDialogComponent implements Component {
|
|
|
116
118
|
* Pointer actions: a tab click switches to it; a click on an answer row is
|
|
117
119
|
* Enter on it (Space for a multi-select option, so it toggles; its
|
|
118
120
|
* double-click `activate` is Enter); the action bar's Submit, Note and Skip
|
|
119
|
-
* run
|
|
121
|
+
* run what Enter (see {@link #pressEnter} for Other), `n` and Esc run on the highlighted row; Hide/Show
|
|
120
122
|
* collapse and restore the dialog.
|
|
121
123
|
*/
|
|
122
124
|
handleNativeEvent(event: NativeUiEvent): void;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type Component, type OverlayFocusOwner, type OverlayHandle, type OverlayOptions } from "../index.js";
|
|
2
2
|
import type { NativeNode, NativeUiEvent } from "../native/node.js";
|
|
3
|
+
/** Outstanding turn and job counts supplied by the pause-screen host. */
|
|
4
|
+
export interface PauseWorkStatus {
|
|
5
|
+
unsettledTurns: number;
|
|
6
|
+
unfinishedJobs: number;
|
|
7
|
+
}
|
|
8
|
+
/** Optional live work source for the pause overlay. */
|
|
9
|
+
export interface PauseScreenOptions {
|
|
10
|
+
getWorkStatus?: () => PauseWorkStatus;
|
|
11
|
+
}
|
|
3
12
|
/**
|
|
4
13
|
* Slice of `InteractiveModeContext` the pause screen drives. Narrow so tests
|
|
5
14
|
* can exercise the full engage → hold → release lifecycle without a real TUI.
|
|
@@ -22,12 +31,13 @@ export interface PauseScreenHost {
|
|
|
22
31
|
* Paint the pause scene as exactly `height` rows, vertically centered.
|
|
23
32
|
* Exported for tests.
|
|
24
33
|
*/
|
|
25
|
-
export declare function renderPauseScreen(width: number, height: number, elapsedMs: number, sessionName?: string): string[];
|
|
34
|
+
export declare function renderPauseScreen(width: number, height: number, elapsedMs: number, sessionName?: string, workStatus?: PauseWorkStatus): string[];
|
|
26
35
|
/** Fullscreen overlay component; resolves {@link run} when a resume key lands. */
|
|
27
36
|
export declare class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
28
37
|
#private;
|
|
29
38
|
readonly host: PauseScreenHost;
|
|
30
|
-
|
|
39
|
+
readonly options: PauseScreenOptions;
|
|
40
|
+
constructor(host: PauseScreenHost, options?: PauseScreenOptions);
|
|
31
41
|
/** Start the clock; resolves once the user asks to resume. */
|
|
32
42
|
run(): Promise<void>;
|
|
33
43
|
dispose(): void;
|
|
@@ -57,4 +67,4 @@ export declare class PauseScreenComponent implements Component, OverlayFocusOwne
|
|
|
57
67
|
* gate on the way out (including teardown throws) — a leaked pause would
|
|
58
68
|
* freeze every agent in the process with no UI left to release it.
|
|
59
69
|
*/
|
|
60
|
-
export declare function runPauseScreen(host: PauseScreenHost): Promise<void>;
|
|
70
|
+
export declare function runPauseScreen(host: PauseScreenHost, options?: PauseScreenOptions): Promise<void>;
|
|
@@ -30,9 +30,9 @@ export declare class StatusLineComponent<TSession extends StatusLineSession = St
|
|
|
30
30
|
updateSettings(settings: StatusLineSettings): void;
|
|
31
31
|
getEffectiveSettingsForTest(): EffectiveStatusLineSettings;
|
|
32
32
|
setAutoCompactEnabled(enabled: boolean): void;
|
|
33
|
-
setRunningSubagents(agentIds: readonly string[]):
|
|
33
|
+
setRunningSubagents(agentIds: readonly string[]): boolean;
|
|
34
34
|
/** Host-computed spend of the main session's whole subagent tree (Agent Hub projection). */
|
|
35
|
-
setSubagentTreeCost(cost: number):
|
|
35
|
+
setSubagentTreeCost(cost: number): boolean;
|
|
36
36
|
/**
|
|
37
37
|
* Compatibility shim for callers predating the simplified subagent badge.
|
|
38
38
|
* The status line now intentionally shows only the active count.
|
|
@@ -12,6 +12,8 @@ export interface QuestionResult {
|
|
|
12
12
|
customInput?: string;
|
|
13
13
|
/** Optional note attached to the selected answer in the rich ask dialog. */
|
|
14
14
|
note?: string;
|
|
15
|
+
/** Label of the option the note is on; unset when it is on the custom answer. */
|
|
16
|
+
noteOption?: string;
|
|
15
17
|
/** True when the answer was auto-selected because the dialog timed out. */
|
|
16
18
|
timedOut?: boolean;
|
|
17
19
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-tui",
|
|
4
|
-
"version": "18.13.
|
|
4
|
+
"version": "18.13.1",
|
|
5
5
|
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"gen:glyphs": "bun scripts/gen-glyphs.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@oh-my-pi/omptype": "18.13.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "18.13.
|
|
46
|
-
"@oh-my-pi/pi-ai": "18.13.
|
|
47
|
-
"@oh-my-pi/pi-catalog": "18.13.
|
|
48
|
-
"@oh-my-pi/pi-natives": "18.13.
|
|
49
|
-
"@oh-my-pi/pi-utils": "18.13.
|
|
50
|
-
"@oh-my-pi/pi-wire": "18.13.
|
|
51
|
-
"@oh-my-pi/snapcompact": "18.13.
|
|
44
|
+
"@oh-my-pi/omptype": "18.13.1",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "18.13.1",
|
|
46
|
+
"@oh-my-pi/pi-ai": "18.13.1",
|
|
47
|
+
"@oh-my-pi/pi-catalog": "18.13.1",
|
|
48
|
+
"@oh-my-pi/pi-natives": "18.13.1",
|
|
49
|
+
"@oh-my-pi/pi-utils": "18.13.1",
|
|
50
|
+
"@oh-my-pi/pi-wire": "18.13.1",
|
|
51
|
+
"@oh-my-pi/snapcompact": "18.13.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"kitty-vt-wasm": "^0.2.0"
|
|
@@ -51,6 +51,8 @@ export interface ExtensionAskDialogResultItem {
|
|
|
51
51
|
customInputImages?: ImageContent[];
|
|
52
52
|
note?: string;
|
|
53
53
|
noteImages?: ImageContent[];
|
|
54
|
+
/** Label of the option the note is on; unset when it is on the custom answer. */
|
|
55
|
+
noteOption?: string;
|
|
54
56
|
timedOut?: boolean;
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -404,7 +406,7 @@ function describeAnswer(question: ExtensionAskDialogQuestion, state: QuestionSta
|
|
|
404
406
|
push(optionLabelSpans(question, index, labels[index] ?? option.label));
|
|
405
407
|
});
|
|
406
408
|
if (state.customInput !== undefined) push([span(`“${normalizedInlineInput(state.customInput)}”`)]);
|
|
407
|
-
const note = noteForSubmittedAnswer(question, state)?.trim();
|
|
409
|
+
const note = noteForSubmittedAnswer(question, state)?.text.trim();
|
|
408
410
|
if (note) spans.push(span(" ✎ ", "dim"), span(normalizedInlineInput(note), "em"));
|
|
409
411
|
return spans;
|
|
410
412
|
}
|
|
@@ -448,13 +450,17 @@ function clearNoteUnlessRow(state: QuestionState, rowKey: string): void {
|
|
|
448
450
|
if (state.noteRowKey !== undefined && state.noteRowKey !== rowKey) clearNote(state);
|
|
449
451
|
}
|
|
450
452
|
|
|
451
|
-
|
|
453
|
+
/** The note that goes out with the answer (its row is part of it), and the option it is on (unset for Other). */
|
|
454
|
+
function noteForSubmittedAnswer(
|
|
455
|
+
question: ExtensionAskDialogQuestion,
|
|
456
|
+
state: QuestionState,
|
|
457
|
+
): { text: string; option?: string } | undefined {
|
|
452
458
|
if (state.note === undefined || state.noteRowKey === undefined) return undefined;
|
|
453
|
-
if (state.noteRowKey === "other") return state.customInput !== undefined ? state.note : undefined;
|
|
459
|
+
if (state.noteRowKey === "other") return state.customInput !== undefined ? { text: state.note } : undefined;
|
|
454
460
|
const match = /^option:(\d+)$/.exec(state.noteRowKey);
|
|
455
461
|
const optionIndex = match?.[1] === undefined ? Number.NaN : Number.parseInt(match[1], 10);
|
|
456
462
|
const option = Number.isInteger(optionIndex) ? question.options[optionIndex] : undefined;
|
|
457
|
-
return option && state.selectedOptions.has(option.label) ? state.note : undefined;
|
|
463
|
+
return option && state.selectedOptions.has(option.label) ? { text: state.note, option: option.label } : undefined;
|
|
458
464
|
}
|
|
459
465
|
|
|
460
466
|
function renderRowLabel(
|
|
@@ -585,6 +591,12 @@ export class AskDialogComponent implements Component {
|
|
|
585
591
|
#countdownDeadline = 0;
|
|
586
592
|
/** Once-a-second re-describe while the native countdown ring is shown. */
|
|
587
593
|
#ringTick: NodeJS.Timeout | undefined;
|
|
594
|
+
/**
|
|
595
|
+
* The answer whose pick just moved the dialog on to the next question. Note annotates it, not the
|
|
596
|
+
* next question's highlighted row, until that question is answered or any other input arrives:
|
|
597
|
+
* a Tern click commits a single-choice row, so picking then pressing Note is the mouse flow.
|
|
598
|
+
*/
|
|
599
|
+
#justAnswered: { questionIndex: number; rowKey: string } | undefined;
|
|
588
600
|
readonly #panel: OverlayPanel;
|
|
589
601
|
readonly #headerRegion: PanelRows;
|
|
590
602
|
readonly #bodyRegion: PanelRows;
|
|
@@ -682,6 +694,7 @@ export class AskDialogComponent implements Component {
|
|
|
682
694
|
// closed/prompt guards, matching HookSelector/HookInput semantics.
|
|
683
695
|
this.#countdown?.reset();
|
|
684
696
|
this.#native = undefined;
|
|
697
|
+
if (keyData !== "n" && keyData !== "N") this.#justAnswered = undefined;
|
|
685
698
|
if (matchesSelectCancel(keyData)) {
|
|
686
699
|
this.#finishCancel();
|
|
687
700
|
return;
|
|
@@ -1006,7 +1019,10 @@ export class AskDialogComponent implements Component {
|
|
|
1006
1019
|
tone: "accent",
|
|
1007
1020
|
title: question?.multi ? `${enterLabel} enter · Toggle space` : `${enterLabel} enter`,
|
|
1008
1021
|
});
|
|
1009
|
-
const note = actionButton("Note", "note", {
|
|
1022
|
+
const note = actionButton("Note", "note", {
|
|
1023
|
+
keys: "n",
|
|
1024
|
+
title: "Add a note to the highlighted answer, or to the one just picked n",
|
|
1025
|
+
});
|
|
1010
1026
|
return actionBar([...lead, null, note, skip, submit]);
|
|
1011
1027
|
}
|
|
1012
1028
|
|
|
@@ -1014,11 +1030,12 @@ export class AskDialogComponent implements Component {
|
|
|
1014
1030
|
* Pointer actions: a tab click switches to it; a click on an answer row is
|
|
1015
1031
|
* Enter on it (Space for a multi-select option, so it toggles; its
|
|
1016
1032
|
* double-click `activate` is Enter); the action bar's Submit, Note and Skip
|
|
1017
|
-
* run
|
|
1033
|
+
* run what Enter (see {@link #pressEnter} for Other), `n` and Esc run on the highlighted row; Hide/Show
|
|
1018
1034
|
* collapse and restore the dialog.
|
|
1019
1035
|
*/
|
|
1020
1036
|
handleNativeEvent(event: NativeUiEvent): void {
|
|
1021
1037
|
if (this.#closed || this.#promptActive) return;
|
|
1038
|
+
if (event.type !== "action" || event.act !== "note") this.#justAnswered = undefined;
|
|
1022
1039
|
if (event.type === "action") {
|
|
1023
1040
|
if (event.act === "cancel") {
|
|
1024
1041
|
this.#finishCancel();
|
|
@@ -1062,7 +1079,13 @@ export class AskDialogComponent implements Component {
|
|
|
1062
1079
|
this.#requestRender();
|
|
1063
1080
|
}
|
|
1064
1081
|
|
|
1065
|
-
/**
|
|
1082
|
+
/**
|
|
1083
|
+
* The action bar's Submit/Next: submit on the Review tab, else commit the highlighted row —
|
|
1084
|
+
* except Other, whose row (click or Enter) already opens the editor. Reopening it here trapped a
|
|
1085
|
+
* cancelled Other: Next kept bringing the editor back instead of moving on. With a Review tab to
|
|
1086
|
+
* land on, Next keeps any custom answer and moves on; a lone single-select question has nowhere
|
|
1087
|
+
* to go but submitting nothing, so it still asks.
|
|
1088
|
+
*/
|
|
1066
1089
|
#pressEnter(): void {
|
|
1067
1090
|
this.#countdown?.reset();
|
|
1068
1091
|
if (this.#isSubmitTab()) {
|
|
@@ -1072,18 +1095,38 @@ export class AskDialogComponent implements Component {
|
|
|
1072
1095
|
const active = this.#activeQuestionState();
|
|
1073
1096
|
const rowItem = active ? this.#questionRows(active.question)[active.state.cursorIndex] : undefined;
|
|
1074
1097
|
if (!active || !rowItem) return;
|
|
1098
|
+
if (rowItem.kind === "other" && this.#hasSubmitTab()) {
|
|
1099
|
+
this.#advanceAfterQuestion(active.state.customInput === undefined ? undefined : rowItem.key);
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1075
1102
|
this.#commitRow(active.question, active.state, rowItem, true);
|
|
1076
1103
|
this.#requestRender();
|
|
1077
1104
|
}
|
|
1078
1105
|
|
|
1079
|
-
/** `n`: a note on the
|
|
1106
|
+
/** `n`: a note on the {@link #noteTarget} row (questions only). */
|
|
1080
1107
|
#pressNote(): void {
|
|
1081
1108
|
if (this.#isSubmitTab()) return;
|
|
1082
1109
|
this.#countdown?.reset();
|
|
1110
|
+
const target = this.#noteTarget();
|
|
1111
|
+
if (target) void this.#promptForNote(target.question, target.state, target.rowItem);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* The row a note goes to: the answer just picked while the question it moved on to is still
|
|
1116
|
+
* unanswered, else the highlighted row.
|
|
1117
|
+
*/
|
|
1118
|
+
#noteTarget(): { question: ExtensionAskDialogQuestion; state: QuestionState; rowItem: QuestionRow } | undefined {
|
|
1083
1119
|
const active = this.#activeQuestionState();
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1120
|
+
if (!active) return undefined;
|
|
1121
|
+
const last = this.#justAnswered;
|
|
1122
|
+
if (last && last.questionIndex !== this.#currentQuestionIndex() && !isAnswered(active.state)) {
|
|
1123
|
+
const question = this.#questions[last.questionIndex];
|
|
1124
|
+
const state = this.#states[last.questionIndex];
|
|
1125
|
+
const rowItem = question ? this.#questionRows(question).find(row => row.key === last.rowKey) : undefined;
|
|
1126
|
+
if (question && state && rowItem) return { question, state, rowItem };
|
|
1127
|
+
}
|
|
1128
|
+
const rowItem = this.#questionRows(active.question)[active.state.cursorIndex];
|
|
1129
|
+
return rowItem ? { question: active.question, state: active.state, rowItem } : undefined;
|
|
1087
1130
|
}
|
|
1088
1131
|
|
|
1089
1132
|
/** Re-describe once a second while a countdown ring is shown (the ring's value is data, not a terminal clock). */
|
|
@@ -1334,9 +1377,7 @@ export class AskDialogComponent implements Component {
|
|
|
1334
1377
|
const rowItem = rows[state.cursorIndex];
|
|
1335
1378
|
if (!rowItem) return;
|
|
1336
1379
|
if (keyData === "n" || keyData === "N") {
|
|
1337
|
-
|
|
1338
|
-
void this.#promptForNote(question, state, rowItem);
|
|
1339
|
-
}
|
|
1380
|
+
this.#pressNote();
|
|
1340
1381
|
return;
|
|
1341
1382
|
}
|
|
1342
1383
|
const isEnter = matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n";
|
|
@@ -1364,7 +1405,7 @@ export class AskDialogComponent implements Component {
|
|
|
1364
1405
|
// focused option; Space toggles. Advances to the next question
|
|
1365
1406
|
// (submitting only for a single-question dialog), matching
|
|
1366
1407
|
// single-select Enter (#8252).
|
|
1367
|
-
this.#advanceAfterQuestion();
|
|
1408
|
+
this.#advanceAfterQuestion(state.selectedOptions.has(option.label) ? rowItem.key : undefined);
|
|
1368
1409
|
return;
|
|
1369
1410
|
}
|
|
1370
1411
|
if (state.selectedOptions.has(option.label)) {
|
|
@@ -1380,7 +1421,7 @@ export class AskDialogComponent implements Component {
|
|
|
1380
1421
|
state.customInput = undefined;
|
|
1381
1422
|
state.customInputImages = undefined;
|
|
1382
1423
|
clearNoteUnlessRow(state, rowItem.key);
|
|
1383
|
-
this.#advanceAfterQuestion();
|
|
1424
|
+
this.#advanceAfterQuestion(rowItem.key);
|
|
1384
1425
|
}
|
|
1385
1426
|
|
|
1386
1427
|
#handleSubmitTabInput(keyData: string): void {
|
|
@@ -1405,8 +1446,13 @@ export class AskDialogComponent implements Component {
|
|
|
1405
1446
|
this.#submitScrollOffset = 0;
|
|
1406
1447
|
}
|
|
1407
1448
|
|
|
1408
|
-
|
|
1449
|
+
/** Move past the current question; `answeredRowKey` is the row whose pick did it, for {@link #noteTarget}. */
|
|
1450
|
+
#advanceAfterQuestion(answeredRowKey?: string): void {
|
|
1409
1451
|
const current = this.#currentQuestionIndex();
|
|
1452
|
+
this.#justAnswered =
|
|
1453
|
+
answeredRowKey !== undefined && this.#questions.length > 1
|
|
1454
|
+
? { questionIndex: current, rowKey: answeredRowKey }
|
|
1455
|
+
: undefined;
|
|
1410
1456
|
if (this.#questions.length === 1) {
|
|
1411
1457
|
this.#finishSubmit();
|
|
1412
1458
|
return;
|
|
@@ -1458,7 +1504,7 @@ export class AskDialogComponent implements Component {
|
|
|
1458
1504
|
this.#activeTabIndex = this.#submitTabIndex();
|
|
1459
1505
|
this.#submitScrollOffset = 0;
|
|
1460
1506
|
} else {
|
|
1461
|
-
this.#advanceAfterQuestion();
|
|
1507
|
+
this.#advanceAfterQuestion(rowItem.key);
|
|
1462
1508
|
}
|
|
1463
1509
|
} finally {
|
|
1464
1510
|
this.#promptActive = false;
|
|
@@ -1590,7 +1636,7 @@ export class AskDialogComponent implements Component {
|
|
|
1590
1636
|
const label = questionTabLabel(question, index);
|
|
1591
1637
|
const answer = renderAnswerSummary(question, state);
|
|
1592
1638
|
allLines.push(`${theme.fg("dim", `${index + 1}. ${label}:`)} ${answer}`);
|
|
1593
|
-
const submittedNote = noteForSubmittedAnswer(question, state);
|
|
1639
|
+
const submittedNote = noteForSubmittedAnswer(question, state)?.text;
|
|
1594
1640
|
if (submittedNote?.trim()) {
|
|
1595
1641
|
const note = normalizedInlineInput(submittedNote);
|
|
1596
1642
|
allLines.push(
|
|
@@ -1724,8 +1770,9 @@ export class AskDialogComponent implements Component {
|
|
|
1724
1770
|
selectedOptions,
|
|
1725
1771
|
customInput: state.customInput,
|
|
1726
1772
|
customInputImages: state.customInputImages,
|
|
1727
|
-
note,
|
|
1773
|
+
note: note?.text,
|
|
1728
1774
|
noteImages: note === undefined ? undefined : state.noteImages,
|
|
1775
|
+
noteOption: note?.option,
|
|
1729
1776
|
timedOut: state.timedOut || undefined,
|
|
1730
1777
|
});
|
|
1731
1778
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { agentPauseGate } from "@oh-my-pi/pi-agent-core";
|
|
15
15
|
import { type Component, matchesKey, type OverlayFocusOwner, type OverlayHandle, type OverlayOptions } from "../index";
|
|
16
16
|
import { formatCoarseDuration } from "../chrome/format";
|
|
17
|
-
import { centerLine } from "../utils";
|
|
17
|
+
import { centerLine, visibleWidth } from "../utils";
|
|
18
18
|
import { theme } from "../theme/theme";
|
|
19
19
|
import { matchesAppInterrupt } from "../keybinding-matchers";
|
|
20
20
|
import { formatKeyHint, formatTooltipKey } from "../app-keybindings";
|
|
@@ -24,6 +24,17 @@ import { col, keyed, node, row, span, text } from "../native/describe";
|
|
|
24
24
|
import { actionBar, actionButton } from "../native/overlay";
|
|
25
25
|
import { isNativeRendering } from "../native/state";
|
|
26
26
|
|
|
27
|
+
/** Outstanding turn and job counts supplied by the pause-screen host. */
|
|
28
|
+
export interface PauseWorkStatus {
|
|
29
|
+
unsettledTurns: number;
|
|
30
|
+
unfinishedJobs: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Optional live work source for the pause overlay. */
|
|
34
|
+
export interface PauseScreenOptions {
|
|
35
|
+
getWorkStatus?: () => PauseWorkStatus;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* Slice of `InteractiveModeContext` the pause screen drives. Narrow so tests
|
|
29
40
|
* can exercise the full engage → hold → release lifecycle without a real TUI.
|
|
@@ -39,9 +50,10 @@ export interface PauseScreenHost {
|
|
|
39
50
|
readonly sessionName?: string;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
const TICK_MS = 1_000;
|
|
53
|
+
const EMPTY_WORK_STATUS: PauseWorkStatus = { unsettledTurns: 0, unfinishedJobs: 0 };
|
|
44
54
|
|
|
55
|
+
/** Refresh cadence for the live hold clock and work-status snapshot. */
|
|
56
|
+
const TICK_MS = 1_000;
|
|
45
57
|
/** Pause-bar glyph geometry (rows × columns of full blocks per bar). */
|
|
46
58
|
const BAR_ROWS = 7;
|
|
47
59
|
const BAR_WIDTH = 5;
|
|
@@ -49,7 +61,7 @@ const BAR_GAP = 4;
|
|
|
49
61
|
|
|
50
62
|
/** Below either bound the full scene cannot breathe; drop to the compact card. */
|
|
51
63
|
const MIN_FULL_WIDTH = 64;
|
|
52
|
-
const MIN_FULL_HEIGHT =
|
|
64
|
+
const MIN_FULL_HEIGHT = 23;
|
|
53
65
|
|
|
54
66
|
const TITLE = "P A U S E D";
|
|
55
67
|
const BODY_LINES = [
|
|
@@ -79,51 +91,110 @@ function formatClock(ms: number): string {
|
|
|
79
91
|
* Paint the pause scene as exactly `height` rows, vertically centered.
|
|
80
92
|
* Exported for tests.
|
|
81
93
|
*/
|
|
82
|
-
export function renderPauseScreen(
|
|
83
|
-
|
|
94
|
+
export function renderPauseScreen(
|
|
95
|
+
width: number,
|
|
96
|
+
height: number,
|
|
97
|
+
elapsedMs: number,
|
|
98
|
+
sessionName?: string,
|
|
99
|
+
workStatus: PauseWorkStatus = EMPTY_WORK_STATUS,
|
|
100
|
+
): string[] {
|
|
101
|
+
const screenWidth = Math.max(1, Math.floor(width));
|
|
102
|
+
const screenHeight = Math.max(1, Math.floor(height));
|
|
103
|
+
const compact = screenWidth < MIN_FULL_WIDTH || screenHeight < MIN_FULL_HEIGHT;
|
|
104
|
+
const outstanding = workStatus.unsettledTurns > 0 || workStatus.unfinishedJobs > 0;
|
|
105
|
+
const status = outstanding ? "Pausing — waiting for work to settle" : "Paused — ready to suspend";
|
|
106
|
+
const workCounts = [
|
|
107
|
+
`Unsettled turns: ${workStatus.unsettledTurns}`,
|
|
108
|
+
`Unfinished jobs: ${workStatus.unfinishedJobs}`,
|
|
109
|
+
];
|
|
84
110
|
const content: string[] = [];
|
|
85
111
|
// Resume keys mirror PauseScreenComponent#handleInput: `app.interrupt` (raw Escape when unbound), Enter, Space.
|
|
86
112
|
const esc = interruptKey();
|
|
87
113
|
|
|
88
114
|
if (compact) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
const sessionLine = sessionName
|
|
116
|
+
? centerLine(theme.bold(sessionName.replace(/\s+/g, " ").trim()), screenWidth).trimEnd()
|
|
117
|
+
: undefined;
|
|
118
|
+
const titleLine = centerLine(theme.bold(theme.fg("accent", `▌▌ ${TITLE}`)), screenWidth).trimEnd();
|
|
119
|
+
const clockLine = centerLine(theme.fg("dim", `paused for ${formatClock(elapsedMs)}`), screenWidth).trimEnd();
|
|
120
|
+
const statusLine = centerLine(theme.fg(outstanding ? "warning" : "success", status), screenWidth).trimEnd();
|
|
121
|
+
const resumeLine = centerLine(theme.fg("dim", `${esc} to resume`), screenWidth).trimEnd();
|
|
122
|
+
const fullContent: string[] = [];
|
|
123
|
+
if (sessionLine) fullContent.push(sessionLine);
|
|
124
|
+
fullContent.push(titleLine, "", clockLine, statusLine);
|
|
125
|
+
if (outstanding)
|
|
126
|
+
fullContent.push(...workCounts.map(line => centerLine(theme.fg("muted", line), screenWidth).trimEnd()));
|
|
127
|
+
fullContent.push(resumeLine);
|
|
128
|
+
|
|
129
|
+
if (screenHeight >= fullContent.length) {
|
|
130
|
+
content.push(...fullContent);
|
|
131
|
+
} else {
|
|
132
|
+
// Keep the live state and a way out on screen before the decorative card details.
|
|
133
|
+
if (screenHeight === 1) {
|
|
134
|
+
const counts = `T:${workStatus.unsettledTurns} J:${workStatus.unfinishedJobs}`;
|
|
135
|
+
const summary = outstanding ? `${counts} · ${esc} to resume` : `Paused · ${esc} to resume`;
|
|
136
|
+
const abbreviatedSummary = outstanding
|
|
137
|
+
? `T${workStatus.unsettledTurns} J${workStatus.unfinishedJobs} ${esc} resume`
|
|
138
|
+
: `${esc} resume`;
|
|
139
|
+
const visibleSummary = visibleWidth(summary) > screenWidth ? abbreviatedSummary : summary;
|
|
140
|
+
content.push(
|
|
141
|
+
centerLine(theme.fg(outstanding ? "warning" : "success", visibleSummary), screenWidth).trimEnd(),
|
|
142
|
+
);
|
|
143
|
+
} else if (screenHeight === 2 && outstanding) {
|
|
144
|
+
const counts = `Pausing · T:${workStatus.unsettledTurns} J:${workStatus.unfinishedJobs}`;
|
|
145
|
+
content.push(centerLine(theme.fg("warning", counts), screenWidth).trimEnd(), resumeLine);
|
|
146
|
+
} else {
|
|
147
|
+
content.push(statusLine);
|
|
148
|
+
if (outstanding && screenHeight === 3) {
|
|
149
|
+
content.push(
|
|
150
|
+
centerLine(
|
|
151
|
+
theme.fg("muted", `Turns: ${workStatus.unsettledTurns} · Jobs: ${workStatus.unfinishedJobs}`),
|
|
152
|
+
screenWidth,
|
|
153
|
+
).trimEnd(),
|
|
154
|
+
);
|
|
155
|
+
} else if (outstanding) {
|
|
156
|
+
content.push(...workCounts.map(line => centerLine(theme.fg("muted", line), screenWidth).trimEnd()));
|
|
157
|
+
}
|
|
158
|
+
content.push(resumeLine);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (content.length < screenHeight && sessionLine) content.push(sessionLine);
|
|
162
|
+
if (content.length < screenHeight) content.push(titleLine);
|
|
163
|
+
if (content.length < screenHeight) content.push(clockLine);
|
|
92
164
|
}
|
|
93
|
-
content.push(centerLine(theme.bold(theme.fg("accent", `▌▌ ${TITLE}`)), width).trimEnd());
|
|
94
|
-
content.push("");
|
|
95
|
-
content.push(centerLine(theme.fg("dim", `paused for ${formatClock(elapsedMs)}`), width).trimEnd());
|
|
96
|
-
content.push(centerLine(theme.fg("dim", `${esc} to resume`), width).trimEnd());
|
|
97
165
|
} else {
|
|
98
166
|
if (sessionName) {
|
|
99
|
-
content.push(centerLine(theme.bold(sessionName),
|
|
100
|
-
content.push("");
|
|
167
|
+
content.push(centerLine(theme.bold(sessionName.replace(/\s+/g, " ").trim()), screenWidth).trimEnd());
|
|
101
168
|
content.push("");
|
|
102
169
|
}
|
|
103
170
|
const bar = "█".repeat(BAR_WIDTH);
|
|
104
171
|
const glyphRow = `${bar}${" ".repeat(BAR_GAP)}${bar}`;
|
|
105
172
|
for (let i = 0; i < BAR_ROWS; i++) {
|
|
106
|
-
content.push(centerLine(theme.fg("accent", glyphRow),
|
|
173
|
+
content.push(centerLine(theme.fg("accent", glyphRow), screenWidth).trimEnd());
|
|
107
174
|
}
|
|
108
175
|
content.push("");
|
|
109
|
-
content.push(centerLine(theme.bold(theme.fg("accent", TITLE)),
|
|
176
|
+
content.push(centerLine(theme.bold(theme.fg("accent", TITLE)), screenWidth).trimEnd());
|
|
110
177
|
content.push("");
|
|
111
178
|
for (const line of BODY_LINES) {
|
|
112
|
-
content.push(centerLine(theme.fg("muted", line),
|
|
179
|
+
content.push(centerLine(theme.fg("muted", line), screenWidth).trimEnd());
|
|
113
180
|
}
|
|
114
|
-
content.push("");
|
|
115
|
-
|
|
181
|
+
content.push(centerLine(theme.fg(outstanding ? "warning" : "success", status), screenWidth).trimEnd());
|
|
182
|
+
if (outstanding) {
|
|
183
|
+
content.push(centerLine(theme.fg("muted", workCounts[0]!), screenWidth).trimEnd());
|
|
184
|
+
content.push(centerLine(theme.fg("muted", workCounts[1]!), screenWidth).trimEnd());
|
|
185
|
+
}
|
|
186
|
+
content.push(centerLine(theme.fg("dim", `paused for ${formatClock(elapsedMs)}`), screenWidth).trimEnd());
|
|
116
187
|
content.push("");
|
|
117
188
|
const resumeHint = `${esc} · ${formatKeyHint("enter")} · ${formatKeyHint("space")} — resume`;
|
|
118
|
-
content.push(centerLine(theme.fg("dim", resumeHint),
|
|
189
|
+
content.push(centerLine(theme.fg("dim", resumeHint), screenWidth).trimEnd());
|
|
119
190
|
}
|
|
120
191
|
|
|
121
|
-
const topPad = Math.max(0, Math.floor((
|
|
192
|
+
const topPad = Math.max(0, Math.floor((screenHeight - content.length) / 2));
|
|
122
193
|
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
123
194
|
const lines: string[] = new Array(topPad).fill("");
|
|
124
195
|
lines.push(...content);
|
|
125
|
-
while (lines.length <
|
|
126
|
-
return lines.slice(0,
|
|
196
|
+
while (lines.length < screenHeight) lines.push("");
|
|
197
|
+
return lines.slice(0, screenHeight);
|
|
127
198
|
}
|
|
128
199
|
|
|
129
200
|
/** Fullscreen overlay component; resolves {@link run} when a resume key lands. */
|
|
@@ -133,19 +204,25 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
133
204
|
#disposed = false;
|
|
134
205
|
#startedAt = Date.now();
|
|
135
206
|
#native: NativeNode | undefined;
|
|
207
|
+
#workSnapshot: PauseWorkStatus;
|
|
136
208
|
|
|
137
|
-
constructor(
|
|
209
|
+
constructor(
|
|
210
|
+
readonly host: PauseScreenHost,
|
|
211
|
+
readonly options: PauseScreenOptions = {},
|
|
212
|
+
) {
|
|
213
|
+
this.#workSnapshot = this.#readWorkSnapshot();
|
|
214
|
+
}
|
|
138
215
|
|
|
139
216
|
/** Start the clock; resolves once the user asks to resume. */
|
|
140
217
|
run(): Promise<void> {
|
|
141
218
|
this.#startedAt = agentPauseGate.pausedAt ?? Date.now();
|
|
142
219
|
this.#native = undefined;
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
220
|
+
this.#refreshWorkSnapshot();
|
|
221
|
+
this.#timer ??= setInterval(() => {
|
|
222
|
+
if (this.#disposed) return;
|
|
223
|
+
const statusChanged = this.#refreshWorkSnapshot();
|
|
224
|
+
if (statusChanged || !isNativeRendering()) this.host.ui.requestRender();
|
|
225
|
+
}, TICK_MS);
|
|
149
226
|
this.host.ui.requestRender();
|
|
150
227
|
return this.#done.promise;
|
|
151
228
|
}
|
|
@@ -181,6 +258,25 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
181
258
|
this.#native = undefined;
|
|
182
259
|
}
|
|
183
260
|
|
|
261
|
+
#readWorkSnapshot(): PauseWorkStatus {
|
|
262
|
+
const workStatus = this.options.getWorkStatus?.() ?? EMPTY_WORK_STATUS;
|
|
263
|
+
return {
|
|
264
|
+
unsettledTurns: workStatus.unsettledTurns,
|
|
265
|
+
unfinishedJobs: workStatus.unfinishedJobs,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#refreshWorkSnapshot(): boolean {
|
|
270
|
+
const next = this.#readWorkSnapshot();
|
|
271
|
+
const previous = this.#workSnapshot;
|
|
272
|
+
if (next.unsettledTurns === previous.unsettledTurns && next.unfinishedJobs === previous.unfinishedJobs) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
this.#workSnapshot = next;
|
|
276
|
+
this.#native = undefined;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
184
280
|
/** A glass sheet titled "Paused" (not the whole pane: the transcript stays visible, frozen). */
|
|
185
281
|
readonly nativeOverlay = { role: "omp.overlay.pause", size: "md", anchor: "center", head: "Paused" } as const;
|
|
186
282
|
|
|
@@ -191,6 +287,41 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
191
287
|
*/
|
|
192
288
|
describe(): NativeNode {
|
|
193
289
|
if (this.#native) return this.#native;
|
|
290
|
+
const snapshot = this.#workSnapshot;
|
|
291
|
+
const outstanding = snapshot.unsettledTurns > 0 || snapshot.unfinishedJobs > 0;
|
|
292
|
+
const status = col(
|
|
293
|
+
[
|
|
294
|
+
keyed(
|
|
295
|
+
text(
|
|
296
|
+
[
|
|
297
|
+
span(
|
|
298
|
+
outstanding ? "Pausing — waiting for work to settle" : "Paused — ready to suspend",
|
|
299
|
+
outstanding ? "warning" : "success",
|
|
300
|
+
),
|
|
301
|
+
],
|
|
302
|
+
{ role: "omp.pause.settlement" },
|
|
303
|
+
),
|
|
304
|
+
"settlement",
|
|
305
|
+
),
|
|
306
|
+
...(outstanding
|
|
307
|
+
? [
|
|
308
|
+
keyed(
|
|
309
|
+
text([span(`Unsettled turns: ${snapshot.unsettledTurns}`, "muted")], {
|
|
310
|
+
role: "omp.pause.unsettled-turn-count",
|
|
311
|
+
}),
|
|
312
|
+
"unsettled-turns",
|
|
313
|
+
),
|
|
314
|
+
keyed(
|
|
315
|
+
text([span(`Unfinished jobs: ${snapshot.unfinishedJobs}`, "muted")], {
|
|
316
|
+
role: "omp.pause.unfinished-job-count",
|
|
317
|
+
}),
|
|
318
|
+
"unfinished-jobs",
|
|
319
|
+
),
|
|
320
|
+
]
|
|
321
|
+
: []),
|
|
322
|
+
],
|
|
323
|
+
{ role: "omp.pause.settlement-status", gap: "sm" },
|
|
324
|
+
);
|
|
194
325
|
const resumeKey = boundKeys("app.interrupt", ["escape"])[0] ?? "escape";
|
|
195
326
|
const hero = row(
|
|
196
327
|
[
|
|
@@ -244,12 +375,9 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
244
375
|
this.#native = col(
|
|
245
376
|
[
|
|
246
377
|
keyed(hero, "hero"),
|
|
378
|
+
keyed(status, "settlement-status"),
|
|
247
379
|
keyed(facts, "facts"),
|
|
248
380
|
actionBar([
|
|
249
|
-
keyed(
|
|
250
|
-
text([span("Each agent continues where it stopped.", "muted")], { role: "omp.pause.note" }),
|
|
251
|
-
"note",
|
|
252
|
-
),
|
|
253
381
|
null,
|
|
254
382
|
actionButton("Resume", "resume", {
|
|
255
383
|
keys: resumeKey,
|
|
@@ -276,6 +404,7 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
276
404
|
Math.max(1, this.host.ui.terminal.rows),
|
|
277
405
|
elapsed,
|
|
278
406
|
this.host.sessionName,
|
|
407
|
+
this.#workSnapshot,
|
|
279
408
|
);
|
|
280
409
|
}
|
|
281
410
|
}
|
|
@@ -286,9 +415,9 @@ export class PauseScreenComponent implements Component, OverlayFocusOwner {
|
|
|
286
415
|
* gate on the way out (including teardown throws) — a leaked pause would
|
|
287
416
|
* freeze every agent in the process with no UI left to release it.
|
|
288
417
|
*/
|
|
289
|
-
export async function runPauseScreen(host: PauseScreenHost): Promise<void> {
|
|
418
|
+
export async function runPauseScreen(host: PauseScreenHost, options?: PauseScreenOptions): Promise<void> {
|
|
290
419
|
if (!agentPauseGate.pause()) return;
|
|
291
|
-
const component = new PauseScreenComponent(host);
|
|
420
|
+
const component = new PauseScreenComponent(host, options);
|
|
292
421
|
const overlay = host.ui.showOverlay(component, {
|
|
293
422
|
width: "100%",
|
|
294
423
|
maxHeight: "100%",
|
|
@@ -990,24 +990,26 @@ export class StatusLineComponent<TSession extends StatusLineSession = StatusLine
|
|
|
990
990
|
this.#invalidateStatusLineRenderCache();
|
|
991
991
|
}
|
|
992
992
|
|
|
993
|
-
setRunningSubagents(agentIds: readonly string[]):
|
|
993
|
+
setRunningSubagents(agentIds: readonly string[]): boolean {
|
|
994
994
|
if (
|
|
995
995
|
agentIds.length === this.#runningSubagentIds.size &&
|
|
996
996
|
agentIds.every(agentId => this.#runningSubagentIds.has(agentId))
|
|
997
997
|
) {
|
|
998
|
-
return;
|
|
998
|
+
return false;
|
|
999
999
|
}
|
|
1000
1000
|
this.#subagentCount = agentIds.length;
|
|
1001
1001
|
this.#runningSubagentIds = new Set(agentIds);
|
|
1002
1002
|
this.#invalidateStatusLineRenderCache();
|
|
1003
|
+
return true;
|
|
1003
1004
|
}
|
|
1004
1005
|
|
|
1005
1006
|
/** Host-computed spend of the main session's whole subagent tree (Agent Hub projection). */
|
|
1006
|
-
setSubagentTreeCost(cost: number):
|
|
1007
|
+
setSubagentTreeCost(cost: number): boolean {
|
|
1007
1008
|
const next = Number.isFinite(cost) && cost > 0 ? cost : 0;
|
|
1008
|
-
if (next === this.#subagentTreeCost) return;
|
|
1009
|
+
if (next === this.#subagentTreeCost) return false;
|
|
1009
1010
|
this.#subagentTreeCost = next;
|
|
1010
1011
|
this.#invalidateStatusLineRenderCache();
|
|
1012
|
+
return true;
|
|
1011
1013
|
}
|
|
1012
1014
|
|
|
1013
1015
|
/**
|
package/src/tools/ask.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface QuestionResult {
|
|
|
37
37
|
customInput?: string;
|
|
38
38
|
/** Optional note attached to the selected answer in the rich ask dialog. */
|
|
39
39
|
note?: string;
|
|
40
|
+
/** Label of the option the note is on; unset when it is on the custom answer. */
|
|
41
|
+
noteOption?: string;
|
|
40
42
|
/** True when the answer was auto-selected because the dialog timed out. */
|
|
41
43
|
timedOut?: boolean;
|
|
42
44
|
}
|