@agi-fans/dsh-tui 0.22.0 → 0.23.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/LICENSE +26 -0
- package/lib/CHANGELOG.md +199 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +97 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +2 -8
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +197 -120
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +111 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +112 -15
- 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 +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1264 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.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-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +32 -13
- package/lib/session/session-controller.js +444 -98
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +10 -7
- package/lib/views/autocomplete.js +44 -37
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +59 -5
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Interactive terminal selector used by resume, approval, and user questions. */
|
|
2
|
+
import { type UiLanguage } from '../locale/index.ts';
|
|
2
3
|
import { type ImageProtocol } from '../chrome/terminal-images.ts';
|
|
3
4
|
import type { TuiDocumentPosition, TuiPrompt } from '../definition.ts';
|
|
4
5
|
import { type Theme } from '../chrome/theme.ts';
|
|
@@ -6,6 +7,8 @@ import { type HotkeyRow } from './hotkey-format.ts';
|
|
|
6
7
|
/** Presentation state owned by the terminal while a human prompt is active. */
|
|
7
8
|
export interface PromptSelectorState {
|
|
8
9
|
request: TuiPrompt;
|
|
10
|
+
/** Terminal-owned document formatting, shared by rendering and search. */
|
|
11
|
+
renderDocument?: (theme: Theme, width: number) => string[];
|
|
9
12
|
selected: number;
|
|
10
13
|
checked: ReadonlySet<number>;
|
|
11
14
|
/** Requested first document row for full-screen review surfaces. */
|
|
@@ -29,6 +32,20 @@ export interface PromptSelectorState {
|
|
|
29
32
|
armedUntil?: number;
|
|
30
33
|
}
|
|
31
34
|
export interface PromptSelectorFrame {
|
|
35
|
+
actions?: readonly {
|
|
36
|
+
row: number;
|
|
37
|
+
column: number;
|
|
38
|
+
width: number;
|
|
39
|
+
index: number;
|
|
40
|
+
key?: string;
|
|
41
|
+
}[];
|
|
42
|
+
choices?: readonly {
|
|
43
|
+
row: number;
|
|
44
|
+
column: number;
|
|
45
|
+
width: number;
|
|
46
|
+
value: string;
|
|
47
|
+
index: number;
|
|
48
|
+
}[];
|
|
32
49
|
image?: import('../chrome/terminal-images.ts').ImagePlacement;
|
|
33
50
|
lines: string[];
|
|
34
51
|
cursor: {
|
|
@@ -55,7 +72,7 @@ export declare function filteredPromptOptions(request: TuiPrompt, query: string,
|
|
|
55
72
|
/** Keep the same live choice selected as entries arrive, move, or disappear. */
|
|
56
73
|
export declare function refreshPromptOptions<T extends PromptSelectorState>(state: T, options: NonNullable<TuiPrompt['options']>, query: string): T;
|
|
57
74
|
/** Page movement follows the list space remaining after wrapped action hints. */
|
|
58
|
-
export declare function promptTreePageSize(request: TuiPrompt, width: number, height: number): number;
|
|
75
|
+
export declare function promptTreePageSize(request: TuiPrompt, width: number, height: number, language?: UiLanguage): number;
|
|
59
76
|
/** A hierarchical choice with an independently scrollable content preview. */
|
|
60
77
|
export declare function renderPromptTreePage(state: PromptSelectorState, theme: Theme, width: number, height: number, input: string, inputCursor: number, appName: string): PromptSelectorFrame;
|
|
61
78
|
/** Fold/unfold a subtree or move to its parent, preserving selection by value. */
|
|
@@ -109,6 +126,11 @@ export declare function renderPromptSelector(state: PromptSelectorState, theme:
|
|
|
109
126
|
export declare function movePromptSelection(state: PromptSelectorState, next: number, count?: number): PromptSelectorState;
|
|
110
127
|
/** Toggle the active row for a multi-select prompt. */
|
|
111
128
|
export declare function togglePromptSelection(state: PromptSelectorState, query?: string): PromptSelectorState;
|
|
129
|
+
/** Keep option values and free text separate, including punctuation in labels. */
|
|
130
|
+
export declare function structuredPromptAnswer(state: PromptSelectorState, custom: string): {
|
|
131
|
+
selected: string[];
|
|
132
|
+
custom?: string;
|
|
133
|
+
};
|
|
112
134
|
/** Resolve the selected labels in stable option order. */
|
|
113
135
|
export declare function selectedPromptAnswer(state: PromptSelectorState): string | null;
|
|
114
136
|
/** Resolve the selected answer after applying a full-screen selector query. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Interactive terminal selector used by resume, approval, and user questions. */
|
|
2
|
+
import { uiText } from "../locale/index.js";
|
|
2
3
|
import { imageSize } from "../chrome/terminal-images.js";
|
|
3
4
|
import { documentLayout, documentMatches, documentModel, documentPosition, documentStart } from "./document-reader.js";
|
|
4
5
|
import { promptTreeRows } from "./prompt-tree.js";
|
|
@@ -6,7 +7,7 @@ import { rankSearchResults } from "../input/fuzzy-search.js";
|
|
|
6
7
|
import { renderEditor, renderFramedBlock } from "../chrome/box.js";
|
|
7
8
|
import { renderMarkdown } from "../chrome/markdown.js";
|
|
8
9
|
import { BOX, SYMBOL } from "../chrome/theme.js";
|
|
9
|
-
import { padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
|
|
10
|
+
import { expandTabs, padToWidth, sliceCells, stripAnsi, truncateToWidth, visibleWidth, wrapTextStable } from "../chrome/width.js";
|
|
10
11
|
import { formatHotkeyKeys, formatOverlayHint } from "./hotkey-format.js";
|
|
11
12
|
import { renderListRow } from "./list-view.js";
|
|
12
13
|
/** Maximum option rows retained in the prompt overlay before it windows. */
|
|
@@ -21,6 +22,7 @@ const HOTKEY_CHOOSE = { keys: '←→', action: 'Choose — move between the pla
|
|
|
21
22
|
const HOTKEY_PAGE = { keys: 'PgUp / PgDn', action: 'Scroll — page the options or the plan' };
|
|
22
23
|
const HOTKEY_EDGE = { keys: 'Home / End', action: 'Jump to the first or last option, or the plan edges' };
|
|
23
24
|
const HOTKEY_TOGGLE = { keys: 'Space', action: 'Toggle — check or clear a multi-select option' };
|
|
25
|
+
const HOTKEY_ANSWER_TOGGLE = { keys: 'Ctrl+G', action: 'Toggle — check or clear a multi-select option' };
|
|
24
26
|
const HOTKEY_SELECT = { keys: 'Enter', action: 'Select the highlighted option' };
|
|
25
27
|
const HOTKEY_SUBMIT = { keys: 'Enter', action: 'Submit — send revision feedback, or keep planning when empty' };
|
|
26
28
|
const HOTKEY_SUBMIT_NEWLINE = { keys: 'Ctrl+J', action: 'Submit — send revision feedback like Enter' };
|
|
@@ -38,6 +40,7 @@ export const PROMPT_SELECTOR_HOTKEYS = [
|
|
|
38
40
|
HOTKEY_PAGE,
|
|
39
41
|
HOTKEY_EDGE,
|
|
40
42
|
HOTKEY_TOGGLE,
|
|
43
|
+
HOTKEY_ANSWER_TOGGLE,
|
|
41
44
|
HOTKEY_SELECT,
|
|
42
45
|
HOTKEY_SUBMIT,
|
|
43
46
|
HOTKEY_SUBMIT_NEWLINE,
|
|
@@ -58,6 +61,8 @@ export function filteredPromptOptions(request, query, collapsed) {
|
|
|
58
61
|
const options = request.options ?? [];
|
|
59
62
|
if (request.filterable !== true)
|
|
60
63
|
return options;
|
|
64
|
+
if (request.filterOptions !== undefined)
|
|
65
|
+
return request.filterOptions(query);
|
|
61
66
|
return rankSearchResults(options, query, option => [option.label, option.value, option.preview, option.description, option.badge?.label]
|
|
62
67
|
.filter((value) => value !== undefined));
|
|
63
68
|
}
|
|
@@ -75,7 +80,7 @@ export function refreshPromptOptions(state, options, query) {
|
|
|
75
80
|
const previewCache = new WeakMap();
|
|
76
81
|
function treePreview(option, theme, width) {
|
|
77
82
|
if (option === undefined)
|
|
78
|
-
return ['No matching conversation turns.'];
|
|
83
|
+
return [theme.text('No matching conversation turns.')];
|
|
79
84
|
let cache = previewCache.get(option);
|
|
80
85
|
if (cache === undefined) {
|
|
81
86
|
cache = new Map();
|
|
@@ -97,9 +102,9 @@ function treePreview(option, theme, width) {
|
|
|
97
102
|
cache.set(key, lines);
|
|
98
103
|
return lines;
|
|
99
104
|
}
|
|
100
|
-
function treePageLayout(request, width, height) {
|
|
105
|
+
function treePageLayout(request, width, height, language = 'en') {
|
|
101
106
|
const actions = (request.actions ?? []).map(item => `${item.key} ${item.label}`).join(' · ');
|
|
102
|
-
const hint =
|
|
107
|
+
const hint = [uiText(language, '↑↓ · ←→ fold'), ...(width >= 90 ? [uiText(language, 'type search')] : []), ...(actions === '' ? [] : [actions]), ...(width >= 90 ? [uiText(language, 'Ctrl+↑↓ preview')] : [])].join(' · ');
|
|
103
108
|
const hints = wrapTextStable(hint, width - 4).slice(0, Math.max(1, Math.floor((height - 6) / 2)));
|
|
104
109
|
const middle = height - 6 - hints.length;
|
|
105
110
|
const listHeight = width >= 90 ? middle : Math.max(1, Math.floor((middle - 1) / 2));
|
|
@@ -107,8 +112,8 @@ function treePageLayout(request, width, height) {
|
|
|
107
112
|
return { hints, middle, listHeight, previewHeight };
|
|
108
113
|
}
|
|
109
114
|
/** Page movement follows the list space remaining after wrapped action hints. */
|
|
110
|
-
export function promptTreePageSize(request, width, height) {
|
|
111
|
-
return height < 10 || width < 20 ? Math.max(1, height - 8) : treePageLayout(request, width, height).listHeight;
|
|
115
|
+
export function promptTreePageSize(request, width, height, language = 'en') {
|
|
116
|
+
return height < 10 || width < 20 ? Math.max(1, height - 8) : treePageLayout(request, width, height, language).listHeight;
|
|
112
117
|
}
|
|
113
118
|
/** A hierarchical choice with an independently scrollable content preview. */
|
|
114
119
|
export function renderPromptTreePage(state, theme, width, height, input, inputCursor, appName) {
|
|
@@ -119,7 +124,7 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
|
|
|
119
124
|
const option = rows[selected]?.option;
|
|
120
125
|
const split = width >= 90;
|
|
121
126
|
const inner = width - 4;
|
|
122
|
-
const { hints, middle, listHeight, previewHeight } = treePageLayout(state.request, width, height);
|
|
127
|
+
const { hints, middle, listHeight, previewHeight } = treePageLayout(state.request, width, height, theme.language);
|
|
123
128
|
const listWidth = split ? Math.floor((inner - 3) * 0.45) : inner;
|
|
124
129
|
const previewWidth = split ? inner - listWidth - 3 : inner;
|
|
125
130
|
const range = promptSelectorVisibleRange(rows.length, selected, listHeight);
|
|
@@ -155,7 +160,7 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
|
|
|
155
160
|
list.push(fit(head + text + description, listWidth));
|
|
156
161
|
}
|
|
157
162
|
if (rows.length === 0)
|
|
158
|
-
list.push(fit(theme.fg('dim', state.request.emptyText ?? 'No matching conversation turns.'), listWidth));
|
|
163
|
+
list.push(fit(theme.fg('dim', state.request.emptyText ?? theme.text('No matching conversation turns.')), listWidth));
|
|
159
164
|
const preview = treePreview(option, theme, Math.max(1, previewWidth));
|
|
160
165
|
const maxScroll = Math.max(0, preview.length - previewHeight);
|
|
161
166
|
const scroll = Math.max(0, Math.min(state.previewScroll ?? 0, maxScroll));
|
|
@@ -173,8 +178,9 @@ export function renderPromptTreePage(state, theme, width, height, input, inputCu
|
|
|
173
178
|
lines.push(pageRow(theme, preview[scroll + at] ?? '', width));
|
|
174
179
|
}
|
|
175
180
|
const action = option?.submitLabel ?? state.request.submitLabel ?? 'select';
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
const navigation = option === undefined || option.disabled === true ? theme.text('Esc back') : theme.text(`Enter ${action} · Esc back`);
|
|
182
|
+
lines.push(pageRow(theme, theme.fg('dim', navigation), width), ...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)), pageBottom(theme, width));
|
|
183
|
+
return { lines, choices: rows.slice(range.start, range.end).map((row, index) => ({ row: 4 + index, column: 2, width: listWidth, index: range.start + index, value: row.option.value ?? row.option.label })), cursor: { row: 2, column: Math.min(width - 3, 4 + visibleWidth(input.slice(0, inputCursor))) },
|
|
178
184
|
document: { start: scroll, maxStart: maxScroll, pageSize: previewHeight }, cursorVisible: true };
|
|
179
185
|
}
|
|
180
186
|
/** Fold/unfold a subtree or move to its parent, preserving selection by value. */
|
|
@@ -344,6 +350,7 @@ export function promptNeedsPage(request) {
|
|
|
344
350
|
/** Render a bounded, scrollable Markdown document with fixed actions. */
|
|
345
351
|
function documentActionRows(state, theme, width, pageHeight) {
|
|
346
352
|
const actionRows = [];
|
|
353
|
+
const actions = [];
|
|
347
354
|
let actionRow = '';
|
|
348
355
|
const actionWidth = Math.max(1, width - 5);
|
|
349
356
|
for (const [index, option] of (state.request.options ?? []).entries()) {
|
|
@@ -352,6 +359,7 @@ function documentActionRows(state, theme, width, pageHeight) {
|
|
|
352
359
|
actionRows.push(actionRow);
|
|
353
360
|
actionRow = '';
|
|
354
361
|
}
|
|
362
|
+
actions.push({ row: actionRows.length, column: 3 + visibleWidth(actionRow) + (actionRow === '' ? 0 : 3), width: visibleWidth(label), index });
|
|
355
363
|
const painted = index === state.selected ? theme.bold(theme.fg('accent', label)) : theme.fg('muted', label);
|
|
356
364
|
actionRow += (actionRow === '' ? '' : theme.fg('dim', ' ')) + painted;
|
|
357
365
|
}
|
|
@@ -360,8 +368,9 @@ function documentActionRows(state, theme, width, pageHeight) {
|
|
|
360
368
|
const selected = state.request.options?.[state.selected];
|
|
361
369
|
actionRows.splice(0, actionRows.length, selected === undefined ? ''
|
|
362
370
|
: theme.bold(theme.fg('accent', truncateToWidth(`› [ ${selected.label} ]`, actionWidth))));
|
|
371
|
+
actions.splice(0, actions.length, ...(selected === undefined ? [] : [{ row: 0, column: 3, width: visibleWidth(actionRows[0]), index: state.selected }]));
|
|
363
372
|
}
|
|
364
|
-
return actionRows;
|
|
373
|
+
return { lines: actionRows, actions };
|
|
365
374
|
}
|
|
366
375
|
/** Reserve a bordered, padded rectangle; graphics never become display-row content. */
|
|
367
376
|
export function renderImagePreviewPage(state, theme, width, height, appName, protocol, cell) {
|
|
@@ -369,7 +378,7 @@ export function renderImagePreviewPage(state, theme, width, height, appName, pro
|
|
|
369
378
|
if (width < 16 || height < 10)
|
|
370
379
|
return renderPromptSelector({ ...state, request: { ...state.request,
|
|
371
380
|
detail: image.description + '\nEnlarge the terminal or use Open to view the image.' } }, theme, width, '', 0, Math.max(1, height - 8));
|
|
372
|
-
const actions = documentActionRows(state, theme, width, height);
|
|
381
|
+
const { lines: actions } = documentActionRows(state, theme, width, height);
|
|
373
382
|
const hints = 'Tab/←→ actions · Enter activate · Esc back';
|
|
374
383
|
const bodyRows = Math.max(1, height - 8 - actions.length);
|
|
375
384
|
const lines = [pageTop(theme, width, appName + ' · ' + state.request.title), pageRow(theme, state.request.question, width),
|
|
@@ -393,11 +402,19 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
393
402
|
return renderPromptSelector(source === undefined ? state : { ...state, request: { ...state.request, detail: source.text } }, theme, width, input, inputCursor, Math.max(1, pageHeight - 8));
|
|
394
403
|
}
|
|
395
404
|
const feedback = state.feedback === true;
|
|
396
|
-
const
|
|
405
|
+
const actionLayout = documentActionRows(state, theme, width, pageHeight);
|
|
406
|
+
const actionRows = actionLayout.lines;
|
|
397
407
|
const readerInput = state.documentInput !== undefined;
|
|
398
408
|
const customHints = (state.request.actions ?? []).map(action => `${action.key.toUpperCase()} ${action.label}`);
|
|
409
|
+
const renderedReader = source === undefined && state.request.presentation === 'document';
|
|
410
|
+
let renderedHints = renderedReader ? wrapTextStable(readerInput ? 'Enter find · Esc cancel'
|
|
411
|
+
: ['↑↓/PgUp/PgDn/wheel scroll', '/ find', 'Ctrl+N/P match', ...customHints,
|
|
412
|
+
...(state.request.options && state.request.options.length > 1 ? ['Tab actions · Enter'] : []), 'Esc back'].join(' · '), Math.max(1, width - 4)) : [];
|
|
413
|
+
const maxRenderedHints = Math.max(1, pageHeight - (state.request.question === '' ? 5 : 6) - actionRows.length - (readerInput || state.documentQuery ? 1 : 0));
|
|
414
|
+
if (renderedHints.length > maxRenderedHints)
|
|
415
|
+
renderedHints = [...renderedHints.slice(0, maxRenderedHints - 1), '/ find · Esc back'];
|
|
399
416
|
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' : '')
|
|
417
|
+
: (state.request.interrupt === undefined ? '' : 'Ctrl+C interrupt · ') + 'W wrap · Alt+←→ pan · ↑↓/PgUp/PgDn/wheel scroll · ' + (state.request.documentTail === true ? 'Home start · End follow' : 'Home/End edges') + ' · / find · Ctrl+N/P match · G line' + (source.diff ? ' · [/] hunk' : '')
|
|
401
418
|
+ ' · ' + ['Tab/←→ actions', 'Enter activate', ...customHints, 'Esc back'].join(' · '), width - 4);
|
|
402
419
|
const maxHints = Math.max(1, Math.min(4, pageHeight - 8 - actionRows.length));
|
|
403
420
|
if (readerHints.length > maxHints)
|
|
@@ -405,21 +422,28 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
405
422
|
const compactReader = source !== undefined && pageHeight < 10;
|
|
406
423
|
if (compactReader)
|
|
407
424
|
readerHints = [];
|
|
408
|
-
const
|
|
409
|
-
const
|
|
425
|
+
const renderedSearchRows = readerInput || state.documentQuery ? 1 : 0;
|
|
426
|
+
const hintRows = renderedReader ? renderedHints.length - 1 + renderedSearchRows : readerHints.length;
|
|
427
|
+
const footerRows = feedback ? 5 : actionRows.length + 3 + hintRows;
|
|
428
|
+
const headerRows = renderedReader ? 2 + Number(state.request.question !== '')
|
|
429
|
+
: source === undefined ? 5 : compactReader ? 2 : 3;
|
|
430
|
+
const bodyRows = Math.max(1, pageHeight - headerRows - footerRows);
|
|
410
431
|
const markdownWidth = Math.max(1, width - 6);
|
|
411
432
|
const layout = source === undefined ? undefined : documentLayout(source, theme, markdownWidth, {
|
|
412
433
|
wordWrap: state.documentWordWrap ?? state.request.documentPosition?.wordWrap ?? true,
|
|
413
434
|
column: state.documentColumn ?? state.request.documentPosition?.column ?? 0,
|
|
414
435
|
});
|
|
415
|
-
const
|
|
436
|
+
const textDocument = layout === undefined
|
|
437
|
+
? state.renderDocument?.(theme, markdownWidth) ?? documentRows(state.request, theme, markdownWidth)
|
|
438
|
+
: undefined;
|
|
439
|
+
const document = layout?.rows ?? textDocument ?? [];
|
|
416
440
|
const maxStart = Math.max(0, document.length - bodyRows);
|
|
417
441
|
const start = Math.max(0, Math.min(state.documentScroll === Number.POSITIVE_INFINITY ? maxStart : layout !== undefined && state.documentAnchor !== undefined
|
|
418
442
|
? documentStart(layout, state.documentAnchor) : state.documentScroll ?? state.request.documentPosition?.row ?? 0, maxStart));
|
|
419
443
|
const visible = document.slice(start, start + bodyRows);
|
|
420
444
|
const query = state.documentQuery ?? '';
|
|
421
445
|
const matches = source === undefined ? [] : documentMatches(source, query);
|
|
422
|
-
const textMatches =
|
|
446
|
+
const textMatches = textDocument === undefined ? [] : findInRows(textDocument, query);
|
|
423
447
|
const matching = new Set(matches);
|
|
424
448
|
if (layout !== undefined)
|
|
425
449
|
for (let at = 0; at < visible.length; at++) {
|
|
@@ -447,16 +471,17 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
447
471
|
}
|
|
448
472
|
const lines = [
|
|
449
473
|
pageTop(theme, width, `${appName} · ${compactReader ? state.request.question : state.request.title}`),
|
|
450
|
-
...(source === undefined ? [pageRow(theme, '', width)] : []),
|
|
451
|
-
...(compactReader ? [] : [pageRow(theme, ' ' + theme.bold(state.request.question), width)]),
|
|
452
|
-
...(source === undefined ? [pageRow(theme, '', width)] : []),
|
|
474
|
+
...(source === undefined && !renderedReader ? [pageRow(theme, '', width)] : []),
|
|
475
|
+
...(compactReader || renderedReader && state.request.question === '' ? [] : [pageRow(theme, ' ' + theme.bold(state.request.question), width)]),
|
|
476
|
+
...(source === undefined && !renderedReader ? [pageRow(theme, '', width)] : []),
|
|
453
477
|
pageDivider(theme, width),
|
|
454
478
|
...visible.map(line => pageRow(theme, ' ' + line, width)),
|
|
455
479
|
pageDivider(theme, width),
|
|
456
480
|
];
|
|
481
|
+
const actions = [];
|
|
457
482
|
let cursor = { row: Math.max(0, lines.length - 1), column: 1 };
|
|
458
483
|
if (feedback) {
|
|
459
|
-
lines.push(pageRow(theme, ' ' + theme.bold('Revision feedback') + theme.fg('dim', ' · optional'), width));
|
|
484
|
+
lines.push(pageRow(theme, ' ' + theme.bold(theme.text('Revision feedback')) + theme.fg('dim', ' · optional'), width));
|
|
460
485
|
const prefix = theme.fg('accent', '> ');
|
|
461
486
|
const available = Math.max(1, width - 8);
|
|
462
487
|
const displayInput = input.replace(/\r?\n/gu, ' ↵ ');
|
|
@@ -465,7 +490,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
465
490
|
const inputRow = lines.length;
|
|
466
491
|
lines.push(pageRow(theme, ' ' + prefix + value, width));
|
|
467
492
|
// "empty Enter keeps planning" is prose, but it names the key through the catalog.
|
|
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));
|
|
493
|
+
lines.push(pageRow(theme, theme.fg('dim', `[${formatOverlayHint([HOTKEY_SUBMIT, HOTKEY_BACK, HOTKEY_CANCEL_CTRL], 'lowercase', theme.language)} · Ctrl+X editor · empty ${formatHotkeyKeys(HOTKEY_SUBMIT)} keeps planning]`), width));
|
|
469
494
|
lines.push(pageBottom(theme, width));
|
|
470
495
|
cursor = {
|
|
471
496
|
row: inputRow,
|
|
@@ -478,7 +503,7 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
478
503
|
// itself: the query, how many hits it has, and where the reader is in them.
|
|
479
504
|
const index = textMatches.findIndex(match => match.row === state.documentTarget);
|
|
480
505
|
const hits = textMatches.length === 0
|
|
481
|
-
? 'No match'
|
|
506
|
+
? theme.text('No match')
|
|
482
507
|
: `${index < 0 ? 1 : index + 1}/${textMatches.length} matching rows`;
|
|
483
508
|
const label = state.documentInput === 'search' ? 'Find: ' : '';
|
|
484
509
|
const value = input.replace(/\r?\n/gu, ' ');
|
|
@@ -502,23 +527,26 @@ export function renderPlanReviewPage(state, theme, width, height, input, inputCu
|
|
|
502
527
|
cursor = { row: lines.length, column: Math.min(width - 3, 2 + visibleWidth(label + value.slice(0, inputCursor))) };
|
|
503
528
|
lines.push(pageRow(theme, theme.fg('muted', status), width));
|
|
504
529
|
}
|
|
530
|
+
actions.push(...actionLayout.actions.map(action => ({ ...action, row: action.row + lines.length })));
|
|
505
531
|
lines.push(...actionRows.map(row => pageRow(theme, ' ' + row, width)));
|
|
506
532
|
const shortcuts = state.request.presentation === 'document' && state.request.actions?.length
|
|
507
533
|
? 'Tab/←→ select · Enter activate · ' + state.request.actions.map(action => `${action.key.toUpperCase()} ${action.label}`).join(' · ') + ' · Esc back'
|
|
508
|
-
: formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL])
|
|
509
|
-
+ (state.request.presentation === 'plan-review' ? ' · Ctrl+X editor' : '');
|
|
534
|
+
: formatOverlayHint([HOTKEY_PAGE, HOTKEY_NAVIGATE_TAB, HOTKEY_SELECT, HOTKEY_CANCEL], 'lowercase', theme.language)
|
|
535
|
+
+ (state.request.presentation === 'plan-review' && state.request.allowCustom !== false ? ' · Ctrl+X editor' : '');
|
|
510
536
|
// A page long enough to need scrolling is a page someone will want to search.
|
|
511
537
|
const pageHint = source === undefined && state.request.presentation === 'document'
|
|
512
538
|
? `[${shortcuts} · / find · Ctrl+N/P match]`
|
|
513
539
|
: `[${shortcuts}]`;
|
|
514
|
-
|
|
540
|
+
const hints = renderedReader ? renderedHints : source === undefined ? [pageHint] : readerHints;
|
|
541
|
+
lines.push(...hints.map(hint => pageRow(theme, theme.fg('dim', hint), width)));
|
|
515
542
|
lines.push(pageBottom(theme, width));
|
|
516
543
|
}
|
|
517
544
|
return {
|
|
518
545
|
lines,
|
|
519
546
|
cursor,
|
|
547
|
+
actions,
|
|
520
548
|
document: { start, maxStart, pageSize: Math.max(1, bodyRows - 1),
|
|
521
|
-
...(layout === undefined ? state.request.presentation === 'document' ? { position: { row: start, wrap: 0, query
|
|
549
|
+
...(layout === undefined ? state.request.presentation === 'document' ? { position: { row: start, wrap: 0, query } } : {} : { position: { ...documentPosition(layout, start, query),
|
|
522
550
|
...(state.request.documentTail === true ? { following: state.documentScroll === Number.POSITIVE_INFINITY } : {}) } }) },
|
|
523
551
|
cursorVisible: feedback || readerInput,
|
|
524
552
|
};
|
|
@@ -535,39 +563,48 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
535
563
|
const filtering = input !== '';
|
|
536
564
|
const contentWidth = Math.max(1, width - 4);
|
|
537
565
|
const body = [state.request.question];
|
|
566
|
+
const choices = [];
|
|
567
|
+
const actions = [];
|
|
568
|
+
const bodyRow = () => 1 + body.reduce((count, line) => count + wrapTextStable(expandTabs(line, 8, 2), contentWidth).length, 0);
|
|
538
569
|
if (state.request.wait !== undefined) {
|
|
539
570
|
const remaining = Math.max(0, Math.ceil((state.request.wait.deadline - Date.now()) / 1_000));
|
|
540
571
|
body.push(theme.fg('dim', state.waitHeld === true
|
|
541
|
-
? 'Take your time · waiting for your answer'
|
|
542
|
-
:
|
|
572
|
+
? theme.text('Take your time · waiting for your answer')
|
|
573
|
+
: theme.text('Continues in {seconds}s · {key} take time', { seconds: remaining, key: formatHotkeyKeys(HOTKEY_TAKE_TIME) })));
|
|
543
574
|
}
|
|
544
575
|
if (state.request.detail !== undefined && state.request.detail !== '')
|
|
545
576
|
body.push('', state.request.detail);
|
|
546
577
|
if (options.length > 0) {
|
|
547
578
|
const { start, end } = promptSelectorVisibleRange(options.length, state.selected, maxVisible);
|
|
548
|
-
body.push(''
|
|
579
|
+
body.push('');
|
|
580
|
+
for (let index = start; index < end; index++) {
|
|
581
|
+
const option = options[index];
|
|
582
|
+
choices.push({ row: bodyRow(), column: 2, width: contentWidth, index, value: option.value ?? option.label });
|
|
583
|
+
body.push(optionRow(option, index, state, theme, contentWidth, (state.request.options ?? []).indexOf(option)));
|
|
584
|
+
}
|
|
549
585
|
if (options.length > maxVisible) {
|
|
550
|
-
body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · scroll for more`));
|
|
586
|
+
body.push(theme.fg('dim', ` ${state.selected + 1}/${options.length} · ${theme.text('scroll for more')}`));
|
|
551
587
|
}
|
|
552
588
|
// The paged list draws the selected row's preview beside the rows; the card
|
|
553
589
|
// gives it three lines underneath, so demoting a previewed list keeps the
|
|
554
590
|
// content it was being browsed for.
|
|
555
591
|
const preview = options[state.selected]?.preview;
|
|
556
592
|
if (preview !== undefined && preview !== '') {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
593
|
+
const previewWidth = Math.max(1, contentWidth - 2);
|
|
594
|
+
const wrapped = options[state.selected]?.previewFormat === 'markdown'
|
|
595
|
+
? renderMarkdown(preview, theme, previewWidth)
|
|
596
|
+
: wrapTextStable(expandTabs(preview, 8, 4), previewWidth);
|
|
560
597
|
body.push('', ...wrapped.slice(0, CARD_PREVIEW_LINES).map(line => theme.fg('dim', ' ' + line)));
|
|
561
598
|
if (wrapped.length > CARD_PREVIEW_LINES) {
|
|
562
599
|
body.push(theme.fg('dim', ` … ${wrapped.length - CARD_PREVIEW_LINES} more lines`));
|
|
563
600
|
}
|
|
564
601
|
}
|
|
565
602
|
}
|
|
566
|
-
else {
|
|
603
|
+
else if (state.request.options !== undefined) {
|
|
567
604
|
// A filter that matches nothing has to say so. An empty frame reads as a
|
|
568
605
|
// broken list, and the reader cannot tell a typo from an empty transcript.
|
|
569
606
|
body.push('', theme.fg('dim', ` ${state.request.emptyText
|
|
570
|
-
?? (filtering ?
|
|
607
|
+
?? (filtering ? theme.text('No match for {query}.', { query: quote(input) }) : theme.text('Nothing to choose.'))}`));
|
|
571
608
|
}
|
|
572
609
|
/**
|
|
573
610
|
* The query, drawn where the reader types it.
|
|
@@ -578,40 +615,57 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
578
615
|
* hits looked broken rather than empty.
|
|
579
616
|
*/
|
|
580
617
|
const filterRow = state.request.filterable === true && state.request.presentation !== 'progress'
|
|
581
|
-
?
|
|
618
|
+
? bodyRow() + 1
|
|
582
619
|
: undefined;
|
|
583
620
|
if (filterRow !== undefined) {
|
|
584
621
|
// The query and what it kept belong on one line: the reader typed one thing
|
|
585
622
|
// and the list answered with a count.
|
|
586
623
|
const count = filtering ? theme.fg('dim', ` · ${options.length === 0 ? 'no match' : `${state.selected + 1}/${options.length}`}`) : '';
|
|
587
624
|
body.push('', input === ''
|
|
588
|
-
? theme.fg('muted', '
|
|
589
|
-
: theme.fg('text',
|
|
625
|
+
? theme.fg('muted', ' ' + theme.text('type to filter'))
|
|
626
|
+
: ' ' + theme.fg('text', input) + count);
|
|
590
627
|
}
|
|
591
628
|
// A request that has options but no match has nothing to submit: advertising
|
|
592
629
|
// `enter select` there promises a key the handler swallows.
|
|
593
|
-
const exhausted = options.length === 0 &&
|
|
594
|
-
const submit = state.request.submitLabel?.trim()
|
|
595
|
-
|| (options.length === 0 ? 'answer' : state.request.multiSelect === true ? 'confirm' : 'select');
|
|
630
|
+
const exhausted = options.length === 0 && state.request.options !== undefined && state.request.allowCustom === false;
|
|
631
|
+
const submit = (state.request.multiSelect === true ? state.request.submitLabel : options[state.selected]?.submitLabel ?? state.request.submitLabel)?.trim()
|
|
632
|
+
|| (options.length === 0 ? theme.text('answer') : state.request.multiSelect === true ? theme.text('confirm') : theme.text('select'));
|
|
596
633
|
// The request supplies the submit verb; the key itself comes from the catalog.
|
|
597
634
|
const select = `${formatHotkeyKeys(HOTKEY_SELECT)} ${submit}`;
|
|
598
635
|
const navigation = state.request.presentation === 'progress'
|
|
599
|
-
? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog')
|
|
636
|
+
? formatOverlayHint([HOTKEY_CANCEL, HOTKEY_CANCEL_CTRL], 'catalog', theme.language)
|
|
600
637
|
: exhausted
|
|
601
|
-
? formatOverlayHint([HOTKEY_CANCEL])
|
|
638
|
+
? formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)
|
|
602
639
|
: options.length === 0
|
|
603
|
-
? `${select} · ${formatOverlayHint([HOTKEY_CANCEL])}`
|
|
640
|
+
? `${select} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`
|
|
604
641
|
: state.request.multiSelect === true
|
|
605
|
-
? `${formatOverlayHint([HOTKEY_NAVIGATE])} · ${formatOverlayHint([HOTKEY_TOGGLE]
|
|
606
|
-
: `${formatOverlayHint([HOTKEY_NAVIGATE]
|
|
642
|
+
? `${formatOverlayHint([HOTKEY_NAVIGATE], 'lowercase', theme.language)} · ${formatOverlayHint([state.request.answerFormat === 'json' ? HOTKEY_ANSWER_TOGGLE : HOTKEY_TOGGLE], 'lowercase', theme.language)} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`
|
|
643
|
+
: `${formatOverlayHint([HOTKEY_NAVIGATE], 'lowercase', theme.language)} · ${formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language)}`;
|
|
607
644
|
const dismiss = state.request.dismissLabel;
|
|
608
645
|
const actionHints = (state.request.actions ?? []).map(action => `${action.key} ${action.label}`);
|
|
609
646
|
const hints = [
|
|
610
|
-
dismiss === undefined ? navigation : navigation.replace(formatOverlayHint([HOTKEY_CANCEL]), `${formatHotkeyKeys(HOTKEY_CANCEL)} ${dismiss.toLowerCase()}`),
|
|
611
|
-
...(state.request.skippable === true ? [formatOverlayHint([HOTKEY_SKIP])] : []),
|
|
612
|
-
...actionHints,
|
|
647
|
+
dismiss === undefined ? navigation : navigation.replace(formatOverlayHint([HOTKEY_CANCEL], 'lowercase', theme.language), `${formatHotkeyKeys(HOTKEY_CANCEL)} ${dismiss.toLowerCase()}`),
|
|
648
|
+
...(state.request.skippable === true ? [formatOverlayHint([HOTKEY_SKIP], 'lowercase', theme.language)] : []),
|
|
613
649
|
];
|
|
614
|
-
body.push('', theme.fg('dim', hints.join(' · ')));
|
|
650
|
+
body.push('', theme.fg('dim', theme.text(hints.join(' · '))));
|
|
651
|
+
if (state.request.options !== undefined) {
|
|
652
|
+
const buttons = [...(options.length === 0 ? [] : [{ key: 'enter', label: submit }]), ...(state.request.actions ?? []).filter(action => options.length > 0 || action.scope === 'list')];
|
|
653
|
+
let line = '';
|
|
654
|
+
for (const button of buttons) {
|
|
655
|
+
const label = truncateToWidth(`[ ${button.key === 'enter' ? 'Enter' : button.key} ${button.label} ]`, contentWidth);
|
|
656
|
+
if (line !== '' && visibleWidth(line + ' ' + label) > contentWidth) {
|
|
657
|
+
body.push(line);
|
|
658
|
+
line = '';
|
|
659
|
+
}
|
|
660
|
+
const gap = line === '' ? '' : ' ';
|
|
661
|
+
actions.push({ row: bodyRow(), column: 2 + visibleWidth(line + gap), width: visibleWidth(label), index: -1, key: button.key.toLowerCase() });
|
|
662
|
+
line += gap + theme.fg('accent', label);
|
|
663
|
+
}
|
|
664
|
+
if (line !== '')
|
|
665
|
+
body.push(line);
|
|
666
|
+
}
|
|
667
|
+
else if (actionHints.length > 0)
|
|
668
|
+
body.push(theme.fg('dim', actionHints.join(' · ')));
|
|
615
669
|
const card = renderFramedBlock({
|
|
616
670
|
header: state.request.title,
|
|
617
671
|
state: 'warning',
|
|
@@ -620,14 +674,14 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
620
674
|
applyBg: false,
|
|
621
675
|
}, theme);
|
|
622
676
|
if (state.request.allowCustom === false || state.request.presentation === 'progress') {
|
|
623
|
-
// With a filter line the caret
|
|
624
|
-
//
|
|
677
|
+
// With a filter line the caret follows the editor's insertion point;
|
|
678
|
+
// without one it stays on body padding, because the selector glyph
|
|
625
679
|
// owns focus while no text editor is present.
|
|
626
680
|
return {
|
|
627
|
-
lines: card,
|
|
681
|
+
lines: card, choices, actions,
|
|
628
682
|
cursor: filterRow === undefined
|
|
629
683
|
? { row: Math.min(1, Math.max(0, card.length - 1)), column: 1 }
|
|
630
|
-
: { row: filterRow
|
|
684
|
+
: { row: filterRow, column: 4 + visibleWidth(input.slice(0, inputCursor)) },
|
|
631
685
|
cursorVisible: filterRow !== undefined,
|
|
632
686
|
};
|
|
633
687
|
}
|
|
@@ -656,10 +710,10 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
656
710
|
input: displayInput,
|
|
657
711
|
inputCursor,
|
|
658
712
|
status: ' ' + theme.fg('accent', label) + ' ',
|
|
659
|
-
...(state.request.question === '' ? {} : { inlineHint: state.request.question }),
|
|
713
|
+
...(displayInput !== '' || state.request.question === '' ? {} : { inlineHint: state.request.question }),
|
|
660
714
|
// The keys live on the bottom rule so the frame says how to answer without
|
|
661
715
|
// spending a row on it.
|
|
662
|
-
...(hints.length === 0 ? {} : { note: hints.join(' · ') }),
|
|
716
|
+
...(hints.length === 0 ? {} : { note: theme.text(hints.join(' · ')) }),
|
|
663
717
|
border: 'accent',
|
|
664
718
|
}, theme);
|
|
665
719
|
return { lines: editor.lines, cursor: editor.cursor, cursorVisible: true };
|
|
@@ -668,12 +722,12 @@ export function renderPromptSelector(state, theme, width, input, inputCursor, ma
|
|
|
668
722
|
width,
|
|
669
723
|
input: displayInput,
|
|
670
724
|
inputCursor,
|
|
671
|
-
status: theme.fg('muted', secret ? 'API key · hidden' : input === '' ? 'answer' : 'custom answer'),
|
|
725
|
+
status: theme.fg('muted', secret ? theme.text('API key · hidden') : input === '' ? theme.text('answer') : theme.text('custom answer')),
|
|
672
726
|
border: 'accent',
|
|
673
727
|
}, theme);
|
|
674
728
|
const editorStart = card.length + 1;
|
|
675
729
|
return {
|
|
676
|
-
lines: [...card, '', ...editor.lines],
|
|
730
|
+
lines: [...card, '', ...editor.lines], choices, actions,
|
|
677
731
|
cursor: { row: editorStart + editor.cursor.row, column: editor.cursor.column },
|
|
678
732
|
cursorVisible: true,
|
|
679
733
|
};
|
|
@@ -700,6 +754,15 @@ export function togglePromptSelection(state, query = '') {
|
|
|
700
754
|
checked.add(index);
|
|
701
755
|
return { ...state, checked };
|
|
702
756
|
}
|
|
757
|
+
/** Keep option values and free text separate, including punctuation in labels. */
|
|
758
|
+
export function structuredPromptAnswer(state, custom) {
|
|
759
|
+
const options = state.request.options ?? [];
|
|
760
|
+
const selected = state.request.multiSelect === true
|
|
761
|
+
? options.flatMap((option, index) => state.checked.has(index) && !option.disabled ? [option.value ?? option.label] : [])
|
|
762
|
+
: custom.trim() !== '' || options[state.selected] === undefined || options[state.selected]?.disabled === true
|
|
763
|
+
? [] : [options[state.selected].value ?? options[state.selected].label];
|
|
764
|
+
return { selected, ...(custom.trim() === '' ? {} : { custom: custom.trim() }) };
|
|
765
|
+
}
|
|
703
766
|
/** Resolve the selected labels in stable option order. */
|
|
704
767
|
export function selectedPromptAnswer(state) {
|
|
705
768
|
const options = state.request.presentation === 'tree'
|