@agi-fans/dsh-tui 0.21.0 → 0.22.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/lib/CHANGELOG.md +158 -1
- package/lib/chrome/box.d.ts +47 -16
- package/lib/chrome/box.js +172 -130
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.d.ts +15 -0
- package/lib/chrome/code-highlight.js +60 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +29 -2
- package/lib/chrome/command-output.js +100 -4
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/main-screen-renderer.d.ts +16 -0
- package/lib/chrome/main-screen-renderer.js +53 -5
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.js +30 -10
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/relative-time.d.ts +17 -0
- package/lib/chrome/relative-time.js +36 -0
- package/lib/chrome/relative-time.js.map +1 -1
- package/lib/chrome/secret-mask.d.ts +35 -0
- package/lib/chrome/secret-mask.js +180 -0
- package/lib/chrome/secret-mask.js.map +1 -0
- package/lib/chrome/status-config.d.ts +161 -51
- package/lib/chrome/status-config.js +237 -126
- package/lib/chrome/status-config.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -2
- package/lib/chrome/status-line.js +105 -63
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/terminal-colors.d.ts +85 -0
- package/lib/chrome/terminal-colors.js +230 -0
- package/lib/chrome/terminal-colors.js.map +1 -0
- package/lib/chrome/terminal-images.d.ts +9 -1
- package/lib/chrome/terminal-images.js +27 -4
- package/lib/chrome/terminal-images.js.map +1 -1
- package/lib/chrome/theme.d.ts +32 -9
- package/lib/chrome/theme.js +595 -498
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-renderers.js +69 -13
- package/lib/chrome/tool-renderers.js.map +1 -1
- package/lib/chrome/tools-list.d.ts +1 -4
- package/lib/chrome/tools-list.js +1 -41
- package/lib/chrome/tools-list.js.map +1 -1
- package/lib/chrome/welcome-tips.d.ts +20 -8
- package/lib/chrome/welcome-tips.js +22 -42
- package/lib/chrome/welcome-tips.js.map +1 -1
- package/lib/commands/auth-ids.d.ts +25 -0
- package/lib/commands/auth-ids.js +27 -0
- package/lib/commands/auth-ids.js.map +1 -0
- package/lib/commands/auth-input.d.ts +34 -0
- package/lib/commands/auth-input.js +96 -0
- package/lib/commands/auth-input.js.map +1 -0
- package/lib/commands/auth-providers.d.ts +39 -0
- package/lib/commands/auth-providers.js +125 -0
- package/lib/commands/auth-providers.js.map +1 -0
- package/lib/commands/auth.d.ts +12 -20
- package/lib/commands/auth.js +42 -229
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/btw.d.ts +13 -1
- package/lib/commands/btw.js +37 -104
- package/lib/commands/btw.js.map +1 -1
- package/lib/commands/diff.js +1 -96
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +1 -1
- package/lib/commands/export.js +3 -27
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +17 -1
- package/lib/commands/files.js +67 -107
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/jobs.d.ts +19 -4
- package/lib/commands/jobs.js +50 -7
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.js +108 -76
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/permission.js +15 -16
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +133 -16
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule.d.ts +10 -0
- package/lib/commands/schedule.js +45 -14
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.js +2 -10
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +14 -38
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.d.ts +14 -8
- package/lib/commands/terminal.js +19 -178
- package/lib/commands/terminal.js.map +1 -1
- package/lib/commands/trajectory.js +4 -1
- package/lib/commands/trajectory.js.map +1 -1
- package/lib/definition.d.ts +122 -22
- package/lib/definition.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +51 -0
- package/lib/input/actions.js +120 -0
- package/lib/input/actions.js.map +1 -0
- package/lib/input/editor.d.ts +0 -10
- package/lib/input/editor.js +11 -9
- package/lib/input/editor.js.map +1 -1
- package/lib/input/image-paste.d.ts +19 -1
- package/lib/input/image-paste.js +82 -1
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/keybindings-config.d.ts +12 -5
- package/lib/input/keybindings-config.js +30 -57
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +16 -0
- package/lib/input/keys.js +38 -3
- package/lib/input/keys.js.map +1 -1
- package/lib/input/wheel-scroll.d.ts +46 -0
- package/lib/input/wheel-scroll.js +69 -0
- package/lib/input/wheel-scroll.js.map +1 -0
- package/lib/prompt-intents.d.ts +182 -0
- package/lib/prompt-intents.js +127 -0
- package/lib/prompt-intents.js.map +1 -0
- package/lib/runtime/cancellable.d.ts +75 -0
- package/lib/runtime/cancellable.js +76 -0
- package/lib/runtime/cancellable.js.map +1 -0
- package/lib/runtime/composer-images.js +5 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.d.ts +17 -2
- package/lib/runtime/composer-shell.js +109 -43
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-recovery-review.d.ts +0 -1
- package/lib/runtime/file-recovery-review.js +29 -30
- package/lib/runtime/file-recovery-review.js.map +1 -1
- package/lib/runtime/file-recovery-store.js +5 -1
- package/lib/runtime/file-recovery-store.js.map +1 -1
- package/lib/runtime/file-review.js +1 -1
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/hook-settings.d.ts +2 -2
- package/lib/runtime/plain-tui.d.ts +2 -0
- package/lib/runtime/plain-tui.js +7 -1
- package/lib/runtime/plain-tui.js.map +1 -1
- package/lib/runtime/plugin-settings.d.ts +1 -1
- package/lib/runtime/plugin-settings.js +10 -8
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +57 -15
- package/lib/runtime/provider-local.js +1112 -471
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +5 -11
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.js +2 -0
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/terminal-modes.d.ts +47 -0
- package/lib/runtime/terminal-modes.js +86 -0
- package/lib/runtime/terminal-modes.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -5
- package/lib/runtime/terminal-notifications.js +4 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +36 -0
- package/lib/runtime/terminal-probe.js +37 -0
- package/lib/runtime/terminal-probe.js.map +1 -0
- package/lib/runtime/terminal-session.d.ts +57 -0
- package/lib/runtime/terminal-session.js +164 -0
- package/lib/runtime/terminal-session.js.map +1 -0
- package/lib/session/interaction-adapter.js +7 -8
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.js +1 -1
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/package-metadata.d.ts +8 -0
- package/lib/session/package-metadata.js +8 -0
- package/lib/session/package-metadata.js.map +1 -1
- package/lib/session/session-controller.d.ts +10 -11
- package/lib/session/session-controller.js +95 -13
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-tree.d.ts +2 -0
- package/lib/session/session-tree.js +2 -0
- package/lib/session/session-tree.js.map +1 -1
- package/lib/session/tui-settings.d.ts +40 -11
- package/lib/session/tui-settings.js +73 -34
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/views/autocomplete.d.ts +18 -2
- package/lib/views/autocomplete.js +51 -38
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/copy-targets.d.ts +12 -13
- package/lib/views/copy-targets.js +16 -46
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.js +18 -5
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +42 -6
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/history-search.js +15 -21
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkeys.d.ts +7 -1
- package/lib/views/hotkeys.js +49 -25
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-navigation.d.ts +62 -0
- package/lib/views/list-navigation.js +73 -0
- package/lib/views/list-navigation.js.map +1 -0
- package/lib/views/list-view.d.ts +99 -0
- package/lib/views/list-view.js +165 -0
- package/lib/views/list-view.js.map +1 -0
- package/lib/views/prompt-selector.d.ts +31 -3
- package/lib/views/prompt-selector.js +249 -125
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/session-files.d.ts +1 -2
- package/lib/views/session-files.js +1 -11
- package/lib/views/session-files.js.map +1 -1
- package/lib/views/settings-list.d.ts +10 -8
- package/lib/views/settings-list.js +116 -283
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/trajectory-timeline.d.ts +72 -0
- package/lib/views/trajectory-timeline.js +133 -0
- package/lib/views/trajectory-timeline.js.map +1 -0
- package/lib/views/trajectory.d.ts +15 -4
- package/lib/views/trajectory.js +557 -123
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +95 -25
- package/lib/views/transcript-render.js +501 -167
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-search.d.ts +39 -6
- package/lib/views/transcript-search.js +46 -11
- package/lib/views/transcript-search.js.map +1 -1
- package/lib/views/transcript-types.d.ts +63 -5
- package/lib/views/transcript-types.js +23 -11
- package/lib/views/transcript-types.js.map +1 -1
- package/package.json +67 -86
- package/lib/commands/advisor.d.ts +0 -15
- package/lib/commands/advisor.js +0 -200
- package/lib/commands/advisor.js.map +0 -1
- package/lib/commands/loop.d.ts +0 -52
- package/lib/commands/loop.js +0 -307
- package/lib/commands/loop.js.map +0 -1
- package/lib/commands/rate.d.ts +0 -33
- package/lib/commands/rate.js +0 -212
- package/lib/commands/rate.js.map +0 -1
- package/lib/commands/steer.d.ts +0 -5
- package/lib/commands/steer.js +0 -24
- package/lib/commands/steer.js.map +0 -1
- package/lib/commands/worktree.d.ts +0 -5
- package/lib/commands/worktree.js +0 -103
- package/lib/commands/worktree.js.map +0 -1
- package/lib/runtime/terminal-reader.d.ts +0 -11
- package/lib/runtime/terminal-reader.js +0 -87
- package/lib/runtime/terminal-reader.js.map +0 -1
- package/lib/views/agent-hub.d.ts +0 -32
- package/lib/views/agent-hub.js +0 -209
- package/lib/views/agent-hub.js.map +0 -1
- package/lib/views/copy-selector.d.ts +0 -54
- package/lib/views/copy-selector.js +0 -170
- package/lib/views/copy-selector.js.map +0 -1
|
@@ -6,8 +6,9 @@ import { rankSearchResults } from "../input/fuzzy-search.js";
|
|
|
6
6
|
import { renderEditor, renderFramedBlock } from "../chrome/box.js";
|
|
7
7
|
import { renderMarkdown } from "../chrome/markdown.js";
|
|
8
8
|
import { BOX, SYMBOL } from "../chrome/theme.js";
|
|
9
|
-
import { padToWidth, truncateToWidth, visibleWidth,
|
|
9
|
+
import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
|
|
10
10
|
import { formatHotkeyKeys, formatOverlayHint } from "./hotkey-format.js";
|
|
11
|
+
import { renderListRow } from "./list-view.js";
|
|
11
12
|
/** Maximum option rows retained in the prompt overlay before it windows. */
|
|
12
13
|
export const PROMPT_SELECTOR_MAX_VISIBLE = 10;
|
|
13
14
|
// Every key the prompt handlers accept, covering the inline card, the full-screen
|
|
@@ -52,7 +53,7 @@ export function maskPromptSecret(input) {
|
|
|
52
53
|
}
|
|
53
54
|
/** Options matching the current full-screen selector query. */
|
|
54
55
|
export function filteredPromptOptions(request, query, collapsed) {
|
|
55
|
-
if (request.presentation === '
|
|
56
|
+
if (request.presentation === 'tree')
|
|
56
57
|
return promptTreeRows(request, collapsed, query).map(row => row.option);
|
|
57
58
|
const options = request.options ?? [];
|
|
58
59
|
if (request.filterable !== true)
|
|
@@ -80,11 +81,17 @@ function treePreview(option, theme, width) {
|
|
|
80
81
|
cache = new Map();
|
|
81
82
|
previewCache.set(option, cache);
|
|
82
83
|
}
|
|
83
|
-
const key = `${width}:${theme.
|
|
84
|
+
const key = `${width}:${theme.key}`;
|
|
84
85
|
const cached = cache.get(key);
|
|
85
86
|
if (cached !== undefined)
|
|
86
87
|
return cached;
|
|
87
|
-
const
|
|
88
|
+
const source = option.preview ?? option.label;
|
|
89
|
+
// One default for one field: plain text unless the caller says markdown. The
|
|
90
|
+
// tree page used to render markdown unconditionally while the card rendered
|
|
91
|
+
// text, so the same `preview` meant two things depending on the surface.
|
|
92
|
+
const lines = option.previewFormat === 'markdown'
|
|
93
|
+
? renderMarkdown(source, theme, width)
|
|
94
|
+
: wrapTextStable(source, width);
|
|
88
95
|
if (cache.size >= 4)
|
|
89
96
|
cache.clear();
|
|
90
97
|
cache.set(key, lines);
|
|
@@ -93,7 +100,7 @@ function treePreview(option, theme, width) {
|
|
|
93
100
|
function treePageLayout(request, width, height) {
|
|
94
101
|
const actions = (request.actions ?? []).map(item => `${item.key} ${item.label}`).join(' · ');
|
|
95
102
|
const hint = `↑↓ · ←→ fold${width >= 90 ? ' · type search' : ''}${actions === '' ? '' : ` · ${actions}`}${width >= 90 ? ' · Ctrl+↑↓ preview' : ''}`;
|
|
96
|
-
const hints =
|
|
103
|
+
const hints = wrapTextStable(hint, width - 4).slice(0, Math.max(1, Math.floor((height - 6) / 2)));
|
|
97
104
|
const middle = height - 6 - hints.length;
|
|
98
105
|
const listHeight = width >= 90 ? middle : Math.max(1, Math.floor((middle - 1) / 2));
|
|
99
106
|
const previewHeight = width >= 90 ? middle : middle - listHeight - 1;
|
|
@@ -121,11 +128,31 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
|
|
|
121
128
|
const row = rows[at];
|
|
122
129
|
const marker = at === selected ? theme.fg('accent', SYMBOL.cursor + ' ') : ' ';
|
|
123
130
|
const fold = row.expandable ? state.collapsed?.has(row.option.value ?? row.option.label) && input === '' ? '▸ ' : '▾ ' : ' ';
|
|
124
|
-
const badge = row.option.badge?.label === undefined ? '' : `[${row.option.badge.label}] `;
|
|
125
|
-
const text = badge + row.option.label;
|
|
126
131
|
const prefixLimit = Math.max(1, Math.floor(listWidth / 3));
|
|
127
132
|
const prefix = row.prefix.length <= prefixLimit ? row.prefix : '…' + row.prefix.slice(-(prefixLimit - 1));
|
|
128
|
-
|
|
133
|
+
const head = marker + theme.fg('dim', prefix + fold);
|
|
134
|
+
/**
|
|
135
|
+
* A row says everything the option knows.
|
|
136
|
+
*
|
|
137
|
+
* The card mode already drew the badge in its tone and the description in a
|
|
138
|
+
* column; this page drew neither, so a tree opened full screen lost the
|
|
139
|
+
* agent's phase colour and its elapsed time — the two things that tell two
|
|
140
|
+
* similar-looking nodes apart.
|
|
141
|
+
*/
|
|
142
|
+
const badge = row.option.badge === undefined
|
|
143
|
+
? ''
|
|
144
|
+
: theme.fg(row.option.badge.tone ?? 'muted', `[${row.option.badge.label}] `);
|
|
145
|
+
const label = badge + row.option.label;
|
|
146
|
+
// A heading is focusable but not an answer, so it never takes the cursor colour.
|
|
147
|
+
const text = row.option.disabled === true
|
|
148
|
+
? theme.fg('muted', label)
|
|
149
|
+
: at === selected ? theme.fg('accent', label) : theme.fg('text', label);
|
|
150
|
+
const used = visibleWidth(head) + visibleWidth(label);
|
|
151
|
+
const room = listWidth - used - 2;
|
|
152
|
+
const description = row.option.description === undefined || row.option.description === '' || room < 8
|
|
153
|
+
? ''
|
|
154
|
+
: theme.fg('dim', ' ' + truncateToWidth(row.option.description, room));
|
|
155
|
+
list.push(fit(head + text + description, listWidth));
|
|
129
156
|
}
|
|
130
157
|
if (rows.length === 0)
|
|
131
158
|
list.push(fit(theme.fg('dim', state.request.emptyText ?? 'No matching conversation turns.'), listWidth));
|
|
@@ -175,26 +202,43 @@ export function movePromptTree(state, direction, query) {
|
|
|
175
202
|
}
|
|
176
203
|
/** Start on a matching node instead of a context-only ancestor. */
|
|
177
204
|
export function searchPromptSelection(state, query) {
|
|
178
|
-
const selected = state.request.presentation === '
|
|
205
|
+
const selected = state.request.presentation === 'tree'
|
|
179
206
|
? Math.max(0, promptTreeRows(state.request, state.collapsed, query).findIndex(row => row.matched)) : 0;
|
|
180
207
|
return { ...state, selected, previewScroll: 0 };
|
|
181
208
|
}
|
|
182
|
-
/** Visible option window centered around the selected row. */
|
|
183
209
|
export function promptSelectorVisibleRange(count, selected, maxVisible = PROMPT_SELECTOR_MAX_VISIBLE) {
|
|
184
210
|
const max = Math.max(1, maxVisible);
|
|
185
211
|
const index = Math.max(0, Math.min(selected, Math.max(0, count - 1)));
|
|
186
212
|
const start = Math.max(0, Math.min(index - Math.floor(max / 2), Math.max(0, count - max)));
|
|
187
213
|
return { start, end: Math.min(count, start + max) };
|
|
188
214
|
}
|
|
189
|
-
function optionRow(option, index, state, theme, width
|
|
190
|
-
|
|
191
|
-
|
|
215
|
+
function optionRow(option, index, state, theme, width,
|
|
216
|
+
/**
|
|
217
|
+
* The option's index in the request, when the visible row is a filtered one.
|
|
218
|
+
*
|
|
219
|
+
* Checked state is keyed by the request's order, not the filtered list's: a
|
|
220
|
+
* query that hides the row above must not move every checkbox below it.
|
|
221
|
+
*/
|
|
222
|
+
identity = index) {
|
|
223
|
+
// The multi-select marker is part of the label so the shared row keeps one
|
|
224
|
+
// cursor cell and one description gap for every prompt, checked or not.
|
|
225
|
+
if (option.disabled === true) {
|
|
226
|
+
// Same reason as the tree: focusable, dimmed, and not a value.
|
|
227
|
+
const row = { label: option.label, ...(option.description === undefined ? {} : { description: option.description }), secondary: true };
|
|
228
|
+
return renderListRow(row, theme, Math.max(1, width), { selected: index === state.selected });
|
|
229
|
+
}
|
|
230
|
+
const checked = state.checked.has(identity);
|
|
192
231
|
const marker = state.request.multiSelect === true
|
|
193
|
-
? theme.fg(
|
|
232
|
+
? theme.fg(checked ? 'success' : 'dim', checked ? '[x] ' : '[ ] ')
|
|
194
233
|
: '';
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
const row = {
|
|
235
|
+
label: marker + option.label,
|
|
236
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
237
|
+
// A badge is part of what the row says — a session's status, a command's
|
|
238
|
+
// kind. The paged list drew it, so the card must draw it too.
|
|
239
|
+
...(option.badge?.label === undefined ? {} : { value: theme.fg(option.badge.tone ?? 'muted', option.badge.label) }),
|
|
240
|
+
};
|
|
241
|
+
return renderListRow(row, theme, Math.max(1, width), { selected: index === state.selected });
|
|
198
242
|
}
|
|
199
243
|
function fit(text, width) {
|
|
200
244
|
if (width <= 0)
|
|
@@ -222,102 +266,51 @@ function pageDivider(theme, width) {
|
|
|
222
266
|
function pageBottom(theme, width) {
|
|
223
267
|
return pageBorder(theme, BOX.bottomLeft + BOX.horizontal.repeat(Math.max(0, width - 2)) + BOX.bottomRight);
|
|
224
268
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const visibleChecked = new Set(options.flatMap((option, index) => state.checked.has(state.request.options?.indexOf(option) ?? -1) ? [index] : []));
|
|
243
|
-
if (pageHeight < 10 || width < 16) {
|
|
244
|
-
return renderPromptSelector({ ...state, request: { ...state.request, options }, checked: visibleChecked }, theme, width, input, inputCursor, Math.max(1, pageHeight - 8));
|
|
245
|
-
}
|
|
246
|
-
const selected = Math.max(0, Math.min(state.selected, Math.max(0, options.length - 1)));
|
|
247
|
-
const compact = state.request.optionLayout === 'compact';
|
|
248
|
-
const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`).join(' · ');
|
|
249
|
-
const hints = wrapText(formatOverlayHint([HOTKEY_TEXT, HOTKEY_NAVIGATE, ...(state.request.multiSelect === true ? [HOTKEY_TOGGLE] : []), HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
250
|
-
+ (actionHints === '' ? '' : ' · ' + actionHints), width - 4).slice(0, Math.max(1, Math.floor((height - 6) / 2)));
|
|
251
|
-
const fixedRows = (compact ? 9 : 11) + hints.length - 1;
|
|
252
|
-
const visibleCount = compact
|
|
253
|
-
? Math.max(1, pageHeight - fixedRows)
|
|
254
|
-
: Math.max(1, Math.floor((pageHeight - fixedRows) / 4));
|
|
255
|
-
const { start, end } = promptSelectorVisibleRange(options.length, selected, visibleCount);
|
|
256
|
-
const detail = state.request.detail === undefined || state.request.detail === ''
|
|
257
|
-
? ''
|
|
258
|
-
: ' ' + theme.fg('muted', `(${state.request.detail})`);
|
|
259
|
-
const heading = theme.bold(state.request.title) + detail;
|
|
260
|
-
const lines = compact
|
|
261
|
-
? [
|
|
262
|
-
pageTop(theme, width, appName),
|
|
263
|
-
pageRow(theme, ' ' + heading, width),
|
|
264
|
-
pageDivider(theme, width),
|
|
265
|
-
]
|
|
266
|
-
: [
|
|
267
|
-
pageTop(theme, width, appName),
|
|
268
|
-
pageRow(theme, '', width),
|
|
269
|
-
pageRow(theme, ' ' + heading, width),
|
|
270
|
-
pageRow(theme, '', width),
|
|
271
|
-
pageDivider(theme, width),
|
|
272
|
-
];
|
|
273
|
-
const searchPrefix = theme.fg('dim', '> ');
|
|
274
|
-
const searchText = truncateToWidth(input, Math.max(1, width - 8));
|
|
275
|
-
const searchRow = lines.length;
|
|
276
|
-
lines.push(pageRow(theme, searchPrefix + searchText, width), pageRow(theme, '', width));
|
|
277
|
-
if (options.length === 0) {
|
|
278
|
-
// The generic line is a fallback only: this selector also serves approvals
|
|
279
|
-
// and questions, whose empty states have nothing to do with sessions.
|
|
280
|
-
const fallback = state.request.emptyText ?? 'No matching options.';
|
|
281
|
-
const empty = input === '' ? fallback : `No match for “${input}”.`;
|
|
282
|
-
lines.push(pageRow(theme, ' ' + theme.fg('muted', empty), width));
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
for (let index = start; index < end; index += 1) {
|
|
286
|
-
const option = options[index];
|
|
287
|
-
if (option === undefined)
|
|
288
|
-
continue;
|
|
289
|
-
if (compact) {
|
|
290
|
-
lines.push(pageRow(theme, optionRow(option, index, { ...state, selected, checked: visibleChecked }, theme, Math.max(1, width - 6)), width));
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
const active = index === selected;
|
|
294
|
-
const marker = active ? theme.fg('accent', SYMBOL.cursor + ' ') : ' ';
|
|
295
|
-
const check = state.request.multiSelect === true ? visibleChecked.has(index) ? '[x] ' : '[ ] ' : '';
|
|
296
|
-
const label = active ? theme.bold(option.label) : option.label;
|
|
297
|
-
lines.push(pageRow(theme, marker + check + label, width));
|
|
298
|
-
lines.push(pageRow(theme, ' ' + theme.fg('dim', option.preview ?? ''), width));
|
|
299
|
-
const badge = optionBadge(option, theme);
|
|
300
|
-
const description = option.description === undefined ? '' : theme.fg('dim', option.description);
|
|
301
|
-
const separator = description !== '' && badge !== '' ? theme.fg('dim', ' · ') : '';
|
|
302
|
-
lines.push(pageRow(theme, ' ' + description + separator + badge, width), pageRow(theme, '', width));
|
|
269
|
+
const documentCache = new WeakMap();
|
|
270
|
+
/**
|
|
271
|
+
* Case-insensitive find over rows the reader can already see.
|
|
272
|
+
*
|
|
273
|
+
* A rendered page has no source model to search, so the rows are the document.
|
|
274
|
+
* Offsets are display cells, not string indexes, so CJK and emoji highlights land
|
|
275
|
+
* on the glyphs the reader pointed at rather than shearing them.
|
|
276
|
+
*/
|
|
277
|
+
export function findInRows(rows, query) {
|
|
278
|
+
const needle = query.trim().toLowerCase();
|
|
279
|
+
if (needle === '')
|
|
280
|
+
return [];
|
|
281
|
+
const matches = [];
|
|
282
|
+
for (const [row, line] of rows.entries()) {
|
|
283
|
+
const plain = stripAnsi(line).toLowerCase();
|
|
284
|
+
for (let at = plain.indexOf(needle); at >= 0; at = plain.indexOf(needle, at + needle.length)) {
|
|
285
|
+
matches.push({ row, start: visibleWidth(stripAnsi(line).slice(0, at)), width: visibleWidth(query.trim()) });
|
|
303
286
|
}
|
|
304
287
|
}
|
|
305
|
-
|
|
306
|
-
const targetBeforeFooter = Math.max(0, pageHeight - footerRows);
|
|
307
|
-
if (lines.length > targetBeforeFooter)
|
|
308
|
-
lines.length = targetBeforeFooter;
|
|
309
|
-
while (lines.length < targetBeforeFooter)
|
|
310
|
-
lines.push(pageRow(theme, '', width));
|
|
311
|
-
const position = options.length === 0 ? '' : `${selected + 1}/${options.length}`;
|
|
312
|
-
lines.push(pageRow(theme, theme.fg('dim', position.trim()), width), ...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)), pageRow(theme, '', width), pageBottom(theme, width));
|
|
313
|
-
const cursorColumn = Math.min(Math.max(1, width - 3), 4 + visibleWidth(input.slice(0, inputCursor)));
|
|
314
|
-
return {
|
|
315
|
-
lines,
|
|
316
|
-
cursor: { row: searchRow, column: cursorColumn },
|
|
317
|
-
};
|
|
288
|
+
return matches;
|
|
318
289
|
}
|
|
319
|
-
|
|
320
|
-
function
|
|
290
|
+
/** Invert the matched cells of one styled row. */
|
|
291
|
+
export function highlightMatches(line, spans, theme) {
|
|
292
|
+
if (spans.length === 0)
|
|
293
|
+
return line;
|
|
294
|
+
const plain = stripAnsi(line);
|
|
295
|
+
const total = visibleWidth(plain);
|
|
296
|
+
const ordered = [...spans].sort((left, right) => left.start - right.start);
|
|
297
|
+
let out = '';
|
|
298
|
+
let at = 0;
|
|
299
|
+
for (const span of ordered) {
|
|
300
|
+
const start = Math.max(at, Math.min(span.start, total));
|
|
301
|
+
const end = Math.min(total, Math.max(start, span.start + span.width));
|
|
302
|
+
if (start > at)
|
|
303
|
+
out += sliceCells(line, at, start - at);
|
|
304
|
+
if (end > start)
|
|
305
|
+
out += theme.inverse(sliceCells(line, start, end - start));
|
|
306
|
+
at = end;
|
|
307
|
+
}
|
|
308
|
+
if (at < total)
|
|
309
|
+
out += sliceCells(line, at, total - at);
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
/** The rows a rendered document shows, shared with the find that searches them. */
|
|
313
|
+
export function documentRows(request, theme, width) {
|
|
321
314
|
let cache = documentCache.get(request);
|
|
322
315
|
if (cache === undefined) {
|
|
323
316
|
cache = new Map();
|
|
@@ -333,6 +326,21 @@ function documentRows(request, theme, width) {
|
|
|
333
326
|
cache.set(key, lines);
|
|
334
327
|
return lines;
|
|
335
328
|
}
|
|
329
|
+
/**
|
|
330
|
+
* Does this prompt need the whole screen?
|
|
331
|
+
*
|
|
332
|
+
* Only a tree does. Length is not the question — a long list is still a list, and
|
|
333
|
+
* the card windows its rows, filters by typing, and draws the selected row's
|
|
334
|
+
* preview — so counting rows was the wrong variable. Choosing is a different act
|
|
335
|
+
* from reading, and a choice belongs beside the conversation it interrupted.
|
|
336
|
+
*/
|
|
337
|
+
export function promptNeedsPage(request) {
|
|
338
|
+
// A list is a choice, and a choice belongs beside the conversation whatever its
|
|
339
|
+
// length: the card windows its rows, filters by typing, and shows the selected
|
|
340
|
+
// row's preview. Counting rows was the wrong variable — a long list is still a
|
|
341
|
+
// list. Only a tree has structure the card cannot show.
|
|
342
|
+
return request.presentation === 'tree';
|
|
343
|
+
}
|
|
336
344
|
/** Render a bounded, scrollable Markdown document with fixed actions. */
|
|
337
345
|
function documentActionRows(state, theme, width, pageHeight) {
|
|
338
346
|
const actionRows = [];
|
|
@@ -368,7 +376,7 @@ export function renderImagePreviewPage(state, theme, width, height, appName, pro
|
|
|
368
376
|
pageRow(theme, theme.fg('muted', image.description), width), pageDivider(theme, width)];
|
|
369
377
|
const top = lines.length;
|
|
370
378
|
const canOpen = state.request.options?.some(option => option.value === 'open') === true;
|
|
371
|
-
const fallback = protocol === undefined ?
|
|
379
|
+
const fallback = protocol === undefined ? wrapTextStable('Inline images are unavailable in this terminal. '
|
|
372
380
|
+ (canOpen ? 'Use Open to view the original.' : 'This attachment has no local original to open.'), width - 8) : [];
|
|
373
381
|
for (let at = 0; at < bodyRows; at++)
|
|
374
382
|
lines.push(pageRow(theme, ' ' + (fallback[at] ?? ''), width));
|
|
@@ -386,9 +394,9 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
386
394
|
}
|
|
387
395
|
const feedback = state.feedback === true;
|
|
388
396
|
const actionRows = documentActionRows(state, theme, width, pageHeight);
|
|
389
|
-
const readerInput =
|
|
397
|
+
const readerInput = state.documentInput !== undefined;
|
|
390
398
|
const customHints = (state.request.actions ?? []).map(action => `${action.key.toUpperCase()} ${action.label}`);
|
|
391
|
-
let readerHints = source === undefined ? [] :
|
|
399
|
+
let readerHints = source === undefined ? [] : wrapTextStable(readerInput ? 'Enter apply · Esc cancel'
|
|
392
400
|
: (state.request.interrupt === undefined ? '' : 'Ctrl+C interrupt · ') + 'W wrap · Alt+←→ pan · ↑↓/PgUp/PgDn scroll · ' + (state.request.documentTail === true ? 'Home start · End follow' : 'Home/End edges') + ' · / find · Ctrl+N/P match · G line' + (source.diff ? ' · [/] hunk' : '')
|
|
393
401
|
+ ' · ' + ['Tab/←→ actions', 'Enter activate', ...customHints, 'Esc back'].join(' · '), width - 4);
|
|
394
402
|
const maxHints = Math.max(1, Math.min(4, pageHeight - 8 - actionRows.length));
|
|
@@ -411,6 +419,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
411
419
|
const visible = document.slice(start, start + bodyRows);
|
|
412
420
|
const query = state.documentQuery ?? '';
|
|
413
421
|
const matches = source === undefined ? [] : documentMatches(source, query);
|
|
422
|
+
const textMatches = source === undefined ? findInRows(document, query) : [];
|
|
414
423
|
const matching = new Set(matches);
|
|
415
424
|
if (layout !== undefined)
|
|
416
425
|
for (let at = 0; at < visible.length; at++) {
|
|
@@ -419,6 +428,15 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
419
428
|
visible[at] = theme.inverse(layout.gutterWidth === 0 ? row : '›' + row.slice(1));
|
|
420
429
|
}
|
|
421
430
|
}
|
|
431
|
+
if (textMatches.length > 0) {
|
|
432
|
+
// Paint the hits themselves, so the reader sees the word, not just the row.
|
|
433
|
+
for (const match of textMatches) {
|
|
434
|
+
const at = match.row - start;
|
|
435
|
+
if (at < 0 || at >= visible.length)
|
|
436
|
+
continue;
|
|
437
|
+
visible[at] = highlightMatches(visible[at] ?? '', [match], theme);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
422
440
|
while (visible.length < bodyRows)
|
|
423
441
|
visible.push('');
|
|
424
442
|
const subject = state.request.presentation === 'document' ? 'document' : 'plan';
|
|
@@ -455,6 +473,19 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
455
473
|
};
|
|
456
474
|
}
|
|
457
475
|
else {
|
|
476
|
+
if (source === undefined && (readerInput || query !== '')) {
|
|
477
|
+
// A rendered page has no line numbers to cite, so the status is the find
|
|
478
|
+
// itself: the query, how many hits it has, and where the reader is in them.
|
|
479
|
+
const index = textMatches.findIndex(match => match.row === state.documentTarget);
|
|
480
|
+
const hits = textMatches.length === 0
|
|
481
|
+
? 'No match'
|
|
482
|
+
: `${index < 0 ? 1 : index + 1}/${textMatches.length} matching rows`;
|
|
483
|
+
const label = state.documentInput === 'search' ? 'Find: ' : '';
|
|
484
|
+
const value = input.replace(/\r?\n/gu, ' ');
|
|
485
|
+
const status = readerInput ? label + value : `${hits} · “${query}”`;
|
|
486
|
+
cursor = { row: lines.length, column: Math.min(width - 3, 2 + visibleWidth(label + value.slice(0, inputCursor))) };
|
|
487
|
+
lines.push(pageRow(theme, theme.fg('muted', status), width));
|
|
488
|
+
}
|
|
458
489
|
if (source !== undefined) {
|
|
459
490
|
const row = layout?.sourceRows[start] ?? 0;
|
|
460
491
|
const model = documentModel(source);
|
|
@@ -476,7 +507,11 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
476
507
|
? 'Tab/←→ select · Enter activate · ' + state.request.actions.map(action => `${action.key.toUpperCase()} ${action.label}`).join(' · ') + ' · Esc back'
|
|
477
508
|
: formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
478
509
|
+ (state.request.presentation === 'plan-review' ? ' · Ctrl+X editor' : '');
|
|
479
|
-
|
|
510
|
+
// A page long enough to need scrolling is a page someone will want to search.
|
|
511
|
+
const pageHint = source === undefined && state.request.presentation === 'document'
|
|
512
|
+
? `[${shortcuts} · / find · Ctrl+N/P match]`
|
|
513
|
+
: `[${shortcuts}]`;
|
|
514
|
+
lines.push(...(source === undefined ? [pageHint] : readerHints).map(hint => pageRow(theme, theme.fg('dim', hint), width)));
|
|
480
515
|
lines.push(pageBottom(theme, width));
|
|
481
516
|
}
|
|
482
517
|
return {
|
|
@@ -488,9 +523,16 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
488
523
|
cursorVisible: feedback || readerInput,
|
|
489
524
|
};
|
|
490
525
|
}
|
|
526
|
+
/** Rows of the selected option's preview the card shows before it truncates. */
|
|
527
|
+
const CARD_PREVIEW_LINES = 3;
|
|
528
|
+
/** The reader's query as it reads in a sentence, quoted so blanks are visible. */
|
|
529
|
+
function quote(text) {
|
|
530
|
+
return `“${text}”`;
|
|
531
|
+
}
|
|
491
532
|
/** Render the prompt card and its answer editor as one bottom-of-screen overlay. */
|
|
492
533
|
export function renderPromptSelector(state, theme, width, input, inputCursor, maxVisible = PROMPT_SELECTOR_MAX_VISIBLE) {
|
|
493
534
|
const options = filteredPromptOptions(state.request, input, state.collapsed);
|
|
535
|
+
const filtering = input !== '';
|
|
494
536
|
const contentWidth = Math.max(1, width - 4);
|
|
495
537
|
const body = [state.request.question];
|
|
496
538
|
if (state.request.wait !== undefined) {
|
|
@@ -503,22 +545,65 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
503
545
|
body.push('', state.request.detail);
|
|
504
546
|
if (options.length > 0) {
|
|
505
547
|
const { start, end } = promptSelectorVisibleRange(options.length, state.selected, maxVisible);
|
|
506
|
-
body.push('', ...options.slice(start, end).map((option, offset) => optionRow(option, start + offset, state, theme, contentWidth)));
|
|
548
|
+
body.push('', ...options.slice(start, end).map((option, offset) => optionRow(option, start + offset, state, theme, contentWidth, (state.request.options ?? []).indexOf(option))));
|
|
507
549
|
if (options.length > maxVisible) {
|
|
508
550
|
body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · scroll for more`));
|
|
509
551
|
}
|
|
552
|
+
// The paged list draws the selected row's preview beside the rows; the card
|
|
553
|
+
// gives it three lines underneath, so demoting a previewed list keeps the
|
|
554
|
+
// content it was being browsed for.
|
|
555
|
+
const preview = options[state.selected]?.preview;
|
|
556
|
+
if (preview !== undefined && preview !== '') {
|
|
557
|
+
const wrapped = state.request.options?.[state.selected]?.previewFormat === 'markdown'
|
|
558
|
+
? renderMarkdown(preview, theme, contentWidth)
|
|
559
|
+
: wrapTextStable(preview, contentWidth);
|
|
560
|
+
body.push('', ...wrapped.slice(0, CARD_PREVIEW_LINES).map(line => theme.fg('dim', ' ' + line)));
|
|
561
|
+
if (wrapped.length > CARD_PREVIEW_LINES) {
|
|
562
|
+
body.push(theme.fg('dim', ` … ${wrapped.length - CARD_PREVIEW_LINES} more lines`));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
// A filter that matches nothing has to say so. An empty frame reads as a
|
|
568
|
+
// broken list, and the reader cannot tell a typo from an empty transcript.
|
|
569
|
+
body.push('', theme.fg('dim', ` ${state.request.emptyText
|
|
570
|
+
?? (filtering ? `No match for ${quote(input)}.` : 'Nothing to choose.')}`));
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* The query, drawn where the reader types it.
|
|
574
|
+
*
|
|
575
|
+
* Every filterable card filters on the composer text, and the paged list used
|
|
576
|
+
* to print it — so demoting a list to a card made the query invisible: the rows
|
|
577
|
+
* narrowed while the reader's own words appeared nowhere, and a card with no
|
|
578
|
+
* hits looked broken rather than empty.
|
|
579
|
+
*/
|
|
580
|
+
const filterRow = state.request.filterable === true && state.request.presentation !== 'progress'
|
|
581
|
+
? body.length
|
|
582
|
+
: undefined;
|
|
583
|
+
if (filterRow !== undefined) {
|
|
584
|
+
// The query and what it kept belong on one line: the reader typed one thing
|
|
585
|
+
// and the list answered with a count.
|
|
586
|
+
const count = filtering ? theme.fg('dim', ` · ${options.length === 0 ? 'no match' : `${state.selected + 1}/${options.length}`}`) : '';
|
|
587
|
+
body.push('', input === ''
|
|
588
|
+
? theme.fg('muted', ' ⌕ type to filter') + count
|
|
589
|
+
: theme.fg('text', ' ⌕ ') + input + count);
|
|
510
590
|
}
|
|
591
|
+
// A request that has options but no match has nothing to submit: advertising
|
|
592
|
+
// `enter select` there promises a key the handler swallows.
|
|
593
|
+
const exhausted = options.length === 0 && (state.request.options?.length ?? 0) > 0;
|
|
511
594
|
const submit = state.request.submitLabel?.trim()
|
|
512
595
|
|| (options.length === 0 ? 'answer' : state.request.multiSelect === true ? 'confirm' : 'select');
|
|
513
596
|
// The request supplies the submit verb; the key itself comes from the catalog.
|
|
514
597
|
const select = `${formatHotkeyKeys(HOTKEY_SELECT)} ${submit}`;
|
|
515
598
|
const navigation = state.request.presentation === 'progress'
|
|
516
599
|
? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog')
|
|
517
|
-
:
|
|
518
|
-
?
|
|
519
|
-
:
|
|
520
|
-
? `${
|
|
521
|
-
:
|
|
600
|
+
: exhausted
|
|
601
|
+
? formatOverlayHint([HOTKEY_CANCEL])
|
|
602
|
+
: options.length === 0
|
|
603
|
+
? `${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`
|
|
604
|
+
: state.request.multiSelect === true
|
|
605
|
+
? `${formatOverlayHint([HOTKEY_NAVIGATE])} · ${formatOverlayHint([HOTKEY_TOGGLE])} · ${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`
|
|
606
|
+
: `${formatOverlayHint([HOTKEY_NAVIGATE])} · ${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`;
|
|
522
607
|
const dismiss = state.request.dismissLabel;
|
|
523
608
|
const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`);
|
|
524
609
|
const hints = [
|
|
@@ -535,16 +620,50 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
535
620
|
applyBg: false,
|
|
536
621
|
}, theme);
|
|
537
622
|
if (state.request.allowCustom === false || state.request.presentation === 'progress') {
|
|
623
|
+
// With a filter line the caret belongs at its end, where the next character
|
|
624
|
+
// will land; without one it stays on body padding, because the selector glyph
|
|
625
|
+
// owns focus while no text editor is present.
|
|
538
626
|
return {
|
|
539
627
|
lines: card,
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
cursorVisible:
|
|
628
|
+
cursor: filterRow === undefined
|
|
629
|
+
? { row: Math.min(1, Math.max(0, card.length - 1)), column: 1 }
|
|
630
|
+
: { row: filterRow + 2, column: 4 + visibleWidth(input) },
|
|
631
|
+
cursorVisible: filterRow !== undefined,
|
|
544
632
|
};
|
|
545
633
|
}
|
|
546
634
|
const secret = state.request.secret === true;
|
|
547
635
|
const displayInput = secret ? maskPromptSecret(input) : input;
|
|
636
|
+
/**
|
|
637
|
+
* A question that is only text is the composer, not a card above it.
|
|
638
|
+
*
|
|
639
|
+
* This path used to draw a framed question and then a second framed editor
|
|
640
|
+
* under it: two borders that each looked like the place to type, two hint
|
|
641
|
+
* lines saying what Enter does, and a label reading `answer` that named
|
|
642
|
+
* nothing the reader had not already been told. A modal question is an
|
|
643
|
+
* interaction state the composer can carry itself — label in the top rule,
|
|
644
|
+
* question as the placeholder, keys in the bottom rule — which is also how pi
|
|
645
|
+
* mounts a dialog, in the editor's own slot.
|
|
646
|
+
*
|
|
647
|
+
* A prompt that also offers options keeps the card: a list plus a text field
|
|
648
|
+
* genuinely is two things. So does one that carries supporting copy, and one
|
|
649
|
+
* counting down a deadline — both need rows the bottom rule cannot give them.
|
|
650
|
+
*/
|
|
651
|
+
const detail = state.request.detail ?? '';
|
|
652
|
+
if (options.length === 0 && detail === '' && state.request.wait === undefined) {
|
|
653
|
+
const label = secret ? `${state.request.title} · hidden` : state.request.title;
|
|
654
|
+
const editor = renderEditor({
|
|
655
|
+
width,
|
|
656
|
+
input: displayInput,
|
|
657
|
+
inputCursor,
|
|
658
|
+
status: ' ' + theme.fg('accent', label) + ' ',
|
|
659
|
+
...(state.request.question === '' ? {} : { inlineHint: state.request.question }),
|
|
660
|
+
// The keys live on the bottom rule so the frame says how to answer without
|
|
661
|
+
// spending a row on it.
|
|
662
|
+
...(hints.length === 0 ? {} : { note: hints.join(' · ') }),
|
|
663
|
+
border: 'accent',
|
|
664
|
+
}, theme);
|
|
665
|
+
return { lines: editor.lines, cursor: editor.cursor, cursorVisible: true };
|
|
666
|
+
}
|
|
548
667
|
const editor = renderEditor({
|
|
549
668
|
width,
|
|
550
669
|
input: displayInput,
|
|
@@ -583,12 +702,14 @@ export function togglePromptSelection(state, query = '') {
|
|
|
583
702
|
}
|
|
584
703
|
/** Resolve the selected labels in stable option order. */
|
|
585
704
|
export function selectedPromptAnswer(state) {
|
|
586
|
-
const options = state.request.presentation === '
|
|
705
|
+
const options = state.request.presentation === 'tree'
|
|
587
706
|
? filteredPromptOptions(state.request, '', state.collapsed) : state.request.options ?? [];
|
|
588
707
|
if (options.length === 0)
|
|
589
708
|
return null;
|
|
590
709
|
if (state.request.multiSelect !== true) {
|
|
591
710
|
const option = options[state.selected];
|
|
711
|
+
if (option?.disabled === true)
|
|
712
|
+
return null;
|
|
592
713
|
return option?.value ?? option?.label ?? null;
|
|
593
714
|
}
|
|
594
715
|
const labels = options.flatMap((option, index) => state.checked.has(index) ? [option.value ?? option.label] : []);
|
|
@@ -597,6 +718,9 @@ export function selectedPromptAnswer(state) {
|
|
|
597
718
|
/** Resolve the selected answer after applying a full-screen selector query. */
|
|
598
719
|
export function selectedFilteredPromptAnswer(state, query) {
|
|
599
720
|
const option = filteredPromptOptions(state.request, query, state.collapsed)[state.selected];
|
|
721
|
+
// A disabled row is focusable but carries no answer, filtered or not.
|
|
722
|
+
if (option?.disabled === true)
|
|
723
|
+
return null;
|
|
600
724
|
return option?.value ?? option?.label ?? null;
|
|
601
725
|
}
|
|
602
726
|
//# sourceMappingURL=prompt-selector.js.map
|