@linxiraos/pi-tui 1.2.1 → 1.3.0
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 +188 -125
- package/README.md +36 -0
- package/THIRD-PARTY-NOTICES.txt +55 -5
- package/dist/types/app-keybindings.d.ts +18 -8
- package/dist/types/apps/git/diff-pane.d.ts +5 -0
- package/dist/types/apps/git/git-tui.d.ts +16 -2
- package/dist/types/apps/git/state.d.ts +11 -0
- package/dist/types/autocomplete.d.ts +2 -0
- package/dist/types/charts/chart-svg.d.ts +7 -13
- package/dist/types/chat/advisor-message.d.ts +4 -1
- package/dist/types/chat/assistant-message.d.ts +9 -4
- package/dist/types/chat/figure-markdown.d.ts +7 -2
- package/dist/types/chat/messages.d.ts +3 -1
- package/dist/types/chat/read-tool-group.d.ts +2 -2
- package/dist/types/chat/table-chart.d.ts +11 -15
- package/dist/types/chat/tool-execution.d.ts +32 -20
- package/dist/types/chat/user-message.d.ts +3 -2
- package/dist/types/chrome/countdown-timer.d.ts +3 -2
- package/dist/types/chrome/diff.d.ts +2 -3
- package/dist/types/chrome/transcript-container.d.ts +2 -0
- package/dist/types/components/image.d.ts +2 -0
- package/dist/types/components/markdown.d.ts +49 -3
- package/dist/types/components/text.d.ts +2 -0
- package/dist/types/debug-server.d.ts +2 -0
- package/dist/types/keybinding-matchers.d.ts +6 -0
- package/dist/types/multiplexers/tmux.d.ts +1 -0
- package/dist/types/multiplexers/types.d.ts +6 -0
- package/dist/types/native/backend.d.ts +5 -4
- package/dist/types/native/blobs.d.ts +24 -12
- package/dist/types/overlays/agent-hub-projection.d.ts +10 -5
- package/dist/types/overlays/annotation-overlay.d.ts +19 -0
- package/dist/types/overlays/approval-dialog.d.ts +79 -0
- package/dist/types/overlays/ask-dialog.d.ts +11 -6
- package/dist/types/overlays/btw-history-panel.d.ts +2 -6
- package/dist/types/overlays/hook-editor.d.ts +7 -2
- package/dist/types/overlays/hook-input.d.ts +12 -4
- package/dist/types/overlays/job-tree.d.ts +50 -0
- package/dist/types/overlays/jobs-panel.d.ts +25 -11
- package/dist/types/overlays/login-dialog.d.ts +8 -3
- package/dist/types/overlays/mcp-add-wizard.d.ts +9 -1
- package/dist/types/overlays/model-browser.d.ts +6 -1
- package/dist/types/overlays/model-picker.d.ts +2 -0
- package/dist/types/overlays/pause-screen.d.ts +5 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +4 -0
- package/dist/types/overlays/report-panel.d.ts +5 -1
- package/dist/types/overlays/reset-usage-selector.d.ts +7 -5
- package/dist/types/overlays/session-account-selector.d.ts +4 -0
- package/dist/types/overlays/session-info-overlay.d.ts +7 -3
- package/dist/types/overlays/session-selector.d.ts +3 -1
- package/dist/types/overlays/usage-dashboard.d.ts +6 -6
- package/dist/types/prompt/composer.d.ts +21 -20
- package/dist/types/prompt/custom-editor.d.ts +20 -10
- package/dist/types/prompt/image-references.d.ts +22 -0
- package/dist/types/prompt/internal-url-autocomplete.d.ts +1 -0
- package/dist/types/prompt/macos-spelling.d.ts +2 -1
- package/dist/types/prompt/prose-gate.d.ts +3 -0
- package/dist/types/prompt/queued-messages.d.ts +1 -1
- package/dist/types/prompt/typing-word.d.ts +25 -0
- package/dist/types/prompt/welcome.d.ts +26 -6
- package/dist/types/render/hanging-wrap.d.ts +7 -0
- package/dist/types/render/hyperlink.d.ts +2 -0
- package/dist/types/render/render-utils.d.ts +9 -1
- package/dist/types/render/utils.d.ts +1 -1
- package/dist/types/setup/scenes/splash.d.ts +6 -5
- package/dist/types/status-line/context-usage.d.ts +4 -1
- package/dist/types/status-line/host.d.ts +6 -4
- package/dist/types/status-line/startup.d.ts +6 -6
- package/dist/types/status-line/types.d.ts +3 -0
- package/dist/types/stdin-buffer.d.ts +7 -0
- package/dist/types/terminal-capabilities.d.ts +16 -1
- package/dist/types/terminal-multiplexer.d.ts +7 -0
- package/dist/types/terminal.d.ts +8 -4
- package/dist/types/theme/symbols.d.ts +1 -1
- package/dist/types/theme/theme.d.ts +11 -0
- package/dist/types/thinking.d.ts +5 -0
- package/dist/types/tmux.d.ts +39 -4
- package/dist/types/tools/ast-edit.d.ts +2 -2
- package/dist/types/tools/ast-grep.d.ts +2 -2
- package/dist/types/tools/bash.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +2 -0
- package/dist/types/tools/eval.d.ts +53 -4
- package/dist/types/tools/find.d.ts +14 -4
- package/dist/types/tools/glob.d.ts +3 -3
- package/dist/types/tools/grep.d.ts +2 -2
- package/dist/types/tools/grouped-file-output.d.ts +9 -4
- package/dist/types/tools/native-view.d.ts +1 -1
- package/dist/types/tools/read.d.ts +32 -10
- package/dist/types/tools/renderer.d.ts +2 -0
- package/dist/types/tools/task.d.ts +31 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui.d.ts +20 -3
- package/package.json +10 -10
- package/src/app-keybindings.d.ts +18 -8
- package/src/app-keybindings.ts +20 -8
- package/src/apps/autoresearch-dashboard.ts +444 -27
- package/src/apps/debug/raw-sse.ts +2 -2
- package/src/apps/git/diff-pane.ts +39 -41
- package/src/apps/git/git-tui.ts +80 -14
- package/src/apps/git/sidebar.ts +10 -1
- package/src/apps/git/state.ts +12 -0
- package/src/autocomplete.d.ts +2 -0
- package/src/autocomplete.ts +21 -19
- package/src/bracketed-paste.ts +9 -2
- package/src/charts/chart-svg.d.ts +7 -13
- package/src/charts/chart-svg.ts +19 -22
- package/src/chat/advisor-message.d.ts +4 -1
- package/src/chat/advisor-message.ts +77 -24
- package/src/chat/assistant-message.d.ts +9 -4
- package/src/chat/assistant-message.ts +127 -31
- package/src/chat/bash-execution.ts +7 -2
- package/src/chat/figure-markdown.d.ts +7 -2
- package/src/chat/figure-markdown.ts +10 -3
- package/src/chat/image-loading.ts +9 -6
- package/src/chat/messages.d.ts +3 -1
- package/src/chat/messages.ts +4 -1
- package/src/chat/read-tool-group.d.ts +2 -2
- package/src/chat/read-tool-group.ts +97 -42
- package/src/chat/svg-figure.ts +3 -1
- package/src/chat/svg-source.ts +6 -3
- package/src/chat/table-chart.d.ts +11 -15
- package/src/chat/table-chart.ts +24 -57
- package/src/chat/tool-execution.d.ts +32 -20
- package/src/chat/tool-execution.ts +103 -36
- package/src/chat/user-message.d.ts +3 -2
- package/src/chat/user-message.ts +54 -34
- package/src/chrome/countdown-timer.ts +3 -3
- package/src/chrome/diff.d.ts +2 -3
- package/src/chrome/diff.ts +4 -9
- package/src/chrome/transcript-container.d.ts +2 -0
- package/src/chrome/transcript-container.ts +212 -28
- package/src/components/editor.ts +12 -12
- package/src/components/image.d.ts +2 -0
- package/src/components/image.ts +54 -20
- package/src/components/markdown.d.ts +49 -3
- package/src/components/markdown.ts +461 -11
- package/src/components/settings-list.ts +2 -4
- package/src/components/text.d.ts +2 -0
- package/src/components/text.ts +11 -1
- package/src/debug-server.d.ts +2 -0
- package/src/debug-server.ts +51 -69
- package/src/hotkeys-markdown.ts +2 -0
- package/src/keybinding-matchers.d.ts +6 -0
- package/src/keybinding-matchers.ts +19 -0
- package/src/multiplexers/tmux.d.ts +1 -0
- package/src/multiplexers/tmux.ts +3 -0
- package/src/multiplexers/types.d.ts +6 -0
- package/src/multiplexers/types.ts +6 -0
- package/src/native/backend.d.ts +5 -4
- package/src/native/backend.ts +13 -5
- package/src/native/blobs.d.ts +24 -12
- package/src/native/blobs.ts +56 -7
- package/src/native/reconcile.ts +43 -21
- package/src/overlays/advisor-config.ts +259 -8
- package/src/overlays/agent-hub-projection.d.ts +10 -5
- package/src/overlays/agent-hub-projection.ts +63 -12
- package/src/overlays/annotation-overlay.ts +422 -168
- package/src/overlays/approval-dialog.ts +389 -0
- package/src/overlays/ask-dialog.ts +244 -105
- package/src/overlays/btw-history-panel.ts +208 -147
- package/src/overlays/extensions/extension-list.ts +4 -6
- package/src/overlays/hook-editor.ts +90 -32
- package/src/overlays/hook-input.ts +105 -19
- package/src/overlays/job-tree.ts +177 -0
- package/src/overlays/jobs-panel.ts +222 -142
- package/src/overlays/login-dialog.ts +201 -91
- package/src/overlays/mcp-add-wizard.ts +562 -279
- package/src/overlays/model-browser.ts +32 -7
- package/src/overlays/model-hub.ts +4 -10
- package/src/overlays/model-picker.ts +58 -21
- package/src/overlays/pause-screen.ts +81 -23
- package/src/overlays/plan-review-overlay.ts +16 -0
- package/src/overlays/plugin-selector.ts +17 -1
- package/src/overlays/queue-mode-selector.ts +10 -0
- package/src/overlays/report-panel.ts +6 -2
- package/src/overlays/reset-usage-selector.ts +130 -21
- package/src/overlays/session-account-selector.ts +36 -0
- package/src/overlays/session-info-overlay.ts +199 -76
- package/src/overlays/session-selector.ts +15 -10
- package/src/overlays/settings-selector.ts +3 -0
- package/src/overlays/show-images-selector.ts +1 -0
- package/src/overlays/snapcompact-shape-preview.ts +13 -6
- package/src/overlays/theme-selector.ts +6 -1
- package/src/overlays/thinking-selector.ts +8 -4
- package/src/overlays/tree-selector.ts +117 -2
- package/src/overlays/usage-dashboard.ts +498 -144
- package/src/prompt/composer-cache.ts +1 -1
- package/src/prompt/composer.ts +194 -74
- package/src/prompt/custom-editor.ts +168 -65
- package/src/prompt/image-references.d.ts +22 -0
- package/src/prompt/image-references.ts +79 -2
- package/src/prompt/internal-url-autocomplete.ts +1 -0
- package/src/prompt/macos-spelling.ts +28 -5
- package/src/prompt/prose-gate.ts +3 -0
- package/src/prompt/queued-messages.ts +9 -2
- package/src/prompt/tips.txt +1 -2
- package/src/prompt/typing-word.ts +59 -0
- package/src/prompt/welcome.ts +97 -45
- package/src/render/code-cell.ts +18 -5
- package/src/render/hanging-wrap.d.ts +7 -0
- package/src/render/hanging-wrap.ts +58 -0
- package/src/render/hyperlink.d.ts +2 -0
- package/src/render/hyperlink.ts +5 -0
- package/src/render/render-utils.d.ts +9 -1
- package/src/render/render-utils.ts +16 -2
- package/src/render/utils.d.ts +1 -1
- package/src/render/utils.ts +1 -1
- package/src/render/width-aware-text.ts +6 -5
- package/src/setup/scenes/splash.ts +22 -8
- package/src/status-line/component.ts +23 -10
- package/src/status-line/context-usage.ts +176 -73
- package/src/status-line/footer.ts +18 -35
- package/src/status-line/host.ts +6 -1
- package/src/status-line/segments.ts +16 -5
- package/src/status-line/startup.ts +6 -6
- package/src/status-line/types.ts +1 -0
- package/src/stdin-buffer.d.ts +7 -0
- package/src/stdin-buffer.ts +11 -1
- package/src/terminal-capabilities.d.ts +16 -1
- package/src/terminal-capabilities.ts +66 -28
- package/src/terminal-multiplexer.d.ts +7 -0
- package/src/terminal-multiplexer.ts +14 -0
- package/src/terminal.d.ts +8 -4
- package/src/terminal.ts +160 -46
- package/src/theme/symbols.d.ts +1 -1
- package/src/theme/symbols.ts +1 -0
- package/src/theme/theme-class.ts +1 -0
- package/src/theme/theme.d.ts +11 -0
- package/src/theme/theme.ts +46 -14
- package/src/thinking.d.ts +5 -0
- package/src/thinking.ts +9 -1
- package/src/tmux.d.ts +39 -4
- package/src/tmux.ts +110 -16
- package/src/tools/ask.ts +20 -9
- package/src/tools/ast-edit.d.ts +2 -2
- package/src/tools/ast-edit.ts +30 -7
- package/src/tools/ast-grep.d.ts +2 -2
- package/src/tools/ast-grep.ts +39 -35
- package/src/tools/bash.d.ts +6 -1
- package/src/tools/bash.ts +15 -9
- package/src/tools/debug.ts +2 -1
- package/src/tools/default-renderer.ts +3 -2
- package/src/tools/edit.d.ts +2 -0
- package/src/tools/edit.ts +100 -21
- package/src/tools/eval.d.ts +53 -4
- package/src/tools/eval.ts +173 -137
- package/src/tools/find.d.ts +14 -4
- package/src/tools/find.ts +71 -18
- package/src/tools/glob.d.ts +3 -3
- package/src/tools/glob.ts +51 -17
- package/src/tools/grep.d.ts +2 -2
- package/src/tools/grep.ts +99 -22
- package/src/tools/grouped-file-output.d.ts +9 -4
- package/src/tools/grouped-file-output.ts +21 -6
- package/src/tools/lsp.ts +30 -18
- package/src/tools/mcp.ts +2 -2
- package/src/tools/native-view.d.ts +1 -1
- package/src/tools/native-view.ts +2 -2
- package/src/tools/read.d.ts +32 -10
- package/src/tools/read.ts +89 -43
- package/src/tools/renderer.d.ts +2 -0
- package/src/tools/renderer.ts +2 -0
- package/src/tools/task.d.ts +31 -0
- package/src/tools/task.ts +40 -7
- package/src/tools/todo.ts +3 -2
- package/src/tools/wait.ts +16 -30
- package/src/tools/web-search.ts +2 -1
- package/src/tools/write.d.ts +2 -0
- package/src/tools/write.ts +36 -12
- package/src/tools/yield.ts +2 -2
- package/src/tui.d.ts +20 -3
- package/src/tui.ts +140 -36
- package/src/utils.ts +5 -3
- package/dist/types/tools/eval-format/code-mode-declarations.d.ts +0 -5
- package/src/tools/eval-format/code-mode-declarations.ts +0 -61
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-approval prompt in the composer's place, presented like the ask
|
|
3
|
+
* dialog: the approval title and its details over a list of choices —
|
|
4
|
+
* Approve, an optional "approve for this session", and Deny — and an action
|
|
5
|
+
* bar. The arrows move between the choices and Enter takes the highlighted
|
|
6
|
+
* one; `app.approval.approve` (Ctrl+Enter by default) approves this call
|
|
7
|
+
* outright and Esc denies. Enter and the approve chord only arm
|
|
8
|
+
* {@link APPROVE_ARM_MS} after mount, so a key meant for the composer (Enter,
|
|
9
|
+
* or the follow-up chord pressed mid-run) as the prompt appears never answers
|
|
10
|
+
* it. With an {@link ApprovalDialogDraft}, every other key edits the composer
|
|
11
|
+
* mounted beneath the dialog, so a message can deny the call and steer the
|
|
12
|
+
* turn instead: while the composer holds a draft, the arrows and Enter edit and
|
|
13
|
+
* send it, and the highlight sits on Deny, relabeled to say Enter sends.
|
|
14
|
+
* The prompt stays a few rows so the composer keeps its room; Page Up/Down
|
|
15
|
+
* scroll the details, and with a full prompt the expand key
|
|
16
|
+
* (`app.tools.expand`) swaps it in, wrapped.
|
|
17
|
+
*/
|
|
18
|
+
import { type Component, Ellipsis, ScrollView, type TUI, truncateToWidth, wrapTextWithAnsi } from "../index";
|
|
19
|
+
import { formatKeyHints, formatTooltipKey } from "../app-keybindings";
|
|
20
|
+
import { OverlayPanel, PanelDivider, PanelRows } from "../chrome/overlay-box";
|
|
21
|
+
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
22
|
+
import { getKeybindings } from "../keybindings";
|
|
23
|
+
import {
|
|
24
|
+
appApproveKeys,
|
|
25
|
+
appToolsExpandKey,
|
|
26
|
+
matchesAppApprove,
|
|
27
|
+
matchesAppToolsExpand,
|
|
28
|
+
matchesSelectCancel,
|
|
29
|
+
matchesSelectDown,
|
|
30
|
+
matchesSelectPageDown,
|
|
31
|
+
matchesSelectPageUp,
|
|
32
|
+
matchesSelectUp,
|
|
33
|
+
} from "../keybinding-matchers";
|
|
34
|
+
import { col, item, node } from "../native/describe";
|
|
35
|
+
import { leafKey, type NativeNode, type NativeUiEvent } from "../native/node";
|
|
36
|
+
import { actionBar, actionButton } from "../native/overlay";
|
|
37
|
+
import { PREVIEW_LIMITS, sanitizeDisplayLines } from "../render/render-utils";
|
|
38
|
+
import { theme } from "../theme/theme";
|
|
39
|
+
import { CountdownTimer } from "../chrome/countdown-timer";
|
|
40
|
+
|
|
41
|
+
/** Detail rows shown before the rest scrolls, so the composer beneath keeps its room. */
|
|
42
|
+
const MAX_DETAIL_ROWS: number = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
43
|
+
/** Enter and the approve key are ignored this long after mount, so a key sent as the prompt appears cannot answer it. */
|
|
44
|
+
export const APPROVE_ARM_MS = 500;
|
|
45
|
+
/** The Deny row while a composer draft holds Enter: sending it denies the call and steers the turn. */
|
|
46
|
+
const DENY_AND_SEND_LABEL = "Deny and send message";
|
|
47
|
+
|
|
48
|
+
type ApprovalAnswer = "once" | "session" | "deny";
|
|
49
|
+
|
|
50
|
+
interface ApprovalChoice {
|
|
51
|
+
readonly label: string;
|
|
52
|
+
readonly answer: ApprovalAnswer;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface ApprovalDialogCallbacks {
|
|
56
|
+
onApprove(): void;
|
|
57
|
+
onDeny(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The composer mounted beneath the dialog: it receives every key the dialog does not own. */
|
|
61
|
+
export interface ApprovalDialogDraft {
|
|
62
|
+
/** True while the composer holds a draft — text, attachments, or a paste still landing: keys then edit and send it. */
|
|
63
|
+
hasDraft(): boolean;
|
|
64
|
+
handleInput(keyData: string): void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ApprovalDialogOptions {
|
|
68
|
+
tui?: TUI;
|
|
69
|
+
/** The prompt with every detail in full, which the expand key swaps in (wrapped) for the summarized one. */
|
|
70
|
+
fullPrompt?: string;
|
|
71
|
+
/** Deny once this many ms pass without input (any key restarts the countdown). */
|
|
72
|
+
timeout?: number;
|
|
73
|
+
/** Called when {@link timeout} expires, before the dialog denies. */
|
|
74
|
+
onTimeout?: () => void;
|
|
75
|
+
/** Called when the countdown starts, as the dialog mounts. */
|
|
76
|
+
onTimeoutStart?: () => void;
|
|
77
|
+
/** Called each time a key restarts the countdown. */
|
|
78
|
+
onTimeoutReset?: () => void;
|
|
79
|
+
/** Route typing to the composer instead of dropping it. */
|
|
80
|
+
draft?: ApprovalDialogDraft;
|
|
81
|
+
/** Offer to approve this and later like calls for the rest of the session, as the row between Approve and Deny. */
|
|
82
|
+
session?: { label: string; onApprove(): void };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A prompt's detail rows: every line after the title, trailing blanks dropped. */
|
|
86
|
+
function promptDetails(lines: string[]): string[] {
|
|
87
|
+
const details = lines.slice(1);
|
|
88
|
+
while (details.length > 0 && details[details.length - 1]!.trim() === "") details.pop();
|
|
89
|
+
return details;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class ApprovalDialogComponent implements Component {
|
|
93
|
+
readonly #title: string;
|
|
94
|
+
readonly #summary: readonly string[];
|
|
95
|
+
readonly #full: readonly string[] | undefined;
|
|
96
|
+
#expanded = false;
|
|
97
|
+
readonly #panel: OverlayPanel;
|
|
98
|
+
readonly #body = new PanelRows();
|
|
99
|
+
readonly #choiceRows = new PanelRows();
|
|
100
|
+
readonly #footer = new PanelRows();
|
|
101
|
+
#scrollOffset = 0;
|
|
102
|
+
#bodyRows = MAX_DETAIL_ROWS;
|
|
103
|
+
/** In list order; the first (Approve) is highlighted on mount and Deny is last. */
|
|
104
|
+
readonly #choices: readonly ApprovalChoice[];
|
|
105
|
+
#choice = 0;
|
|
106
|
+
#closed = false;
|
|
107
|
+
#native: NativeNode | undefined;
|
|
108
|
+
/** Whether a draft held the keys when {@link #native} was described: it moves the highlight. */
|
|
109
|
+
#nativeDrafting = false;
|
|
110
|
+
readonly #armedAt = performance.now() + APPROVE_ARM_MS;
|
|
111
|
+
readonly #countdown: CountdownTimer | undefined;
|
|
112
|
+
|
|
113
|
+
/** `prompt`: the approval title on its first line, the details after it. */
|
|
114
|
+
constructor(
|
|
115
|
+
prompt: string,
|
|
116
|
+
private readonly callbacks: ApprovalDialogCallbacks,
|
|
117
|
+
private readonly options: ApprovalDialogOptions = {},
|
|
118
|
+
) {
|
|
119
|
+
const lines = sanitizeDisplayLines(prompt);
|
|
120
|
+
const title = lines[0] ?? "";
|
|
121
|
+
this.#title = title;
|
|
122
|
+
this.#summary = promptDetails(lines);
|
|
123
|
+
this.#full =
|
|
124
|
+
options.fullPrompt === undefined ? undefined : promptDetails(sanitizeDisplayLines(options.fullPrompt));
|
|
125
|
+
this.#choices = [
|
|
126
|
+
{ label: "Approve", answer: "once" },
|
|
127
|
+
...(options.session ? [{ label: options.session.label, answer: "session" as const }] : []),
|
|
128
|
+
{ label: "Deny", answer: "deny" },
|
|
129
|
+
];
|
|
130
|
+
this.#panel = new OverlayPanel(title, "zeta.overlay.approval");
|
|
131
|
+
this.#footer.setHeight(1);
|
|
132
|
+
this.#panel.addChild(this.#body);
|
|
133
|
+
this.#panel.addChild(this.#choiceRows);
|
|
134
|
+
this.#panel.addChild(new PanelDivider());
|
|
135
|
+
this.#panel.addChild(this.#footer);
|
|
136
|
+
if (options.timeout && options.timeout > 0) {
|
|
137
|
+
options.onTimeoutStart?.();
|
|
138
|
+
this.#countdown = new CountdownTimer(
|
|
139
|
+
options.timeout,
|
|
140
|
+
options.tui,
|
|
141
|
+
seconds => {
|
|
142
|
+
this.#panel.title = `${title} (${seconds}s)`;
|
|
143
|
+
},
|
|
144
|
+
() => {
|
|
145
|
+
options.onTimeout?.();
|
|
146
|
+
this.#finish("deny");
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
invalidate(): void {
|
|
153
|
+
this.#native = undefined;
|
|
154
|
+
this.#panel.invalidate();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
dispose(): void {
|
|
158
|
+
this.#closed = true;
|
|
159
|
+
this.#countdown?.dispose();
|
|
160
|
+
this.#panel.dispose();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
handleInput(keyData: string): void {
|
|
164
|
+
if (this.#closed) return;
|
|
165
|
+
this.#restartCountdown();
|
|
166
|
+
if (matchesAppApprove(keyData)) {
|
|
167
|
+
if (performance.now() >= this.#armedAt) this.#finish("once");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (this.#full && matchesAppToolsExpand(keyData)) {
|
|
171
|
+
this.#toggleExpanded();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const draft = this.options.draft;
|
|
175
|
+
// The composer's clear key (Ctrl+C, also a cancel key) clears a draft before it denies.
|
|
176
|
+
if (draft?.hasDraft() && getKeybindings().matches(keyData, "app.clear")) {
|
|
177
|
+
this.#editDraft(draft, keyData);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (matchesSelectCancel(keyData)) {
|
|
181
|
+
this.#finish("deny");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (draft?.hasDraft()) {
|
|
185
|
+
this.#editDraft(draft, keyData);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const page = Math.max(1, this.#bodyRows - 1);
|
|
189
|
+
if (matchesSelectUp(keyData)) this.#moveChoice(-1);
|
|
190
|
+
else if (matchesSelectDown(keyData)) this.#moveChoice(1);
|
|
191
|
+
else if (matchesSelectPageUp(keyData)) this.#scroll(-page);
|
|
192
|
+
else if (matchesSelectPageDown(keyData)) this.#scroll(page);
|
|
193
|
+
else if (getKeybindings().matches(keyData, "tui.select.confirm")) {
|
|
194
|
+
if (performance.now() >= this.#armedAt) this.#finish(this.#choices[this.#choice]!.answer);
|
|
195
|
+
}
|
|
196
|
+
// Enter never reaches an empty composer: it would submit nothing, or interrupt the run.
|
|
197
|
+
else if (draft && !getKeybindings().matches(keyData, "tui.input.submit")) this.#editDraft(draft, keyData);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#moveChoice(delta: number): void {
|
|
201
|
+
const count = this.#choices.length;
|
|
202
|
+
this.#choice = (this.#choice + delta + count) % count;
|
|
203
|
+
this.#native = undefined;
|
|
204
|
+
this.options.tui?.requestRender();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#editDraft(draft: ApprovalDialogDraft, keyData: string): void {
|
|
208
|
+
draft.handleInput(keyData);
|
|
209
|
+
this.options.tui?.requestRender();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Swap the summarized prompt for every detail, wrapped, or back, as the expand key does here;
|
|
214
|
+
* for hosts that intercept that key first. Restarts the countdown like any key. False (nothing
|
|
215
|
+
* changes) without a full prompt, so the host's own expand handling (tool previews) can run.
|
|
216
|
+
*/
|
|
217
|
+
toggleDetails(): boolean {
|
|
218
|
+
if (this.#closed || !this.#full) return false;
|
|
219
|
+
this.#restartCountdown();
|
|
220
|
+
this.#toggleExpanded();
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Any user input restarts the countdown (and its described form). */
|
|
225
|
+
#restartCountdown(): void {
|
|
226
|
+
if (!this.#countdown) return;
|
|
227
|
+
this.#countdown.reset();
|
|
228
|
+
this.options.onTimeoutReset?.();
|
|
229
|
+
this.#native = undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#toggleExpanded(): void {
|
|
233
|
+
this.#expanded = !this.#expanded;
|
|
234
|
+
this.#scrollOffset = 0;
|
|
235
|
+
this.#native = undefined;
|
|
236
|
+
this.options.tui?.requestRender();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The rows on show: the summary, or every detail once expanded. */
|
|
240
|
+
get #details(): readonly string[] {
|
|
241
|
+
return this.#expanded && this.#full ? this.#full : this.#summary;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
render(width: number): readonly string[] {
|
|
245
|
+
const innerWidth = Math.max(1, width - 4);
|
|
246
|
+
let lines = this.#detailLines(innerWidth);
|
|
247
|
+
const rows = Math.max(1, Math.min(lines.length, MAX_DETAIL_ROWS));
|
|
248
|
+
const overflows = lines.length > rows;
|
|
249
|
+
// Leave a column for the scrollbar once the details overflow.
|
|
250
|
+
if (overflows && innerWidth > 1) lines = this.#detailLines(innerWidth - 1);
|
|
251
|
+
this.#bodyRows = rows;
|
|
252
|
+
this.#scrollOffset = Math.max(0, Math.min(this.#scrollOffset, lines.length - rows));
|
|
253
|
+
if (overflows) {
|
|
254
|
+
const view = new ScrollView(lines, {
|
|
255
|
+
height: rows,
|
|
256
|
+
scrollbar: "auto",
|
|
257
|
+
theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
|
|
258
|
+
});
|
|
259
|
+
view.setScrollOffset(this.#scrollOffset);
|
|
260
|
+
this.#body.setLines(view.render(innerWidth));
|
|
261
|
+
} else {
|
|
262
|
+
this.#body.setLines(lines);
|
|
263
|
+
}
|
|
264
|
+
this.#body.setHeight(rows);
|
|
265
|
+
const drafting = this.options.draft?.hasDraft() ?? false;
|
|
266
|
+
// While a draft holds Enter the highlight sits on Deny (sending the draft denies), else on the arrows' choice.
|
|
267
|
+
const shown = drafting ? this.#choices.length - 1 : this.#choice;
|
|
268
|
+
this.#choiceRows.setLines(
|
|
269
|
+
this.#choices.map((choice, index) => {
|
|
270
|
+
const label = drafting && choice.answer === "deny" ? DENY_AND_SEND_LABEL : choice.label;
|
|
271
|
+
if (index !== shown) return truncateToWidth(` ${label}`, innerWidth, Ellipsis.Unicode);
|
|
272
|
+
const color = choice.answer === "deny" ? "error" : "accent";
|
|
273
|
+
const row = truncateToWidth(`${theme.nav.cursor} ${label}`, innerWidth, Ellipsis.Unicode);
|
|
274
|
+
return theme.bold(theme.fg(color, row));
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
this.#footer.setLines([theme.fg("dim", this.#footerHint(overflows, drafting))]);
|
|
278
|
+
return this.#panel.render(width);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* In the dock, in the composer's place and framed as the composer is (an
|
|
283
|
+
* `zeta.editor.*` root role), like the ask dialog, rather than a card over
|
|
284
|
+
* the transcript: the title, the details, the choices with the keyboard's
|
|
285
|
+
* highlight (a click takes one), then Details (with a full prompt), Deny,
|
|
286
|
+
* and the primary Approve, whose keys show in their tooltips.
|
|
287
|
+
*/
|
|
288
|
+
describe(): NativeNode {
|
|
289
|
+
const drafting = this.options.draft?.hasDraft() ?? false;
|
|
290
|
+
if (this.#native && this.#nativeDrafting === drafting) return this.#native;
|
|
291
|
+
const cancelKey = getKeybindings().getKeys("tui.select.cancel")[0];
|
|
292
|
+
const approveKey = appApproveKeys()[0];
|
|
293
|
+
const deny = actionButton("Deny", "deny", {
|
|
294
|
+
...(cancelKey ? { keys: cancelKey } : {}),
|
|
295
|
+
tone: "error",
|
|
296
|
+
title: `Deny this call${cancelKey ? ` ${formatTooltipKey(cancelKey)}` : ""}`,
|
|
297
|
+
});
|
|
298
|
+
const approve = actionButton("Approve", "approve", {
|
|
299
|
+
...(approveKey ? { keys: approveKey } : {}),
|
|
300
|
+
tone: "accent",
|
|
301
|
+
title: `Approve this call${approveKey ? ` ${formatTooltipKey(approveKey)}` : ""}`,
|
|
302
|
+
});
|
|
303
|
+
const expandKey = appToolsExpandKey();
|
|
304
|
+
const expand = this.#full
|
|
305
|
+
? [
|
|
306
|
+
actionButton(this.#expanded ? "Less" : "Details", "expand", {
|
|
307
|
+
keys: expandKey,
|
|
308
|
+
title: `${this.#expanded ? "Summarize" : "Show every detail of"} this call ${formatTooltipKey(expandKey)}`,
|
|
309
|
+
}),
|
|
310
|
+
]
|
|
311
|
+
: [];
|
|
312
|
+
const children: NativeNode[] = [
|
|
313
|
+
node("text", { text: this.#title, role: "zeta.approval.title", tone: "accent" }, undefined, "title"),
|
|
314
|
+
];
|
|
315
|
+
if (this.#details.length > 0) {
|
|
316
|
+
// One clipped row per detail like the text view; expanded details wrap.
|
|
317
|
+
const rows = this.#details.map((line, index) =>
|
|
318
|
+
node("text", this.#expanded ? { text: line } : { text: line, truncate: "end" }, undefined, String(index)),
|
|
319
|
+
);
|
|
320
|
+
children.push(node("col", { role: "zeta.approval.details" }, rows, "details"));
|
|
321
|
+
}
|
|
322
|
+
const choices = this.#choices.map(choice =>
|
|
323
|
+
item(choice.answer, {
|
|
324
|
+
label: choice.label,
|
|
325
|
+
role: "zeta.approval.choice",
|
|
326
|
+
tone: choice.answer === "deny" ? "error" : "accent",
|
|
327
|
+
}),
|
|
328
|
+
);
|
|
329
|
+
const selected = drafting ? "deny" : this.#choices[this.#choice]!.answer;
|
|
330
|
+
children.push(node("list", { selected, role: "zeta.approval.choices" }, choices, "choices"));
|
|
331
|
+
// The countdown, when one runs, leads the bar like the ask dialog's.
|
|
332
|
+
children.push(
|
|
333
|
+
actionBar([...(this.#countdown ? [this.#countdown.describe()] : []), null, ...expand, deny, approve]),
|
|
334
|
+
);
|
|
335
|
+
// Under the `zeta.editor.ask` prefix: Tern's console layout sizes an ask's rows to the
|
|
336
|
+
// composer column by that prefix, where a bare `zeta.editor.*` role spans the whole dock.
|
|
337
|
+
this.#native = col(children, { role: "zeta.editor.ask.approval", gap: "md" });
|
|
338
|
+
this.#nativeDrafting = drafting;
|
|
339
|
+
return this.#native;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Deny, Approve, and Details run exactly what Esc, the approve key, and the expand key run; a click on a choice takes it. */
|
|
343
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
344
|
+
if (this.#closed) return;
|
|
345
|
+
if (event.type === "action") {
|
|
346
|
+
if (event.act === "approve") this.#finish("once");
|
|
347
|
+
else if (event.act === "deny") this.#finish("deny");
|
|
348
|
+
else if (event.act === "expand") this.toggleDetails();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if ((event.type !== "select" && event.type !== "activate") || leafKey(event.key) !== "choices") return;
|
|
352
|
+
const choice = this.#choices.find(candidate => candidate.answer === event.item);
|
|
353
|
+
if (choice) this.#finish(choice.answer);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Detail rows at `width`: one clipped row per detail, or wrapped rows once expanded. */
|
|
357
|
+
#detailLines(width: number): string[] {
|
|
358
|
+
if (!this.#expanded) {
|
|
359
|
+
return this.#details.map(line => truncateToWidth(theme.fg("accent", line), width, Ellipsis.Unicode));
|
|
360
|
+
}
|
|
361
|
+
return this.#details.flatMap(line => (line === "" ? [""] : wrapTextWithAnsi(theme.fg("accent", line), width)));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
#footerHint(overflows: boolean, drafting: boolean): string {
|
|
365
|
+
const approve = formatKeyHints(appApproveKeys());
|
|
366
|
+
const expand = this.#full
|
|
367
|
+
? ` · ${formatKeyHints([appToolsExpandKey()])} ${this.#expanded ? "less" : "details"}`
|
|
368
|
+
: "";
|
|
369
|
+
const keys = `${approve} approve · ${editorKey("tui.select.cancel")} deny${expand}`;
|
|
370
|
+
if (drafting) return `${editorKey("tui.input.submit")} deny and send · ${keys}`;
|
|
371
|
+
const choose = `${editorKeys("tui.select.up", "tui.select.down")} choose · ${editorKey("tui.select.confirm")} select`;
|
|
372
|
+
const scroll = overflows ? ` · ${editorKeys("tui.select.pageUp", "tui.select.pageDown")} scroll` : "";
|
|
373
|
+
return `${choose} · ${keys}${scroll}${this.options.draft ? " · type to deny and steer" : ""}`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
#scroll(delta: number): void {
|
|
377
|
+
this.#scrollOffset = Math.max(0, this.#scrollOffset + delta);
|
|
378
|
+
this.options.tui?.requestRender();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#finish(answer: ApprovalAnswer): void {
|
|
382
|
+
if (this.#closed) return;
|
|
383
|
+
this.#closed = true;
|
|
384
|
+
this.#countdown?.dispose();
|
|
385
|
+
if (answer === "once") this.callbacks.onApprove();
|
|
386
|
+
else if (answer === "session") this.options.session?.onApprove();
|
|
387
|
+
else this.callbacks.onDeny();
|
|
388
|
+
}
|
|
389
|
+
}
|