@aragon-agent/cli 0.6.34 → 0.6.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +173 -7
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +67 -3
- package/dist/agent/controller.js +106 -5
- package/dist/agent/headless.js +31 -0
- package/dist/agent/kit-apply.d.ts +56 -0
- package/dist/agent/kit-apply.js +209 -0
- package/dist/agent/model-profile-settings.d.ts +28 -0
- package/dist/agent/model-profile-settings.js +142 -29
- package/dist/agent/reducer.d.ts +13 -2
- package/dist/agent/reducer.js +1 -0
- package/dist/cli.js +12 -0
- package/dist/commands/agents-command.d.ts +15 -0
- package/dist/commands/agents-command.js +27 -0
- package/dist/commands/builtins.js +135 -4
- package/dist/commands/kit-command.d.ts +20 -0
- package/dist/commands/kit-command.js +131 -0
- package/dist/commands/perf.d.ts +11 -0
- package/dist/commands/perf.js +2 -1
- package/dist/commands/registry.d.ts +14 -0
- package/dist/config/app-paths.d.ts +15 -0
- package/dist/config/app-paths.js +33 -2
- package/dist/config/env.d.ts +4 -1
- package/dist/config/env.js +28 -2
- package/dist/config/kit-resolution.d.ts +93 -0
- package/dist/config/kit-resolution.js +226 -0
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/model-kits.d.ts +224 -0
- package/dist/config/model-kits.js +302 -0
- package/dist/config/model-profile-store.d.ts +25 -0
- package/dist/config/model-profiles.d.ts +41 -3
- package/dist/config/model-profiles.js +46 -5
- package/dist/config/schema.d.ts +84 -0
- package/dist/config/schema.js +39 -0
- package/dist/config/store.js +12 -1
- package/dist/config/ui-state.d.ts +18 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +25 -0
- package/dist/exec/events.js +4 -0
- package/dist/exec/index.js +16 -1
- package/dist/exec/runner.js +10 -0
- package/dist/input/keymap.js +18 -4
- package/dist/proc/limits.d.ts +16 -0
- package/dist/proc/limits.js +16 -0
- package/dist/proc/supervisor.d.ts +20 -1
- package/dist/proc/supervisor.js +42 -14
- package/dist/prompts/command.d.ts +34 -0
- package/dist/prompts/command.js +237 -0
- package/dist/prompts/copy.d.ts +89 -0
- package/dist/prompts/copy.js +131 -0
- package/dist/prompts/fill.d.ts +51 -0
- package/dist/prompts/fill.js +46 -0
- package/dist/prompts/limits.d.ts +53 -0
- package/dist/prompts/limits.js +53 -0
- package/dist/prompts/normalize.d.ts +53 -0
- package/dist/prompts/normalize.js +190 -0
- package/dist/prompts/resolve.d.ts +31 -0
- package/dist/prompts/resolve.js +60 -0
- package/dist/prompts/search.d.ts +58 -0
- package/dist/prompts/search.js +146 -0
- package/dist/prompts/store.d.ts +86 -0
- package/dist/prompts/store.js +453 -0
- package/dist/prompts/types.d.ts +204 -0
- package/dist/prompts/types.js +13 -0
- package/dist/session/checkpoint.d.ts +8 -1
- package/dist/session/checkpoint.js +6 -0
- package/dist/session/persist.d.ts +19 -1
- package/dist/session/persist.js +13 -1
- package/dist/session/startup-session.d.ts +7 -2
- package/dist/session/startup-session.js +5 -2
- package/dist/session/store.js +3 -0
- package/dist/session/validate-session.js +64 -1
- package/dist/team/limits.d.ts +25 -0
- package/dist/team/limits.js +26 -0
- package/dist/team/runtime.js +27 -2
- package/dist/team/settled.d.ts +57 -0
- package/dist/team/settled.js +87 -0
- package/dist/team/subagent.js +48 -2
- package/dist/team/timeline.d.ts +74 -0
- package/dist/team/timeline.js +101 -0
- package/dist/team/types.d.ts +24 -1
- package/dist/todo/acceptance.d.ts +120 -0
- package/dist/todo/acceptance.js +399 -0
- package/dist/todo/follow-through.d.ts +48 -23
- package/dist/todo/follow-through.js +110 -44
- package/dist/todo/limits.d.ts +62 -1
- package/dist/todo/limits.js +62 -1
- package/dist/todo/panel-rows.d.ts +29 -39
- package/dist/todo/panel-rows.js +37 -83
- package/dist/todo/plan-sections.d.ts +60 -0
- package/dist/todo/plan-sections.js +108 -0
- package/dist/todo/prompt.d.ts +17 -12
- package/dist/todo/prompt.js +59 -40
- package/dist/todo/row-window.d.ts +51 -0
- package/dist/todo/row-window.js +107 -0
- package/dist/todo/store.d.ts +139 -31
- package/dist/todo/store.js +271 -73
- package/dist/todo/todo-tool.d.ts +33 -8
- package/dist/todo/todo-tool.js +186 -30
- package/dist/todo/types.d.ts +58 -0
- package/dist/ui/App.js +414 -54
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +17 -1
- package/dist/ui/Header.js +8 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +22 -2
- package/dist/ui/ScrollbackShell.d.ts +22 -7
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +18 -0
- package/dist/ui/StatusBar.js +11 -3
- package/dist/ui/TeamPanel.d.ts +35 -41
- package/dist/ui/TeamPanel.js +53 -163
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +23 -3
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- package/dist/ui/entries/TeamCard.d.ts +18 -2
- package/dist/ui/entries/TeamCard.js +47 -13
- package/dist/ui/entries/TodoCard.d.ts +26 -11
- package/dist/ui/entries/TodoCard.js +56 -15
- package/dist/ui/glyphs.d.ts +19 -0
- package/dist/ui/glyphs.js +4 -0
- package/dist/ui/layout/agents-board.d.ts +102 -0
- package/dist/ui/layout/agents-board.js +389 -0
- package/dist/ui/layout/cells.d.ts +61 -0
- package/dist/ui/layout/cells.js +156 -0
- package/dist/ui/layout/header-hints.d.ts +23 -3
- package/dist/ui/layout/header-hints.js +73 -7
- package/dist/ui/layout/kit-chip.d.ts +50 -0
- package/dist/ui/layout/kit-chip.js +81 -0
- package/dist/ui/layout/status-detail-layout.js +9 -0
- package/dist/ui/layout/status-layout.d.ts +13 -0
- package/dist/ui/layout/team-row.d.ts +139 -0
- package/dist/ui/layout/team-row.js +246 -0
- package/dist/ui/layout/todo-layout.d.ts +38 -0
- package/dist/ui/layout/todo-layout.js +75 -0
- package/dist/ui/layout/virtual-window.js +32 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +24 -2
- package/dist/ui/overlays/KitEditor.d.ts +23 -0
- package/dist/ui/overlays/KitEditor.js +101 -0
- package/dist/ui/overlays/KitManager.d.ts +23 -0
- package/dist/ui/overlays/KitManager.js +104 -0
- package/dist/ui/overlays/KitPicker.d.ts +49 -0
- package/dist/ui/overlays/KitPicker.js +58 -0
- package/dist/ui/overlays/ModelProfilePicker.js +16 -6
- package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
- package/dist/ui/overlays/PromptEditorPage.js +227 -0
- package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
- package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
- package/dist/ui/overlays/PromptPicker.d.ts +36 -0
- package/dist/ui/overlays/PromptPicker.js +129 -0
- package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
- package/dist/ui/overlays/PromptTextArea.js +141 -0
- package/dist/ui/overlays/SettingsScreen.js +139 -16
- package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
- package/dist/ui/overlays/kit-picker-state.js +136 -0
- package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
- package/dist/ui/overlays/kit-settings-state.js +267 -0
- package/dist/ui/overlays/model-profile-state.d.ts +26 -3
- package/dist/ui/overlays/model-profile-state.js +59 -6
- package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
- package/dist/ui/overlays/prompt-library-state.js +335 -0
- package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
- package/dist/ui/overlays/prompt-picker-state.js +98 -0
- package/dist/ui/scrollback/LiveTail.d.ts +2 -2
- package/dist/ui/scrollback/LiveTail.js +7 -2
- package/dist/ui/scrollback/band.js +14 -1
- package/dist/ui/scrollback/live-preview.js +10 -3
- package/dist/ui/scrollback/tail-window.d.ts +12 -0
- package/dist/ui/scrollback/tail-window.js +17 -2
- package/dist/ui/scrollback/use-scrollback-view.js +7 -0
- package/dist/ui/team-status.d.ts +166 -0
- package/dist/ui/team-status.js +252 -0
- package/dist/ui/transcript-text.js +13 -4
- package/dist/ui/use-startup-notices.d.ts +35 -1
- package/dist/ui/use-startup-notices.js +29 -2
- package/dist/ui/use-wheel-routing.js +10 -2
- package/package.json +2 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The prompt manager: `Ctrl+P` (quick-prompt-library §4.4 / §6.4).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* A LIST PLUS A DETAIL BLOCK, the `KitPicker` idiom (F-17): three columns of prompt
|
|
8
|
+
* metadata never fit on a 72-column terminal, and the detail block is how this
|
|
9
|
+
* package already shows everything that does not fit in a row.
|
|
10
|
+
*
|
|
11
|
+
* THE OVERLAY OWNS NO DATA. Every mutation goes out through `onMutate`, and the new
|
|
12
|
+
* snapshot comes back through `library`. An overlay that patched its own copy would
|
|
13
|
+
* show a prompt the disk does not have (I-6), and the first time that happened the
|
|
14
|
+
* user would find out at the next launch.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useReducer, useRef } from 'react';
|
|
17
|
+
import { Text, useInput } from 'ink';
|
|
18
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
19
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
20
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
21
|
+
import { promptDeletedCopy, promptErrorCopy, promptSavedCopy, PROMPT_EMPTY_BODY_COPY, PROMPT_EMPTY_TITLE_COPY } from '../../prompts/copy.js';
|
|
22
|
+
import { INITIAL_PROMPT_LIBRARY_STATE, deleteDetail, finishPromptEditor, libraryDetail, libraryHint, libraryRows, promptLibraryReducer, } from './prompt-library-state.js';
|
|
23
|
+
import { PromptEditorPage } from './PromptEditorPage.js';
|
|
24
|
+
/** The reducer takes the library as a third argument, so it is bound per dispatch. */
|
|
25
|
+
function reduce(state, dispatched) {
|
|
26
|
+
return promptLibraryReducer(state, dispatched.action, dispatched.library);
|
|
27
|
+
}
|
|
28
|
+
export function PromptLibraryOverlay(props) {
|
|
29
|
+
const { library, cols, theme, caps } = props;
|
|
30
|
+
const glyphs = pickGlyphs(caps);
|
|
31
|
+
const [state, rawDispatch] = useReducer(reduce, INITIAL_PROMPT_LIBRARY_STATE);
|
|
32
|
+
const dispatch = (action) => rawDispatch({ action, library });
|
|
33
|
+
const rows = libraryRows(library);
|
|
34
|
+
const count = rows.length;
|
|
35
|
+
const cursor = count === 0 ? 0 : Math.min(state.index, count - 1);
|
|
36
|
+
const focused = rows[cursor];
|
|
37
|
+
const offset = useProfileScroll(props, cursor);
|
|
38
|
+
/**
|
|
39
|
+
* CONSUME THE INTENT EXACTLY ONCE (I-8).
|
|
40
|
+
*
|
|
41
|
+
* A `useRef`, not state: the ref is written before the dispatch is processed, so a
|
|
42
|
+
* StrictMode double-invoke of this effect sees `nonce <= consumed` and does nothing.
|
|
43
|
+
* State would be one render behind and would open the editor twice.
|
|
44
|
+
*/
|
|
45
|
+
const consumed = useRef(0);
|
|
46
|
+
const intent = props.intent;
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!intent || intent.nonce <= consumed.current)
|
|
49
|
+
return;
|
|
50
|
+
consumed.current = intent.nonce;
|
|
51
|
+
dispatch({ type: 'init', entry: intent.entry, library, nonce: intent.nonce });
|
|
52
|
+
}, [intent]);
|
|
53
|
+
/** One mutation, its refusal routed to the same two channels every time. */
|
|
54
|
+
const mutate = (action) => {
|
|
55
|
+
const result = props.onMutate(action);
|
|
56
|
+
if (!result.ok)
|
|
57
|
+
props.onNotice?.('warn', promptErrorCopy(result.code));
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
const save = () => {
|
|
61
|
+
const verdict = finishPromptEditor(state.draft);
|
|
62
|
+
if (!verdict.ok) {
|
|
63
|
+
dispatch({ type: 'failed', code: verdict.code, field: verdict.field });
|
|
64
|
+
props.onNotice?.('warn', promptErrorCopy(verdict.code));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const draft = state.draft;
|
|
68
|
+
const result = draft.id === null
|
|
69
|
+
? mutate({ kind: 'create', draft })
|
|
70
|
+
: mutate({ kind: 'update', id: draft.id, draft });
|
|
71
|
+
if (!result.ok) {
|
|
72
|
+
// THE PAGE DOES NOT CLOSE AND THE DRAFT IS NOT TOUCHED (D-12): the editor is
|
|
73
|
+
// where the user has invested the most, so a refusal must cost nothing.
|
|
74
|
+
dispatch({ type: 'failed', code: result.code });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
dispatch({ type: 'commit', id: result.id ?? draft.id });
|
|
78
|
+
props.onNotice?.('info', promptSavedCopy(draft.title.trim()));
|
|
79
|
+
};
|
|
80
|
+
const confirmDelete = () => {
|
|
81
|
+
const id = state.pendingDeleteId;
|
|
82
|
+
if (!id) {
|
|
83
|
+
dispatch({ type: 'cancelled' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const title = library.prompts.find((prompt) => prompt.id === id)?.title ?? '';
|
|
87
|
+
const result = mutate({ kind: 'delete', id });
|
|
88
|
+
dispatch({ type: 'cancelled' });
|
|
89
|
+
// `promptDeletedCopy`, not `promptSavedCopy`: the two sentences are one word apart
|
|
90
|
+
// and the wrong one tells the user their prompt still exists.
|
|
91
|
+
if (result.ok)
|
|
92
|
+
props.onNotice?.('info', promptDeletedCopy(title));
|
|
93
|
+
};
|
|
94
|
+
useInput((input, key) => {
|
|
95
|
+
if (props.isActive === false)
|
|
96
|
+
return;
|
|
97
|
+
// THE EDITOR PAGE OWNS EVERY KEY WHILE IT IS OPEN, Esc INCLUDED. Two `useInput`
|
|
98
|
+
// hooks are live at once (this one and the page's), so handling Esc in both would
|
|
99
|
+
// dispatch `esc` TWICE for one keypress: the first arms the discard and the second
|
|
100
|
+
// executes it, which silently throws away a drafted body on a single Esc. The page
|
|
101
|
+
// already reports Esc through `onEsc`, so this branch must stay silent.
|
|
102
|
+
if (state.page === 'editor')
|
|
103
|
+
return;
|
|
104
|
+
if (state.page === 'delete') {
|
|
105
|
+
if (key.escape || input === 'n' || input === 'N') {
|
|
106
|
+
dispatch({ type: 'cancelled' });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (key.return || input === 'y' || input === 'Y')
|
|
110
|
+
confirmDelete();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// --- list page ---------------------------------------------------------
|
|
114
|
+
if (key.escape) {
|
|
115
|
+
props.onClose();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// THE RAW SEQUENCE **AND** THE PARSED CHORD (D-23, corrected by measurement).
|
|
119
|
+
// Ink 5's `parse-keypress` recognises `\x1b[1;5A`, reports it as
|
|
120
|
+
// `ctrl + upArrow`, and hands the component an EMPTY `input` - so the sequence
|
|
121
|
+
// comparison alone would be a dead branch on this runtime, and the chord
|
|
122
|
+
// comparison alone would miss a host that passes the raw bytes through. Either
|
|
123
|
+
// shape reorders; `\x1b[A` (modifier lost) matches neither and only moves focus.
|
|
124
|
+
const reorderUp = input === '\x1b[1;5A' || (key.ctrl && key.upArrow);
|
|
125
|
+
const reorderDown = input === '\x1b[1;5B' || (key.ctrl && key.downArrow);
|
|
126
|
+
if (reorderUp || reorderDown) {
|
|
127
|
+
if (!focused)
|
|
128
|
+
return;
|
|
129
|
+
const result = mutate({ kind: 'move', id: focused.id, delta: reorderUp ? -1 : 1 });
|
|
130
|
+
if (result.ok)
|
|
131
|
+
dispatch({ type: 'list', focusId: focused.id });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (key.upArrow || (key.tab && key.shift)) {
|
|
135
|
+
dispatch({ type: 'focus', index: count === 0 ? 0 : (cursor - 1 + count) % count });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (key.downArrow) {
|
|
139
|
+
dispatch({ type: 'focus', index: count === 0 ? 0 : (cursor + 1) % count });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (key.return) {
|
|
143
|
+
if (focused)
|
|
144
|
+
dispatch({ type: 'edit', id: focused.id, library });
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (key.ctrl || key.meta)
|
|
148
|
+
return;
|
|
149
|
+
if (input === 'a') {
|
|
150
|
+
dispatch({ type: 'create' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (input === 'd' && focused) {
|
|
154
|
+
dispatch({ type: 'delete', id: focused.id });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (input === 'f' && focused) {
|
|
158
|
+
const prompt = library.prompts.find((entry) => entry.id === focused.id);
|
|
159
|
+
mutate({ kind: 'favorite', id: focused.id, favorite: prompt?.favorite !== true });
|
|
160
|
+
dispatch({ type: 'list', focusId: focused.id });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
if (state.page === 'editor') {
|
|
164
|
+
return (_jsx(PromptEditorPage, { isActive: props.isActive, state: state, cols: cols, maxRows: props.maxRows, theme: theme, caps: caps, ...(props.reducedMotion === undefined ? {} : { reducedMotion: props.reducedMotion }), onDraft: (patch) => dispatch({ type: 'draft', patch }), onSave: save, onEsc: () => dispatch({ type: 'esc' }), onCancel: () => dispatch({ type: 'cancelled' }) }));
|
|
165
|
+
}
|
|
166
|
+
const body = [];
|
|
167
|
+
if (state.page === 'delete') {
|
|
168
|
+
for (const [at, line] of deleteDetail(library, state.pendingDeleteId).entries()) {
|
|
169
|
+
body.push(_jsx(Text, { wrap: "truncate", color: at === 0 ? theme.accent : theme.muted, children: clipProfileText(line, cols - 8) }, `del-${at}`));
|
|
170
|
+
}
|
|
171
|
+
return (_jsx(OverlayFrame, { title: "Delete prompt", hint: libraryHint(state, count), rows: body, scrollOffset: 0, maxRows: props.maxRows, cols: cols, theme: theme, caps: caps }));
|
|
172
|
+
}
|
|
173
|
+
for (const [at, row] of rows.entries()) {
|
|
174
|
+
const isCursor = at === cursor;
|
|
175
|
+
body.push(_jsx(Text, { wrap: "truncate", color: isCursor ? theme.accent : theme.muted, children: `${isCursor ? glyphs.caret : ' '} ${clipProfileText(row.label, cols - 8)}` }, row.id));
|
|
176
|
+
}
|
|
177
|
+
if (count === 0) {
|
|
178
|
+
// TWO ROWS, NOT AN EMPTY SCREEN (§9.7): an empty list that says nothing reads as a
|
|
179
|
+
// broken panel, and the key that fixes it (`a`) is not otherwise discoverable. The
|
|
180
|
+
// sentences come from `prompts/copy.ts` so this state and the picker's own empty
|
|
181
|
+
// state cannot drift into two wordings of the same advice.
|
|
182
|
+
body.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: clipProfileText(PROMPT_EMPTY_TITLE_COPY, cols - 8) }, "empty-title"));
|
|
183
|
+
body.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: clipProfileText(PROMPT_EMPTY_BODY_COPY, cols - 8) }, "empty-body"));
|
|
184
|
+
}
|
|
185
|
+
// THE DETAIL BLOCK DESCRIBES THE FOCUSED ROW ONLY, and it is last so a narrow
|
|
186
|
+
// terminal clips the description rather than the list.
|
|
187
|
+
for (const [at, line] of libraryDetail(focused, library, cols).entries()) {
|
|
188
|
+
body.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: clipProfileText(` ${line}`, cols - 8) }, `detail-${at}`));
|
|
189
|
+
}
|
|
190
|
+
return (_jsx(OverlayFrame, { title: `Prompts${count > 0 ? ` (${count})` : ''}`, hint: libraryHint(state, count), rows: body, scrollOffset: offset, onScrollClamp: props.onScrollClamp, maxRows: props.maxRows, cols: cols, theme: theme, caps: caps }));
|
|
191
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt picker: `/prompts` (quick-prompt-library §4.5).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* THE QUERY BOX IS THE LIST. There is no separate search field and no `/` mode: the
|
|
7
|
+
* picker exists for one keystroke-to-answer gesture, and an extra mode to enter is an
|
|
8
|
+
* extra thing that can be wrong about which component owns the next key.
|
|
9
|
+
*
|
|
10
|
+
* `Ctrl+Enter` IS RECOGNISED BY ITS BYTES, NOT BY `key.ctrl && key.return`. The stdin
|
|
11
|
+
* filter normalises every modified Enter (`\x1b[13;5u`, the win32 record, `\x1b[13;2u`)
|
|
12
|
+
* into `ENTER_NEWLINE_FRAME` before Ink sees it (enter-sequences.ts), so the parsed
|
|
13
|
+
* chord never occurs - a branch written that way is dead code that LOOKS implemented
|
|
14
|
+
* (R-11). On a terminal without keyboard enhancement the chord degrades to a plain
|
|
15
|
+
* `Enter`, which fills without sending; the footer therefore names `Enter fill` first.
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import type { Theme } from '../theme.js';
|
|
19
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
20
|
+
import type { PromptLibrary, PromptLibraryEntry } from '../../prompts/types.js';
|
|
21
|
+
export interface PromptPickerProps {
|
|
22
|
+
isActive: boolean;
|
|
23
|
+
library: PromptLibrary;
|
|
24
|
+
cols: number;
|
|
25
|
+
maxRows: number;
|
|
26
|
+
scrollOffset: number;
|
|
27
|
+
onScrollClamp?: (offset: number) => void;
|
|
28
|
+
theme: Theme;
|
|
29
|
+
caps: TermCapabilities;
|
|
30
|
+
/** `submit` is true only for `Ctrl+Enter` (see `SUBMIT_FRAMES`). */
|
|
31
|
+
onFill: (id: string, submit: boolean) => void;
|
|
32
|
+
/** `Tab` / `a`: hand over to the manager without leaving the overlay stack. */
|
|
33
|
+
onManage: (entry: PromptLibraryEntry) => void;
|
|
34
|
+
onClose: () => void;
|
|
35
|
+
}
|
|
36
|
+
export declare function PromptPicker(props: PromptPickerProps): React.ReactElement;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The prompt picker: `/prompts` (quick-prompt-library §4.5).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* THE QUERY BOX IS THE LIST. There is no separate search field and no `/` mode: the
|
|
8
|
+
* picker exists for one keystroke-to-answer gesture, and an extra mode to enter is an
|
|
9
|
+
* extra thing that can be wrong about which component owns the next key.
|
|
10
|
+
*
|
|
11
|
+
* `Ctrl+Enter` IS RECOGNISED BY ITS BYTES, NOT BY `key.ctrl && key.return`. The stdin
|
|
12
|
+
* filter normalises every modified Enter (`\x1b[13;5u`, the win32 record, `\x1b[13;2u`)
|
|
13
|
+
* into `ENTER_NEWLINE_FRAME` before Ink sees it (enter-sequences.ts), so the parsed
|
|
14
|
+
* chord never occurs - a branch written that way is dead code that LOOKS implemented
|
|
15
|
+
* (R-11). On a terminal without keyboard enhancement the chord degrades to a plain
|
|
16
|
+
* `Enter`, which fills without sending; the footer therefore names `Enter fill` first.
|
|
17
|
+
*/
|
|
18
|
+
import { useState } from 'react';
|
|
19
|
+
import { Text, useInput } from 'ink';
|
|
20
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
21
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
22
|
+
import { ENTER_NEWLINE_FRAME } from '../../input/limits.js';
|
|
23
|
+
import { PROMPT_PICKER_LIMITS } from '../../prompts/limits.js';
|
|
24
|
+
import { nextView } from '../../prompts/search.js';
|
|
25
|
+
import { stripPasteFrames } from '../paste-frames.js';
|
|
26
|
+
import { stripEnterFrames } from '../enter-frames.js';
|
|
27
|
+
import { clipProfileText, useProfileScroll } from './ModelProfilePicker.js';
|
|
28
|
+
import { pickerDetail, pickerDigitTarget, pickerHint, pickerRows, } from './prompt-picker-state.js';
|
|
29
|
+
/** Every modified Enter the stdin filter folds into its frame, plus the raw CSI-u. */
|
|
30
|
+
const SUBMIT_FRAMES = [ENTER_NEWLINE_FRAME, '\x1b[13;5u'];
|
|
31
|
+
export function PromptPicker(props) {
|
|
32
|
+
const { library, cols, theme, caps } = props;
|
|
33
|
+
const glyphs = pickGlyphs(caps);
|
|
34
|
+
const [query, setQuery] = useState('');
|
|
35
|
+
const [view, setView] = useState('manual');
|
|
36
|
+
// THE FOCUS IS AN ID, NOT AN INDEX (D-9's cost, paid here): both filtering and
|
|
37
|
+
// `Ctrl+S` can reorder the list under the cursor, and an index would silently point
|
|
38
|
+
// at a different prompt after either.
|
|
39
|
+
const [focusId, setFocusId] = useState(null);
|
|
40
|
+
const rows = pickerRows({ library, query, view });
|
|
41
|
+
const found = rows.findIndex((row) => row.id === focusId);
|
|
42
|
+
const cursor = rows.length === 0 ? 0 : Math.max(0, Math.min(found, rows.length - 1));
|
|
43
|
+
const focused = rows[cursor];
|
|
44
|
+
const offset = useProfileScroll(props, cursor);
|
|
45
|
+
const empty = library.prompts.length === 0;
|
|
46
|
+
const fill = (id, submit) => {
|
|
47
|
+
if (!id)
|
|
48
|
+
return;
|
|
49
|
+
props.onFill(id, submit);
|
|
50
|
+
};
|
|
51
|
+
useInput((input, key) => {
|
|
52
|
+
if (props.isActive === false)
|
|
53
|
+
return;
|
|
54
|
+
if (key.escape) {
|
|
55
|
+
props.onClose();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// BEFORE `key.return`: the frame arrives with `return` false, so this branch must
|
|
59
|
+
// not be reachable only through the plain-Enter path.
|
|
60
|
+
if (SUBMIT_FRAMES.includes(input)) {
|
|
61
|
+
fill(focused?.id, true);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (key.return) {
|
|
65
|
+
fill(focused?.id, false);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (key.ctrl && (input === 's' || input === 'S')) {
|
|
69
|
+
setView(nextView(view));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (key.upArrow) {
|
|
73
|
+
if (rows.length === 0)
|
|
74
|
+
return;
|
|
75
|
+
setFocusId(rows[(cursor - 1 + rows.length) % rows.length].id);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key.downArrow) {
|
|
79
|
+
if (rows.length === 0)
|
|
80
|
+
return;
|
|
81
|
+
setFocusId(rows[(cursor + 1) % rows.length].id);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (key.tab) {
|
|
85
|
+
// ONE OVERLAY STACK, NOT TWO (AC-23): the picker hands the manager an intent and
|
|
86
|
+
// closes, and an empty library lands straight in the editor because there is
|
|
87
|
+
// nothing for a list page to show.
|
|
88
|
+
props.onManage(empty ? { kind: 'create' } : { kind: 'list' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (key.backspace || key.delete) {
|
|
92
|
+
setQuery((value) => [...value].slice(0, -1).join(''));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key.ctrl || key.meta)
|
|
96
|
+
return;
|
|
97
|
+
const clean = stripPasteFrames(stripEnterFrames(input));
|
|
98
|
+
if (clean.length === 0)
|
|
99
|
+
return;
|
|
100
|
+
if (query.length === 0) {
|
|
101
|
+
// `1`-`9` PICK A ROW (AC-21). Only while the query is empty: otherwise the digit
|
|
102
|
+
// is the first character of what the user is searching for.
|
|
103
|
+
if (input === 'a' && empty) {
|
|
104
|
+
props.onManage({ kind: 'create' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const target = pickerDigitTarget(rows, input);
|
|
108
|
+
if (target !== null) {
|
|
109
|
+
fill(target, false);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
setQuery((value) => (value + clean).slice(0, PROMPT_PICKER_LIMITS.queryMaxChars));
|
|
114
|
+
setFocusId(null);
|
|
115
|
+
});
|
|
116
|
+
const body = [
|
|
117
|
+
_jsx(Text, { wrap: "truncate", color: theme.primary, children: `${glyphs.caret} ${query.length > 0 ? query : '(type to filter)'}` }, "query"),
|
|
118
|
+
];
|
|
119
|
+
for (const [at, row] of rows.entries()) {
|
|
120
|
+
const isCursor = at === cursor;
|
|
121
|
+
const marks = `${row.favorite ? ' [fav]' : ''}${row.tags.length > 0 ? ` #${row.tags[0]}` : ''}`;
|
|
122
|
+
body.push(_jsx(Text, { wrap: "truncate", color: isCursor ? theme.accent : theme.muted, children: `${isCursor ? glyphs.caret : ' '} ${row.index}. `
|
|
123
|
+
+ clipProfileText(`${row.title}${marks}`, cols - 12) }, row.id));
|
|
124
|
+
}
|
|
125
|
+
for (const [at, line] of pickerDetail(focused, library).entries()) {
|
|
126
|
+
body.push(_jsx(Text, { wrap: "truncate", color: theme.muted, children: clipProfileText(` ${line}`, cols - 8) }, `detail-${at}`));
|
|
127
|
+
}
|
|
128
|
+
return (_jsx(OverlayFrame, { title: `Pick a prompt (${rows.length}/${library.prompts.length})`, hint: pickerHint({ count: rows.length, query, view }), rows: body, scrollOffset: offset, onScrollClamp: props.onScrollClamp, maxRows: props.maxRows, cols: cols, theme: theme, caps: caps }));
|
|
129
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one editable multi-line field in this feature (quick-prompt-library D-11).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
5
|
+
*
|
|
6
|
+
* RENDER AND CARET COME FROM THE SAME FUNCTION. `layoutComposer` wraps the buffer and
|
|
7
|
+
* reports the caret's row and column from ONE `buildEditorVisualMap` pass, and the
|
|
8
|
+
* drawing below splits that same row at that same column. Every second authority
|
|
9
|
+
* here - a prose wrapper, a second width table, a "count the characters instead" -
|
|
10
|
+
* puts the caret on a cell that does not hold the character it points at, and the
|
|
11
|
+
* only symptom is that the user's edits land one column off in Chinese text.
|
|
12
|
+
*
|
|
13
|
+
* WHY `layout/wrap-rows.ts::wrapToRows` IS BANNED HERE (it is used one directory
|
|
14
|
+
* away, for the list's read-only preview): it splits on whitespace and rejoins with
|
|
15
|
+
* single spaces, so it eats indentation and destroys the character-index-to-cell
|
|
16
|
+
* mapping. `composer-rows.ts:16-20` says so in as many words, and that is the module
|
|
17
|
+
* this one borrows from.
|
|
18
|
+
*
|
|
19
|
+
* NO PASTE-COLLAPSE TOKENS, deliberately. The token machinery exists so a 218-line
|
|
20
|
+
* paste does not have to live in a one-line input; this IS a multi-line field, and a
|
|
21
|
+
* token inside a saved prompt body would be data corruption the user could not undo.
|
|
22
|
+
*/
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import type { Theme } from '../theme.js';
|
|
25
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
26
|
+
export interface PromptTextAreaState {
|
|
27
|
+
buffer: string;
|
|
28
|
+
cursor: number;
|
|
29
|
+
/** Kept across vertical moves, cleared by any horizontal one (composer parity). */
|
|
30
|
+
preferredVisualColumn?: number;
|
|
31
|
+
}
|
|
32
|
+
export declare const EMPTY_TEXT_AREA: PromptTextAreaState;
|
|
33
|
+
export type PromptTextAreaAction = {
|
|
34
|
+
type: 'insert';
|
|
35
|
+
text: string;
|
|
36
|
+
} | {
|
|
37
|
+
type: 'newline';
|
|
38
|
+
} | {
|
|
39
|
+
type: 'backspace';
|
|
40
|
+
} | {
|
|
41
|
+
type: 'delete';
|
|
42
|
+
} | {
|
|
43
|
+
type: 'left';
|
|
44
|
+
} | {
|
|
45
|
+
type: 'right';
|
|
46
|
+
} | {
|
|
47
|
+
type: 'home';
|
|
48
|
+
} | {
|
|
49
|
+
type: 'end';
|
|
50
|
+
}
|
|
51
|
+
/** Vertical moves need the wrap width; it is part of the ACTION, not of the state. */
|
|
52
|
+
| {
|
|
53
|
+
type: 'up';
|
|
54
|
+
cols: number;
|
|
55
|
+
} | {
|
|
56
|
+
type: 'down';
|
|
57
|
+
cols: number;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'replace';
|
|
60
|
+
buffer: string;
|
|
61
|
+
cursor: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Pure. No React, no Ink, no filesystem - so the twelve editing cases are a table
|
|
65
|
+
* test rather than a rendered frame assertion.
|
|
66
|
+
*/
|
|
67
|
+
export declare function promptTextAreaReducer(state: PromptTextAreaState, action: PromptTextAreaAction): PromptTextAreaState;
|
|
68
|
+
export interface PromptTextAreaProps {
|
|
69
|
+
state: PromptTextAreaState;
|
|
70
|
+
/** Wrap width, in cells, already net of the field's own chrome. */
|
|
71
|
+
cols: number;
|
|
72
|
+
/** Rows the field may occupy. `1` is the single-line case for title/description. */
|
|
73
|
+
rows: number;
|
|
74
|
+
focused: boolean;
|
|
75
|
+
theme: Theme;
|
|
76
|
+
caps: TermCapabilities;
|
|
77
|
+
reducedMotion?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One field's worth of rows.
|
|
81
|
+
*
|
|
82
|
+
* The overflow chip is a plain `+N` on the LAST row rather than a row of its own:
|
|
83
|
+
* an extra row would make this component draw one more line than the caller budgeted
|
|
84
|
+
* for, and the frame it lives in has no way to find out.
|
|
85
|
+
*/
|
|
86
|
+
export declare function PromptTextArea(props: PromptTextAreaProps): React.ReactElement;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The one editable multi-line field in this feature (quick-prompt-library D-11).
|
|
4
|
+
*
|
|
5
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
|
|
6
|
+
*
|
|
7
|
+
* RENDER AND CARET COME FROM THE SAME FUNCTION. `layoutComposer` wraps the buffer and
|
|
8
|
+
* reports the caret's row and column from ONE `buildEditorVisualMap` pass, and the
|
|
9
|
+
* drawing below splits that same row at that same column. Every second authority
|
|
10
|
+
* here - a prose wrapper, a second width table, a "count the characters instead" -
|
|
11
|
+
* puts the caret on a cell that does not hold the character it points at, and the
|
|
12
|
+
* only symptom is that the user's edits land one column off in Chinese text.
|
|
13
|
+
*
|
|
14
|
+
* WHY `layout/wrap-rows.ts::wrapToRows` IS BANNED HERE (it is used one directory
|
|
15
|
+
* away, for the list's read-only preview): it splits on whitespace and rejoins with
|
|
16
|
+
* single spaces, so it eats indentation and destroys the character-index-to-cell
|
|
17
|
+
* mapping. `composer-rows.ts:16-20` says so in as many words, and that is the module
|
|
18
|
+
* this one borrows from.
|
|
19
|
+
*
|
|
20
|
+
* NO PASTE-COLLAPSE TOKENS, deliberately. The token machinery exists so a 218-line
|
|
21
|
+
* paste does not have to live in a one-line input; this IS a multi-line field, and a
|
|
22
|
+
* token inside a saved prompt body would be data corruption the user could not undo.
|
|
23
|
+
*/
|
|
24
|
+
import React from 'react';
|
|
25
|
+
import { Box, Text } from 'ink';
|
|
26
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
27
|
+
import { layoutComposer, splitRowAtColumn } from '../composer-rows.js';
|
|
28
|
+
import { moveVisualCursor, snapGrapheme, stepGrapheme } from '../editor-navigation.js';
|
|
29
|
+
import { PromptCaret } from '../PromptCaret.js';
|
|
30
|
+
export const EMPTY_TEXT_AREA = { buffer: '', cursor: 0 };
|
|
31
|
+
function withCursor(state, buffer, cursor) {
|
|
32
|
+
return { buffer, cursor: snapGrapheme(buffer, cursor, true) };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pure. No React, no Ink, no filesystem - so the twelve editing cases are a table
|
|
36
|
+
* test rather than a rendered frame assertion.
|
|
37
|
+
*/
|
|
38
|
+
export function promptTextAreaReducer(state, action) {
|
|
39
|
+
switch (action.type) {
|
|
40
|
+
case 'insert': {
|
|
41
|
+
const cursor = snapGrapheme(state.buffer, state.cursor);
|
|
42
|
+
const buffer = state.buffer.slice(0, cursor) + action.text + state.buffer.slice(cursor);
|
|
43
|
+
return {
|
|
44
|
+
buffer,
|
|
45
|
+
cursor: snapGrapheme(buffer, cursor + action.text.length, true),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
case 'newline': {
|
|
49
|
+
const cursor = snapGrapheme(state.buffer, state.cursor);
|
|
50
|
+
const buffer = `${state.buffer.slice(0, cursor)}\n${state.buffer.slice(cursor)}`;
|
|
51
|
+
return { buffer, cursor: cursor + 1 };
|
|
52
|
+
}
|
|
53
|
+
case 'backspace':
|
|
54
|
+
case 'delete': {
|
|
55
|
+
const cursor = snapGrapheme(state.buffer, state.cursor);
|
|
56
|
+
if (action.type === 'backspace') {
|
|
57
|
+
if (cursor <= 0)
|
|
58
|
+
return state;
|
|
59
|
+
const from = stepGrapheme(state.buffer, cursor, 'left');
|
|
60
|
+
return withCursor(state, state.buffer.slice(0, from) + state.buffer.slice(cursor), from);
|
|
61
|
+
}
|
|
62
|
+
if (cursor >= state.buffer.length)
|
|
63
|
+
return state;
|
|
64
|
+
const to = stepGrapheme(state.buffer, cursor, 'right');
|
|
65
|
+
return withCursor(state, state.buffer.slice(0, cursor) + state.buffer.slice(to), cursor);
|
|
66
|
+
}
|
|
67
|
+
case 'left':
|
|
68
|
+
return {
|
|
69
|
+
buffer: state.buffer,
|
|
70
|
+
cursor: stepGrapheme(state.buffer, state.cursor, 'left'),
|
|
71
|
+
};
|
|
72
|
+
case 'right':
|
|
73
|
+
return {
|
|
74
|
+
buffer: state.buffer,
|
|
75
|
+
cursor: stepGrapheme(state.buffer, state.cursor, 'right'),
|
|
76
|
+
};
|
|
77
|
+
case 'home':
|
|
78
|
+
return { buffer: state.buffer, cursor: 0 };
|
|
79
|
+
case 'end':
|
|
80
|
+
return { buffer: state.buffer, cursor: snapGrapheme(state.buffer, state.buffer.length) };
|
|
81
|
+
case 'up':
|
|
82
|
+
case 'down': {
|
|
83
|
+
const moved = moveVisualCursor({
|
|
84
|
+
buffer: state.buffer,
|
|
85
|
+
cursor: state.cursor,
|
|
86
|
+
cols: action.cols,
|
|
87
|
+
direction: action.type,
|
|
88
|
+
...(state.preferredVisualColumn === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { preferredVisualColumn: state.preferredVisualColumn }),
|
|
91
|
+
});
|
|
92
|
+
// A vertical move at the first or last visual row stays put rather than
|
|
93
|
+
// jumping to the buffer's edge: the composer behaves the same way, and a jump
|
|
94
|
+
// here would silently relocate the caret in a field the user is editing.
|
|
95
|
+
return moved === null
|
|
96
|
+
? state
|
|
97
|
+
: {
|
|
98
|
+
buffer: state.buffer,
|
|
99
|
+
cursor: moved.cursor,
|
|
100
|
+
preferredVisualColumn: moved.preferredVisualColumn,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
case 'replace':
|
|
104
|
+
return withCursor(state, action.buffer, action.cursor);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function renderSegments(segments, keyPrefix) {
|
|
108
|
+
return segments.map((segment, at) => (_jsx(Text, { children: segment.text }, `${keyPrefix}${at}`)));
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* One field's worth of rows.
|
|
112
|
+
*
|
|
113
|
+
* The overflow chip is a plain `+N` on the LAST row rather than a row of its own:
|
|
114
|
+
* an extra row would make this component draw one more line than the caller budgeted
|
|
115
|
+
* for, and the frame it lives in has no way to find out.
|
|
116
|
+
*/
|
|
117
|
+
export function PromptTextArea(props) {
|
|
118
|
+
const { state, cols, rows, focused } = props;
|
|
119
|
+
const glyphs = pickGlyphs(props.caps);
|
|
120
|
+
const layout = layoutComposer({
|
|
121
|
+
buffer: state.buffer,
|
|
122
|
+
cursor: state.cursor,
|
|
123
|
+
cols: Math.max(4, cols),
|
|
124
|
+
maxRows: Math.max(1, rows),
|
|
125
|
+
active: focused,
|
|
126
|
+
});
|
|
127
|
+
const hidden = layout.hiddenAbove + layout.hiddenBelow;
|
|
128
|
+
const resetKey = React.useMemo(() => ({}), [state.buffer, state.cursor, focused, props.cols, rows]);
|
|
129
|
+
if (state.buffer.length === 0) {
|
|
130
|
+
return (_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: [_jsx(PromptCaret, { text: " ", active: focused, reducedMotion: props.reducedMotion ?? false, colorLevel: props.caps.colorLevel, color: props.theme.muted, resetKey: resetKey }), glyphs.ellipsis] }));
|
|
131
|
+
}
|
|
132
|
+
return (_jsx(Box, { flexDirection: "column", children: layout.rows.map((row, at) => {
|
|
133
|
+
if (at !== layout.cursorRow) {
|
|
134
|
+
return _jsx(Text, { wrap: "truncate", children: renderSegments(row.segments, `r${at}.`) }, at);
|
|
135
|
+
}
|
|
136
|
+
const split = splitRowAtColumn(row, layout.cursorCol);
|
|
137
|
+
return (_jsxs(Text, { wrap: "truncate", children: [renderSegments(split.before, `b${at}.`), _jsx(PromptCaret, { text: split.at ? split.at.text : ' ', active: focused, reducedMotion: props.reducedMotion ?? false, colorLevel: props.caps.colorLevel, resetKey: resetKey }), renderSegments(split.after, `a${at}.`), at === layout.rows.length - 1 && hidden > 0
|
|
138
|
+
? _jsx(Text, { color: props.theme.muted, children: ` +${hidden}` })
|
|
139
|
+
: null] }, at));
|
|
140
|
+
}) }));
|
|
141
|
+
}
|