@agi-fans/dsh-tui 0.20.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 +249 -3
- 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 +18 -0
- package/lib/chrome/main-screen-renderer.js +58 -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/renderer.d.ts +9 -0
- package/lib/chrome/renderer.js +1 -1
- package/lib/chrome/renderer.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 +616 -520
- package/lib/chrome/theme.js.map +1 -1
- package/lib/chrome/tool-args.d.ts +1 -1
- package/lib/chrome/tool-args.js +3 -1
- package/lib/chrome/tool-args.js.map +1 -1
- package/lib/chrome/tool-renderers.d.ts +2 -0
- package/lib/chrome/tool-renderers.js +69 -3
- 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 -7
- package/lib/chrome/welcome-tips.js +22 -41
- 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 +16 -22
- package/lib/commands/auth.js +346 -299
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/btw.d.ts +27 -0
- package/lib/commands/btw.js +59 -0
- package/lib/commands/btw.js.map +1 -0
- package/lib/commands/diff.js +30 -5
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +1 -1
- package/lib/commands/export.js +5 -3
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.d.ts +17 -1
- package/lib/commands/files.js +69 -61
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.js +18 -6
- package/lib/commands/integrations.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 +109 -76
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/permission.js +46 -13
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +196 -42
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule.d.ts +29 -0
- package/lib/commands/schedule.js +228 -0
- package/lib/commands/schedule.js.map +1 -0
- package/lib/commands/session-configuration.d.ts +1 -1
- package/lib/commands/session-configuration.js +29 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +75 -80
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.d.ts +24 -4
- package/lib/commands/terminal.js +38 -94
- 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 +150 -11
- 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-discovery.js +2 -2
- package/lib/input/editor-discovery.js.map +1 -1
- 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 +15 -4
- package/lib/input/keybindings-config.js +57 -35
- package/lib/input/keybindings-config.js.map +1 -1
- package/lib/input/keys.d.ts +18 -0
- package/lib/input/keys.js +48 -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/agent-profile.d.ts +5 -0
- package/lib/runtime/agent-profile.js +12 -0
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/approval-rules.d.ts +59 -0
- package/lib/runtime/approval-rules.js +179 -0
- package/lib/runtime/approval-rules.js.map +1 -0
- package/lib/runtime/btw-policy.d.ts +7 -0
- package/lib/runtime/btw-policy.js +18 -0
- package/lib/runtime/btw-policy.js.map +1 -0
- package/lib/runtime/btw.d.ts +47 -0
- package/lib/runtime/btw.js +200 -0
- package/lib/runtime/btw.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 +30 -0
- package/lib/runtime/composer-shell.js +150 -0
- package/lib/runtime/composer-shell.js.map +1 -0
- package/lib/runtime/file-recovery-local.d.ts +73 -0
- package/lib/runtime/file-recovery-local.js +351 -0
- package/lib/runtime/file-recovery-local.js.map +1 -0
- package/lib/runtime/file-recovery-recording.d.ts +23 -0
- package/lib/runtime/file-recovery-recording.js +131 -0
- package/lib/runtime/file-recovery-recording.js.map +1 -0
- package/lib/runtime/file-recovery-review.d.ts +3 -0
- package/lib/runtime/file-recovery-review.js +130 -0
- package/lib/runtime/file-recovery-review.js.map +1 -0
- package/lib/runtime/file-recovery-store.d.ts +83 -0
- package/lib/runtime/file-recovery-store.js +329 -0
- package/lib/runtime/file-recovery-store.js.map +1 -0
- package/lib/runtime/file-recovery-worker.d.ts +22 -0
- package/lib/runtime/file-recovery-worker.js +153 -0
- package/lib/runtime/file-recovery-worker.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -3
- package/lib/runtime/file-review.js +44 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/herdr-agent.d.ts +4 -13
- package/lib/runtime/herdr-agent.js +19 -63
- package/lib/runtime/herdr-agent.js.map +1 -1
- 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 +25 -2
- package/lib/runtime/plugin-settings.js +329 -31
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/provider-local.d.ts +81 -24
- package/lib/runtime/provider-local.js +1807 -608
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/runner.js +52 -12
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-archive.d.ts +6 -0
- package/lib/runtime/session-archive.js +129 -3
- package/lib/runtime/session-archive.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-preferences.d.ts +5 -0
- package/lib/runtime/settings-preferences.js +34 -0
- package/lib/runtime/settings-preferences.js.map +1 -0
- 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/runtime/terminal-state.d.ts +29 -0
- package/lib/runtime/terminal-state.js +62 -0
- package/lib/runtime/terminal-state.js.map +1 -0
- package/lib/runtime/tool-presentation.js +50 -14
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/session/external-agents.d.ts +5 -0
- package/lib/session/external-agents.js +27 -0
- package/lib/session/external-agents.js.map +1 -0
- package/lib/session/feature-toggles.d.ts +1 -1
- package/lib/session/feature-toggles.js +1 -1
- package/lib/session/interaction-adapter.d.ts +1 -6
- package/lib/session/interaction-adapter.js +95 -18
- 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/model-readiness.d.ts +16 -0
- package/lib/session/model-readiness.js +43 -0
- package/lib/session/model-readiness.js.map +1 -0
- 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/project-context.d.ts +5 -0
- package/lib/session/project-context.js +36 -0
- package/lib/session/project-context.js.map +1 -1
- package/lib/session/session-controller.d.ts +10 -13
- package/lib/session/session-controller.js +183 -98
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/session-library.js +2 -0
- package/lib/session/session-library.js.map +1 -1
- package/lib/session/session-tree.d.ts +3 -0
- package/lib/session/session-tree.js +3 -1
- package/lib/session/session-tree.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +2 -1
- package/lib/session/subagent-roster.js +24 -3
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +6 -2
- package/lib/session/subagent-tracker.js +49 -2
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +47 -13
- package/lib/session/tui-settings.js +76 -34
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/working-directory.d.ts +4 -0
- package/lib/session/working-directory.js +4 -0
- package/lib/session/working-directory.js.map +1 -0
- package/lib/session/worktrees.d.ts +13 -0
- package/lib/session/worktrees.js +49 -0
- package/lib/session/worktrees.js.map +1 -0
- package/lib/views/approval-rule.d.ts +3 -0
- package/lib/views/approval-rule.js +4 -0
- package/lib/views/approval-rule.js.map +1 -0
- package/lib/views/at-complete.d.ts +8 -0
- package/lib/views/at-complete.js +64 -2
- package/lib/views/at-complete.js.map +1 -1
- package/lib/views/autocomplete.d.ts +22 -2
- package/lib/views/autocomplete.js +70 -40
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/copy-targets.d.ts +13 -14
- package/lib/views/copy-targets.js +42 -54
- 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.d.ts +1 -1
- package/lib/views/event-views.js +60 -6
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.d.ts +12 -0
- package/lib/views/file-actions.js +72 -0
- package/lib/views/file-actions.js.map +1 -0
- package/lib/views/history-search.js +15 -21
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkeys.d.ts +8 -1
- package/lib/views/hotkeys.js +53 -26
- 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 +34 -4
- package/lib/views/prompt-selector.js +270 -134
- 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 +51 -10
- package/lib/views/settings-list.js +267 -315
- 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-export.js +9 -0
- package/lib/views/transcript-export.js.map +1 -1
- package/lib/views/transcript-render.d.ts +97 -25
- package/lib/views/transcript-render.js +542 -170
- 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 +95 -65
- 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/steer.d.ts +0 -5
- package/lib/commands/steer.js +0 -24
- package/lib/commands/steer.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 -206
- package/lib/views/agent-hub.js.map +0 -1
- package/lib/views/copy-selector.d.ts +0 -51
- package/lib/views/copy-selector.js +0 -139
- 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,100 +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
|
-
return renderPromptSelector(state, theme, width, input, inputCursor, Math.max(1, pageHeight - 8));
|
|
243
|
-
}
|
|
244
|
-
const options = filteredPromptOptions(state.request, input);
|
|
245
|
-
const selected = Math.max(0, Math.min(state.selected, Math.max(0, options.length - 1)));
|
|
246
|
-
const compact = state.request.optionLayout === 'compact';
|
|
247
|
-
const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`).join(' · ');
|
|
248
|
-
const hints = wrapText(formatOverlayHint([HOTKEY_TEXT, HOTKEY_NAVIGATE, HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
249
|
-
+ (actionHints === '' ? '' : ' · ' + actionHints), width - 4).slice(0, Math.max(1, Math.floor((height - 6) / 2)));
|
|
250
|
-
const fixedRows = (compact ? 9 : 11) + hints.length - 1;
|
|
251
|
-
const visibleCount = compact
|
|
252
|
-
? Math.max(1, pageHeight - fixedRows)
|
|
253
|
-
: Math.max(1, Math.floor((pageHeight - fixedRows) / 4));
|
|
254
|
-
const { start, end } = promptSelectorVisibleRange(options.length, selected, visibleCount);
|
|
255
|
-
const detail = state.request.detail === undefined || state.request.detail === ''
|
|
256
|
-
? ''
|
|
257
|
-
: ' ' + theme.fg('muted', `(${state.request.detail})`);
|
|
258
|
-
const heading = theme.bold(state.request.title) + detail;
|
|
259
|
-
const lines = compact
|
|
260
|
-
? [
|
|
261
|
-
pageTop(theme, width, appName),
|
|
262
|
-
pageRow(theme, ' ' + heading, width),
|
|
263
|
-
pageDivider(theme, width),
|
|
264
|
-
]
|
|
265
|
-
: [
|
|
266
|
-
pageTop(theme, width, appName),
|
|
267
|
-
pageRow(theme, '', width),
|
|
268
|
-
pageRow(theme, ' ' + heading, width),
|
|
269
|
-
pageRow(theme, '', width),
|
|
270
|
-
pageDivider(theme, width),
|
|
271
|
-
];
|
|
272
|
-
const searchPrefix = theme.fg('dim', '> ');
|
|
273
|
-
const searchText = truncateToWidth(input, Math.max(1, width - 8));
|
|
274
|
-
const searchRow = lines.length;
|
|
275
|
-
lines.push(pageRow(theme, searchPrefix + searchText, width), pageRow(theme, '', width));
|
|
276
|
-
if (options.length === 0) {
|
|
277
|
-
// The generic line is a fallback only: this selector also serves approvals
|
|
278
|
-
// and questions, whose empty states have nothing to do with sessions.
|
|
279
|
-
const fallback = state.request.emptyText ?? 'No matching options.';
|
|
280
|
-
const empty = input === '' ? fallback : `No match for “${input}”.`;
|
|
281
|
-
lines.push(pageRow(theme, ' ' + theme.fg('muted', empty), width));
|
|
282
|
-
}
|
|
283
|
-
else {
|
|
284
|
-
for (let index = start; index < end; index += 1) {
|
|
285
|
-
const option = options[index];
|
|
286
|
-
if (option === undefined)
|
|
287
|
-
continue;
|
|
288
|
-
if (compact) {
|
|
289
|
-
lines.push(pageRow(theme, optionRow(option, index, { ...state, selected }, theme, Math.max(1, width - 6)), width));
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
const active = index === selected;
|
|
293
|
-
const marker = active ? theme.fg('accent', SYMBOL.cursor + ' ') : ' ';
|
|
294
|
-
const label = active ? theme.bold(option.label) : option.label;
|
|
295
|
-
lines.push(pageRow(theme, marker + label, width));
|
|
296
|
-
lines.push(pageRow(theme, ' ' + theme.fg('dim', option.preview ?? ''), width));
|
|
297
|
-
const badge = optionBadge(option, theme);
|
|
298
|
-
const description = option.description === undefined ? '' : theme.fg('dim', option.description);
|
|
299
|
-
const separator = description !== '' && badge !== '' ? theme.fg('dim', ' · ') : '';
|
|
300
|
-
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()) });
|
|
301
286
|
}
|
|
302
287
|
}
|
|
303
|
-
|
|
304
|
-
const targetBeforeFooter = Math.max(0, pageHeight - footerRows);
|
|
305
|
-
if (lines.length > targetBeforeFooter)
|
|
306
|
-
lines.length = targetBeforeFooter;
|
|
307
|
-
while (lines.length < targetBeforeFooter)
|
|
308
|
-
lines.push(pageRow(theme, '', width));
|
|
309
|
-
const position = options.length === 0 ? '' : `${selected + 1}/${options.length}`;
|
|
310
|
-
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));
|
|
311
|
-
const cursorColumn = Math.min(Math.max(1, width - 3), 4 + visibleWidth(input.slice(0, inputCursor)));
|
|
312
|
-
return {
|
|
313
|
-
lines,
|
|
314
|
-
cursor: { row: searchRow, column: cursorColumn },
|
|
315
|
-
};
|
|
288
|
+
return matches;
|
|
316
289
|
}
|
|
317
|
-
|
|
318
|
-
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) {
|
|
319
314
|
let cache = documentCache.get(request);
|
|
320
315
|
if (cache === undefined) {
|
|
321
316
|
cache = new Map();
|
|
@@ -331,6 +326,21 @@ function documentRows(request, theme, width) {
|
|
|
331
326
|
cache.set(key, lines);
|
|
332
327
|
return lines;
|
|
333
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
|
+
}
|
|
334
344
|
/** Render a bounded, scrollable Markdown document with fixed actions. */
|
|
335
345
|
function documentActionRows(state, theme, width, pageHeight) {
|
|
336
346
|
const actionRows = [];
|
|
@@ -366,7 +376,7 @@ export function renderImagePreviewPage(state, theme, width, height, appName, pro
|
|
|
366
376
|
pageRow(theme, theme.fg('muted', image.description), width), pageDivider(theme, width)];
|
|
367
377
|
const top = lines.length;
|
|
368
378
|
const canOpen = state.request.options?.some(option => option.value === 'open') === true;
|
|
369
|
-
const fallback = protocol === undefined ?
|
|
379
|
+
const fallback = protocol === undefined ? wrapTextStable('Inline images are unavailable in this terminal. '
|
|
370
380
|
+ (canOpen ? 'Use Open to view the original.' : 'This attachment has no local original to open.'), width - 8) : [];
|
|
371
381
|
for (let at = 0; at < bodyRows; at++)
|
|
372
382
|
lines.push(pageRow(theme, ' ' + (fallback[at] ?? ''), width));
|
|
@@ -384,10 +394,11 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
384
394
|
}
|
|
385
395
|
const feedback = state.feedback === true;
|
|
386
396
|
const actionRows = documentActionRows(state, theme, width, pageHeight);
|
|
387
|
-
const readerInput =
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
397
|
+
const readerInput = state.documentInput !== undefined;
|
|
398
|
+
const customHints = (state.request.actions ?? []).map(action => `${action.key.toUpperCase()} ${action.label}`);
|
|
399
|
+
let readerHints = source === undefined ? [] : wrapTextStable(readerInput ? 'Enter apply · Esc cancel'
|
|
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' : '')
|
|
401
|
+
+ ' · ' + ['Tab/←→ actions', 'Enter activate', ...customHints, 'Esc back'].join(' · '), width - 4);
|
|
391
402
|
const maxHints = Math.max(1, Math.min(4, pageHeight - 8 - actionRows.length));
|
|
392
403
|
if (readerHints.length > maxHints)
|
|
393
404
|
readerHints = [...readerHints.slice(0, maxHints - 1), 'Tab actions · Enter · Esc back'];
|
|
@@ -408,6 +419,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
408
419
|
const visible = document.slice(start, start + bodyRows);
|
|
409
420
|
const query = state.documentQuery ?? '';
|
|
410
421
|
const matches = source === undefined ? [] : documentMatches(source, query);
|
|
422
|
+
const textMatches = source === undefined ? findInRows(document, query) : [];
|
|
411
423
|
const matching = new Set(matches);
|
|
412
424
|
if (layout !== undefined)
|
|
413
425
|
for (let at = 0; at < visible.length; at++) {
|
|
@@ -416,6 +428,15 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
416
428
|
visible[at] = theme.inverse(layout.gutterWidth === 0 ? row : '›' + row.slice(1));
|
|
417
429
|
}
|
|
418
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
|
+
}
|
|
419
440
|
while (visible.length < bodyRows)
|
|
420
441
|
visible.push('');
|
|
421
442
|
const subject = state.request.presentation === 'document' ? 'document' : 'plan';
|
|
@@ -444,7 +465,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
444
465
|
const inputRow = lines.length;
|
|
445
466
|
lines.push(pageRow(theme, ' ' + prefix + value, width));
|
|
446
467
|
// "empty Enter keeps planning" is prose, but it names the key through the catalog.
|
|
447
|
-
lines.push(pageRow(theme, theme.fg('dim', `[${formatOverlayHint([HOTKEY_SUBMIT, HOTKEY_BACK, HOTKEY_CANCEL_CTRL])} · empty ${formatHotkeyKeys(HOTKEY_SUBMIT)} keeps planning]`), width));
|
|
468
|
+
lines.push(pageRow(theme, theme.fg('dim', `[${formatOverlayHint([HOTKEY_SUBMIT, HOTKEY_BACK, HOTKEY_CANCEL_CTRL])} · Ctrl+X editor · empty ${formatHotkeyKeys(HOTKEY_SUBMIT)} keeps planning]`), width));
|
|
448
469
|
lines.push(pageBottom(theme, width));
|
|
449
470
|
cursor = {
|
|
450
471
|
row: inputRow,
|
|
@@ -452,6 +473,19 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
452
473
|
};
|
|
453
474
|
}
|
|
454
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
|
+
}
|
|
455
489
|
if (source !== undefined) {
|
|
456
490
|
const row = layout?.sourceRows[start] ?? 0;
|
|
457
491
|
const model = documentModel(source);
|
|
@@ -471,8 +505,13 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
471
505
|
lines.push(...actionRows.map(row => pageRow(theme, ' ' + row, width)));
|
|
472
506
|
const shortcuts = state.request.presentation === 'document' && state.request.actions?.length
|
|
473
507
|
? 'Tab/←→ select · Enter activate · ' + state.request.actions.map(action => `${action.key.toUpperCase()} ${action.label}`).join(' · ') + ' · Esc back'
|
|
474
|
-
: formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
475
|
-
|
|
508
|
+
: formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
509
|
+
+ (state.request.presentation === 'plan-review' ? ' · Ctrl+X editor' : '');
|
|
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)));
|
|
476
515
|
lines.push(pageBottom(theme, width));
|
|
477
516
|
}
|
|
478
517
|
return {
|
|
@@ -484,9 +523,16 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
484
523
|
cursorVisible: feedback || readerInput,
|
|
485
524
|
};
|
|
486
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
|
+
}
|
|
487
532
|
/** Render the prompt card and its answer editor as one bottom-of-screen overlay. */
|
|
488
533
|
export function renderPromptSelector(state, theme, width, input, inputCursor, maxVisible = PROMPT_SELECTOR_MAX_VISIBLE) {
|
|
489
534
|
const options = filteredPromptOptions(state.request, input, state.collapsed);
|
|
535
|
+
const filtering = input !== '';
|
|
490
536
|
const contentWidth = Math.max(1, width - 4);
|
|
491
537
|
const body = [state.request.question];
|
|
492
538
|
if (state.request.wait !== undefined) {
|
|
@@ -499,24 +545,71 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
499
545
|
body.push('', state.request.detail);
|
|
500
546
|
if (options.length > 0) {
|
|
501
547
|
const { start, end } = promptSelectorVisibleRange(options.length, state.selected, maxVisible);
|
|
502
|
-
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))));
|
|
503
549
|
if (options.length > maxVisible) {
|
|
504
550
|
body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · scroll for more`));
|
|
505
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.')}`));
|
|
506
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);
|
|
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;
|
|
507
594
|
const submit = state.request.submitLabel?.trim()
|
|
508
595
|
|| (options.length === 0 ? 'answer' : state.request.multiSelect === true ? 'confirm' : 'select');
|
|
509
596
|
// The request supplies the submit verb; the key itself comes from the catalog.
|
|
510
597
|
const select = `${formatHotkeyKeys(HOTKEY_SELECT)} ${submit}`;
|
|
511
|
-
const navigation =
|
|
512
|
-
?
|
|
513
|
-
:
|
|
514
|
-
?
|
|
515
|
-
:
|
|
598
|
+
const navigation = state.request.presentation === 'progress'
|
|
599
|
+
? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog')
|
|
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])}`;
|
|
516
607
|
const dismiss = state.request.dismissLabel;
|
|
608
|
+
const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`);
|
|
517
609
|
const hints = [
|
|
518
610
|
dismiss === undefined ? navigation : navigation.replace(formatOverlayHint([HOTKEY_CANCEL]), `${formatHotkeyKeys(HOTKEY_CANCEL)} ${dismiss.toLowerCase()}`),
|
|
519
611
|
...(state.request.skippable === true ? [formatOverlayHint([HOTKEY_SKIP])] : []),
|
|
612
|
+
...actionHints,
|
|
520
613
|
];
|
|
521
614
|
body.push('', theme.fg('dim', hints.join(' · ')));
|
|
522
615
|
const card = renderFramedBlock({
|
|
@@ -526,17 +619,51 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
526
619
|
width,
|
|
527
620
|
applyBg: false,
|
|
528
621
|
}, theme);
|
|
529
|
-
if (state.request.allowCustom === false) {
|
|
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.
|
|
530
626
|
return {
|
|
531
627
|
lines: card,
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
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,
|
|
536
632
|
};
|
|
537
633
|
}
|
|
538
634
|
const secret = state.request.secret === true;
|
|
539
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
|
+
}
|
|
540
667
|
const editor = renderEditor({
|
|
541
668
|
width,
|
|
542
669
|
input: displayInput,
|
|
@@ -559,32 +686,41 @@ export function movePromptSelection(state, next, count = state.request.options?.
|
|
|
559
686
|
return selected === state.selected ? state : { ...state, selected, previewScroll: 0 };
|
|
560
687
|
}
|
|
561
688
|
/** Toggle the active row for a multi-select prompt. */
|
|
562
|
-
export function togglePromptSelection(state) {
|
|
563
|
-
|
|
689
|
+
export function togglePromptSelection(state, query = '') {
|
|
690
|
+
const option = filteredPromptOptions(state.request, query)[state.selected];
|
|
691
|
+
if (state.request.multiSelect !== true || option === undefined)
|
|
692
|
+
return state;
|
|
693
|
+
const index = state.request.options?.indexOf(option) ?? -1;
|
|
694
|
+
if (index < 0)
|
|
564
695
|
return state;
|
|
565
696
|
const checked = new Set(state.checked);
|
|
566
|
-
if (checked.has(
|
|
567
|
-
checked.delete(
|
|
697
|
+
if (checked.has(index))
|
|
698
|
+
checked.delete(index);
|
|
568
699
|
else
|
|
569
|
-
checked.add(
|
|
700
|
+
checked.add(index);
|
|
570
701
|
return { ...state, checked };
|
|
571
702
|
}
|
|
572
703
|
/** Resolve the selected labels in stable option order. */
|
|
573
704
|
export function selectedPromptAnswer(state) {
|
|
574
|
-
const options = state.request.presentation === '
|
|
705
|
+
const options = state.request.presentation === 'tree'
|
|
575
706
|
? filteredPromptOptions(state.request, '', state.collapsed) : state.request.options ?? [];
|
|
576
707
|
if (options.length === 0)
|
|
577
708
|
return null;
|
|
578
709
|
if (state.request.multiSelect !== true) {
|
|
579
710
|
const option = options[state.selected];
|
|
711
|
+
if (option?.disabled === true)
|
|
712
|
+
return null;
|
|
580
713
|
return option?.value ?? option?.label ?? null;
|
|
581
714
|
}
|
|
582
715
|
const labels = options.flatMap((option, index) => state.checked.has(index) ? [option.value ?? option.label] : []);
|
|
583
|
-
return labels.length === 0 ? null : labels.join(', ');
|
|
716
|
+
return labels.length === 0 ? state.request.allowEmptySelection === true ? '' : null : labels.join(', ');
|
|
584
717
|
}
|
|
585
718
|
/** Resolve the selected answer after applying a full-screen selector query. */
|
|
586
719
|
export function selectedFilteredPromptAnswer(state, query) {
|
|
587
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;
|
|
588
724
|
return option?.value ?? option?.label ?? null;
|
|
589
725
|
}
|
|
590
726
|
//# sourceMappingURL=prompt-selector.js.map
|