@aragon-agent/cli 0.6.35 → 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 +3173 -3071
- package/dist/agent/agent-mode.d.ts +20 -4
- package/dist/agent/agent-mode.js +20 -4
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +64 -4
- package/dist/agent/headless.js +31 -0
- 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 +131 -4
- 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/load.d.ts +10 -0
- package/dist/config/load.js +8 -1
- package/dist/config/schema.d.ts +68 -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/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 +310 -46
- package/dist/ui/Composer.d.ts +6 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +9 -1
- package/dist/ui/Header.js +2 -2
- package/dist/ui/PromptInput.d.ts +21 -1
- package/dist/ui/PromptInput.js +21 -1
- package/dist/ui/ScrollbackShell.d.ts +19 -19
- package/dist/ui/ScrollbackShell.js +8 -1
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/TeamPanel.d.ts +26 -44
- package/dist/ui/TeamPanel.js +46 -135
- package/dist/ui/TodoPanel.d.ts +17 -14
- package/dist/ui/TodoPanel.js +146 -38
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/composer-input.d.ts +10 -0
- package/dist/ui/composer-input.js +57 -24
- 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 +13 -3
- package/dist/ui/layout/header-hints.js +48 -14
- 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 +23 -3
- package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
- package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
- package/dist/ui/overlays/HelpOverlay.js +17 -2
- 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/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/band.js +14 -1
- package/dist/ui/team-status.d.ts +25 -0
- package/dist/ui/team-status.js +31 -3
- 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 +9 -1
- package/package.json +2 -2
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prompt manager's state machine and view models (quick-prompt-library §4.4).
|
|
3
|
+
*
|
|
4
|
+
* ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`. Every sentence the
|
|
5
|
+
* user reads comes from `prompts/copy.ts`; nothing in this file may hold a literal
|
|
6
|
+
* non-ASCII byte.
|
|
7
|
+
*
|
|
8
|
+
* WHY A PURE REDUCER AND NOT `useState` INSIDE THE COMPONENT: the page stack has two
|
|
9
|
+
* transitions that are easy to get wrong and impossible to see on screen - "the
|
|
10
|
+
* second Esc discards, the first does not" and "the intent is consumed exactly
|
|
11
|
+
* once" - and both are assertions, not screenshots. Everything below can be driven
|
|
12
|
+
* from a test with no Ink mounted.
|
|
13
|
+
*/
|
|
14
|
+
import type { PromptDraftInput, PromptDraftVerdict, PromptEditorDraft, PromptLibrary, PromptLibraryEntry, PromptWriteError, QuickPrompt } from '../../prompts/types.js';
|
|
15
|
+
export type PromptLibraryPage = 'list' | 'editor' | 'delete';
|
|
16
|
+
/** The six rows `Tab` cycles on the editor page, in visual order. */
|
|
17
|
+
export declare const PROMPT_EDITOR_FIELDS: readonly ["title", "content", "description", "tags", "save", "cancel"];
|
|
18
|
+
export type PromptEditorField = (typeof PROMPT_EDITOR_FIELDS)[number];
|
|
19
|
+
export interface PromptLibraryState {
|
|
20
|
+
page: PromptLibraryPage;
|
|
21
|
+
/** Focus index on the list page (clamped by the view). */
|
|
22
|
+
index: number;
|
|
23
|
+
/**
|
|
24
|
+
* Id the focus should land on when the list is next drawn.
|
|
25
|
+
*
|
|
26
|
+
* BY ID, NEVER BY POSITION: a create appends and an edit does not move the entry,
|
|
27
|
+
* but a delete shifts every row after it, and "focus row 3" would then point at a
|
|
28
|
+
* different prompt than the one the user was working on.
|
|
29
|
+
*/
|
|
30
|
+
focusId: string | null;
|
|
31
|
+
draft: PromptEditorDraft;
|
|
32
|
+
/** The entry the draft was seeded from, so `dirty` is a comparison, not a guess. */
|
|
33
|
+
baseline: PromptEditorDraft | null;
|
|
34
|
+
/** The first Esc on a dirty editor arms the discard; any edit disarms it. */
|
|
35
|
+
escArmed: boolean;
|
|
36
|
+
pendingDeleteId: string | null;
|
|
37
|
+
error: PromptWriteError | null;
|
|
38
|
+
/** The row that should own focus after a refusal (D-12). */
|
|
39
|
+
errorField: PromptEditorField | null;
|
|
40
|
+
/** Highest intent nonce this overlay has acted on (I-8). */
|
|
41
|
+
consumedIntentNonce: number;
|
|
42
|
+
}
|
|
43
|
+
export type PromptLibraryAction = {
|
|
44
|
+
type: 'init';
|
|
45
|
+
entry: PromptLibraryEntry;
|
|
46
|
+
library: PromptLibrary;
|
|
47
|
+
nonce: number;
|
|
48
|
+
} | {
|
|
49
|
+
type: 'list';
|
|
50
|
+
focusId?: string;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'focus';
|
|
53
|
+
index: number;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'create';
|
|
56
|
+
seed?: {
|
|
57
|
+
title?: string;
|
|
58
|
+
content?: string;
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
type: 'edit';
|
|
62
|
+
id: string;
|
|
63
|
+
library: PromptLibrary;
|
|
64
|
+
} | {
|
|
65
|
+
type: 'delete';
|
|
66
|
+
id: string;
|
|
67
|
+
} | {
|
|
68
|
+
type: 'draft';
|
|
69
|
+
patch: Partial<PromptEditorDraft>;
|
|
70
|
+
} | {
|
|
71
|
+
type: 'commit';
|
|
72
|
+
id: string | null;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'failed';
|
|
75
|
+
code: PromptWriteError;
|
|
76
|
+
field?: PromptEditorField;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'esc';
|
|
79
|
+
} | {
|
|
80
|
+
type: 'cancelled';
|
|
81
|
+
};
|
|
82
|
+
/** A new, empty draft. `id: null` is what makes `Save` a create rather than an update. */
|
|
83
|
+
export declare const EMPTY_PROMPT_DRAFT: PromptEditorDraft;
|
|
84
|
+
export declare const INITIAL_PROMPT_LIBRARY_STATE: PromptLibraryState;
|
|
85
|
+
/** The editable projection of a stored entry. */
|
|
86
|
+
export declare function draftFromEntry(entry: QuickPrompt): PromptEditorDraft;
|
|
87
|
+
/** A create draft seeded from a command (`/prompts add title :: body`). */
|
|
88
|
+
export declare function seededDraft(seed?: {
|
|
89
|
+
title?: string;
|
|
90
|
+
content?: string;
|
|
91
|
+
}): PromptEditorDraft;
|
|
92
|
+
/** Structural equality is enough: every field is a string or a boolean. */
|
|
93
|
+
export declare function draftIsDirty(draft: PromptEditorDraft, baseline: PromptEditorDraft | null): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Validate a draft and turn it into what the store accepts (D-12).
|
|
96
|
+
*
|
|
97
|
+
* THE VALIDATOR IS `store.ts`'S, not a second copy here: the sentence the editor
|
|
98
|
+
* shows and the rule the write applies have to be the same rule, or a user can be
|
|
99
|
+
* told a title is fine by the form and refused by the store.
|
|
100
|
+
*/
|
|
101
|
+
export declare function finishPromptEditor(draft: PromptEditorDraft): PromptDraftVerdict;
|
|
102
|
+
/** Index of the first focusable row, for a refusal that named a field. */
|
|
103
|
+
export declare function fieldIndex(field: PromptEditorField | null): number;
|
|
104
|
+
/**
|
|
105
|
+
* The reducer.
|
|
106
|
+
*
|
|
107
|
+
* `init` is the ONLY action that can jump straight to the editor or the delete page:
|
|
108
|
+
* that is what makes `/prompts edit 2` land where it should (D-21). The intent's
|
|
109
|
+
* nonce is stored as `consumedIntentNonce` by the component before dispatching, so a
|
|
110
|
+
* replayed intent is a no-op at the component, not a second edit here.
|
|
111
|
+
*/
|
|
112
|
+
export declare function promptLibraryReducer(state: PromptLibraryState, action: PromptLibraryAction, library: PromptLibrary): PromptLibraryState;
|
|
113
|
+
export interface PromptLibraryRow {
|
|
114
|
+
id: string;
|
|
115
|
+
/** One-line render text: the title plus its markers. The caret is the component's. */
|
|
116
|
+
label: string;
|
|
117
|
+
markers: {
|
|
118
|
+
favorite: boolean;
|
|
119
|
+
hasTags: boolean;
|
|
120
|
+
uses: number;
|
|
121
|
+
};
|
|
122
|
+
/** Body length in code points; the detail block and the delete page share it. */
|
|
123
|
+
chars: number;
|
|
124
|
+
}
|
|
125
|
+
/** `1.2k` for anything a fixed-width column cannot show in full. */
|
|
126
|
+
export declare function formatCount(count: number): string;
|
|
127
|
+
/** `3d ago` / `2h ago` / `just now`. Deliberately coarse: it is a hint, not a clock. */
|
|
128
|
+
export declare function formatAgo(timestamp: number | undefined, now: number): string;
|
|
129
|
+
/**
|
|
130
|
+
* The list, one row per entry, in the library's own order.
|
|
131
|
+
*
|
|
132
|
+
* DUPLICATE TITLES GET AN ID PREFIX (I-2), exactly as `kit-picker-state.ts` does for
|
|
133
|
+
* duplicate kit names: two rows reading the same words make `Enter` a coin flip.
|
|
134
|
+
*/
|
|
135
|
+
export declare function libraryRows(library: PromptLibrary): PromptLibraryRow[];
|
|
136
|
+
/**
|
|
137
|
+
* The detail block under the list: at most `detailMaxLines` rows, fixed shape.
|
|
138
|
+
*
|
|
139
|
+
* FIXED, because `OverlayFrame`'s window is shared with the list (D-16): a body that
|
|
140
|
+
* participated in row height would squeeze six prompts into two visible rows. The
|
|
141
|
+
* full text has exactly one home, and it is the editor.
|
|
142
|
+
*/
|
|
143
|
+
export declare function libraryDetail(row: PromptLibraryRow | undefined, library: PromptLibrary, cols: number, now?: number): string[];
|
|
144
|
+
/**
|
|
145
|
+
* The list page's footer.
|
|
146
|
+
*
|
|
147
|
+
* THE LAST CLAUSE IS ALWAYS THE WAY OUT (§9.5): a narrow terminal clips from the
|
|
148
|
+
* left, so the reader always keeps `Esc close`. The reorder keys are named here
|
|
149
|
+
* rather than in the row above it because they are the one affordance a user cannot
|
|
150
|
+
* discover by pressing a key they already know.
|
|
151
|
+
*/
|
|
152
|
+
export declare function libraryHint(state: PromptLibraryState, count: number): string;
|
|
153
|
+
/** The delete page's four lines: what it is, how big, and what it costs. */
|
|
154
|
+
export declare function deleteDetail(library: PromptLibrary, id: string | null): string[];
|
|
155
|
+
/**
|
|
156
|
+
* The draft as the store accepts it.
|
|
157
|
+
*
|
|
158
|
+
* THE TAGS FIELD IS PARSED HERE, ONCE, and nowhere else: the editor edits a raw
|
|
159
|
+
* string, the store validates a `string[]`, and a second parser between them is how
|
|
160
|
+
* `a,b` and `a b` come to mean different things depending on which surface saved it.
|
|
161
|
+
*/
|
|
162
|
+
export declare function draftToInput(draft: PromptEditorDraft): PromptDraftInput;
|