@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.
Files changed (146) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3173 -3071
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +38 -3
  6. package/dist/agent/controller.js +64 -4
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/reducer.d.ts +13 -2
  9. package/dist/agent/reducer.js +1 -0
  10. package/dist/cli.js +12 -0
  11. package/dist/commands/agents-command.d.ts +15 -0
  12. package/dist/commands/agents-command.js +27 -0
  13. package/dist/commands/builtins.js +131 -4
  14. package/dist/commands/perf.d.ts +11 -0
  15. package/dist/commands/perf.js +2 -1
  16. package/dist/commands/registry.d.ts +14 -0
  17. package/dist/config/app-paths.d.ts +15 -0
  18. package/dist/config/app-paths.js +33 -2
  19. package/dist/config/env.d.ts +4 -1
  20. package/dist/config/env.js +28 -2
  21. package/dist/config/load.d.ts +10 -0
  22. package/dist/config/load.js +8 -1
  23. package/dist/config/schema.d.ts +68 -0
  24. package/dist/config/schema.js +39 -0
  25. package/dist/config/store.js +12 -1
  26. package/dist/config/ui-state.d.ts +18 -0
  27. package/dist/config/ui-state.js +9 -0
  28. package/dist/exec/events.d.ts +25 -0
  29. package/dist/exec/events.js +4 -0
  30. package/dist/exec/index.js +16 -1
  31. package/dist/exec/runner.js +10 -0
  32. package/dist/proc/limits.d.ts +16 -0
  33. package/dist/proc/limits.js +16 -0
  34. package/dist/proc/supervisor.d.ts +20 -1
  35. package/dist/proc/supervisor.js +42 -14
  36. package/dist/prompts/command.d.ts +34 -0
  37. package/dist/prompts/command.js +237 -0
  38. package/dist/prompts/copy.d.ts +89 -0
  39. package/dist/prompts/copy.js +131 -0
  40. package/dist/prompts/fill.d.ts +51 -0
  41. package/dist/prompts/fill.js +46 -0
  42. package/dist/prompts/limits.d.ts +53 -0
  43. package/dist/prompts/limits.js +53 -0
  44. package/dist/prompts/normalize.d.ts +53 -0
  45. package/dist/prompts/normalize.js +190 -0
  46. package/dist/prompts/resolve.d.ts +31 -0
  47. package/dist/prompts/resolve.js +60 -0
  48. package/dist/prompts/search.d.ts +58 -0
  49. package/dist/prompts/search.js +146 -0
  50. package/dist/prompts/store.d.ts +86 -0
  51. package/dist/prompts/store.js +453 -0
  52. package/dist/prompts/types.d.ts +204 -0
  53. package/dist/prompts/types.js +13 -0
  54. package/dist/session/checkpoint.d.ts +8 -1
  55. package/dist/session/checkpoint.js +6 -0
  56. package/dist/session/persist.d.ts +19 -1
  57. package/dist/session/persist.js +13 -1
  58. package/dist/session/startup-session.d.ts +7 -2
  59. package/dist/session/startup-session.js +5 -2
  60. package/dist/session/store.js +3 -0
  61. package/dist/session/validate-session.js +64 -1
  62. package/dist/team/limits.d.ts +25 -0
  63. package/dist/team/limits.js +26 -0
  64. package/dist/team/runtime.js +27 -2
  65. package/dist/team/settled.d.ts +57 -0
  66. package/dist/team/settled.js +87 -0
  67. package/dist/team/subagent.js +48 -2
  68. package/dist/team/timeline.d.ts +74 -0
  69. package/dist/team/timeline.js +101 -0
  70. package/dist/team/types.d.ts +24 -1
  71. package/dist/todo/acceptance.d.ts +120 -0
  72. package/dist/todo/acceptance.js +399 -0
  73. package/dist/todo/follow-through.d.ts +48 -23
  74. package/dist/todo/follow-through.js +110 -44
  75. package/dist/todo/limits.d.ts +62 -1
  76. package/dist/todo/limits.js +62 -1
  77. package/dist/todo/panel-rows.d.ts +29 -39
  78. package/dist/todo/panel-rows.js +37 -83
  79. package/dist/todo/plan-sections.d.ts +60 -0
  80. package/dist/todo/plan-sections.js +108 -0
  81. package/dist/todo/prompt.d.ts +17 -12
  82. package/dist/todo/prompt.js +59 -40
  83. package/dist/todo/row-window.d.ts +51 -0
  84. package/dist/todo/row-window.js +107 -0
  85. package/dist/todo/store.d.ts +139 -31
  86. package/dist/todo/store.js +271 -73
  87. package/dist/todo/todo-tool.d.ts +33 -8
  88. package/dist/todo/todo-tool.js +186 -30
  89. package/dist/todo/types.d.ts +58 -0
  90. package/dist/ui/App.js +310 -46
  91. package/dist/ui/Composer.d.ts +6 -2
  92. package/dist/ui/Composer.js +2 -2
  93. package/dist/ui/Header.d.ts +9 -1
  94. package/dist/ui/Header.js +2 -2
  95. package/dist/ui/PromptInput.d.ts +21 -1
  96. package/dist/ui/PromptInput.js +21 -1
  97. package/dist/ui/ScrollbackShell.d.ts +19 -19
  98. package/dist/ui/ScrollbackShell.js +8 -1
  99. package/dist/ui/StatusBar.d.ts +5 -0
  100. package/dist/ui/StatusBar.js +1 -0
  101. package/dist/ui/TeamPanel.d.ts +26 -44
  102. package/dist/ui/TeamPanel.js +46 -135
  103. package/dist/ui/TodoPanel.d.ts +17 -14
  104. package/dist/ui/TodoPanel.js +146 -38
  105. package/dist/ui/Transcript.js +1 -1
  106. package/dist/ui/composer-input.d.ts +10 -0
  107. package/dist/ui/composer-input.js +57 -24
  108. package/dist/ui/entries/TodoCard.d.ts +26 -11
  109. package/dist/ui/entries/TodoCard.js +56 -15
  110. package/dist/ui/glyphs.d.ts +19 -0
  111. package/dist/ui/glyphs.js +4 -0
  112. package/dist/ui/layout/agents-board.d.ts +102 -0
  113. package/dist/ui/layout/agents-board.js +389 -0
  114. package/dist/ui/layout/cells.d.ts +61 -0
  115. package/dist/ui/layout/cells.js +156 -0
  116. package/dist/ui/layout/header-hints.d.ts +13 -3
  117. package/dist/ui/layout/header-hints.js +48 -14
  118. package/dist/ui/layout/status-detail-layout.js +9 -0
  119. package/dist/ui/layout/status-layout.d.ts +13 -0
  120. package/dist/ui/layout/team-row.d.ts +139 -0
  121. package/dist/ui/layout/team-row.js +246 -0
  122. package/dist/ui/layout/todo-layout.d.ts +38 -0
  123. package/dist/ui/layout/todo-layout.js +75 -0
  124. package/dist/ui/layout/virtual-window.js +23 -3
  125. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  126. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  127. package/dist/ui/overlays/HelpOverlay.js +17 -2
  128. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  129. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  130. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  131. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  132. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  133. package/dist/ui/overlays/PromptPicker.js +129 -0
  134. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  135. package/dist/ui/overlays/PromptTextArea.js +141 -0
  136. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  137. package/dist/ui/overlays/prompt-library-state.js +335 -0
  138. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  139. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  140. package/dist/ui/scrollback/band.js +14 -1
  141. package/dist/ui/team-status.d.ts +25 -0
  142. package/dist/ui/team-status.js +31 -3
  143. package/dist/ui/use-startup-notices.d.ts +35 -1
  144. package/dist/ui/use-startup-notices.js +29 -2
  145. package/dist/ui/use-wheel-routing.js +9 -1
  146. 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;