@aragon-agent/cli 0.6.2 → 0.6.4
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 +321 -0
- package/README.md +297 -2
- package/dist/agent/controller.d.ts +193 -1
- package/dist/agent/controller.js +475 -2
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +47 -0
- package/dist/agent/reducer.d.ts +189 -0
- package/dist/agent/reducer.js +301 -2
- package/dist/agent/system-prompt.d.ts +14 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +297 -16
- package/dist/commands/builtins.js +165 -24
- package/dist/commands/registry.d.ts +27 -0
- package/dist/compaction/archive.d.ts +175 -0
- package/dist/compaction/archive.js +267 -0
- package/dist/compaction/child.d.ts +97 -0
- package/dist/compaction/child.js +118 -0
- package/dist/compaction/command.d.ts +41 -0
- package/dist/compaction/command.js +355 -0
- package/dist/compaction/compactor.d.ts +302 -0
- package/dist/compaction/compactor.js +810 -0
- package/dist/compaction/digest.d.ts +62 -0
- package/dist/compaction/digest.js +164 -0
- package/dist/compaction/limits.d.ts +165 -0
- package/dist/compaction/limits.js +172 -0
- package/dist/compaction/pressure.d.ts +135 -0
- package/dist/compaction/pressure.js +167 -0
- package/dist/compaction/summarize-call.d.ts +70 -0
- package/dist/compaction/summarize-call.js +86 -0
- package/dist/compaction/summary-prompt.d.ts +85 -0
- package/dist/compaction/summary-prompt.js +197 -0
- package/dist/compaction/tail-budget.d.ts +52 -0
- package/dist/compaction/tail-budget.js +67 -0
- package/dist/compaction/types.d.ts +215 -0
- package/dist/compaction/types.js +22 -0
- package/dist/compaction/wiring.d.ts +264 -0
- package/dist/compaction/wiring.js +577 -0
- package/dist/config/app-paths.d.ts +14 -0
- package/dist/config/app-paths.js +16 -0
- package/dist/config/cli-commands.d.ts +41 -0
- package/dist/config/cli-commands.js +77 -1
- package/dist/config/env.d.ts +5 -2
- package/dist/config/env.js +140 -3
- package/dist/config/load.d.ts +42 -0
- package/dist/config/load.js +101 -1
- package/dist/config/schema.d.ts +244 -0
- package/dist/config/schema.js +170 -0
- package/dist/config/store.js +37 -1
- package/dist/config/ui-state.d.ts +28 -1
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +50 -1
- package/dist/exec/runner.js +42 -0
- package/dist/input/limits.d.ts +72 -0
- package/dist/input/limits.js +63 -0
- package/dist/input/mouse-events.d.ts +31 -5
- package/dist/input/mouse-events.js +86 -27
- package/dist/input/paste-parse.d.ts +71 -0
- package/dist/input/paste-parse.js +95 -0
- package/dist/input/stdin-filter.d.ts +81 -0
- package/dist/input/stdin-filter.js +379 -0
- package/dist/logging/install.d.ts +53 -1
- package/dist/logging/install.js +81 -1
- package/dist/logging/logger.d.ts +1 -1
- package/dist/proc/classify.d.ts +52 -0
- package/dist/proc/classify.js +143 -0
- package/dist/proc/kill-tree.d.ts +42 -0
- package/dist/proc/kill-tree.js +119 -0
- package/dist/proc/limits.d.ts +84 -0
- package/dist/proc/limits.js +84 -0
- package/dist/proc/log-ring.d.ts +68 -0
- package/dist/proc/log-ring.js +102 -0
- package/dist/proc/prompt.d.ts +24 -0
- package/dist/proc/prompt.js +49 -0
- package/dist/proc/readiness.d.ts +102 -0
- package/dist/proc/readiness.js +219 -0
- package/dist/proc/supervisor.d.ts +103 -0
- package/dist/proc/supervisor.js +465 -0
- package/dist/proc/types.d.ts +175 -0
- package/dist/proc/types.js +15 -0
- package/dist/session/persist.js +55 -1
- package/dist/team/report.js +7 -1
- package/dist/team/runtime.d.ts +10 -0
- package/dist/team/runtime.js +7 -0
- package/dist/team/subagent.d.ts +31 -1
- package/dist/team/subagent.js +42 -15
- package/dist/team/types.d.ts +8 -0
- package/dist/tools/bash-tool.d.ts +51 -2
- package/dist/tools/bash-tool.js +321 -51
- package/dist/tools/fs-tools.d.ts +17 -0
- package/dist/tools/index.d.ts +55 -2
- package/dist/tools/index.js +39 -1
- package/dist/tools/proc-tools.d.ts +25 -0
- package/dist/tools/proc-tools.js +102 -0
- package/dist/ui/ActivityLine.d.ts +19 -1
- package/dist/ui/ActivityLine.js +10 -2
- package/dist/ui/App.d.ts +62 -2
- package/dist/ui/App.js +546 -16
- package/dist/ui/Composer.d.ts +66 -1
- package/dist/ui/Composer.js +50 -6
- package/dist/ui/PromptInput.d.ts +82 -3
- package/dist/ui/PromptInput.js +232 -49
- package/dist/ui/StatusBar.d.ts +59 -0
- package/dist/ui/StatusBar.js +16 -4
- package/dist/ui/TeamPanel.js +7 -1
- package/dist/ui/Transcript.d.ts +21 -1
- package/dist/ui/Transcript.js +61 -3
- package/dist/ui/clipboard.d.ts +52 -0
- package/dist/ui/clipboard.js +91 -0
- package/dist/ui/composer-limits.d.ts +31 -0
- package/dist/ui/composer-limits.js +50 -0
- package/dist/ui/composer-rows.d.ts +73 -0
- package/dist/ui/composer-rows.js +201 -0
- package/dist/ui/editor-reducer.d.ts +37 -0
- package/dist/ui/editor-reducer.js +88 -12
- package/dist/ui/entries/CompactionCard.d.ts +75 -0
- package/dist/ui/entries/CompactionCard.js +158 -0
- package/dist/ui/entries/ServiceCard.d.ts +66 -0
- package/dist/ui/entries/ServiceCard.js +116 -0
- package/dist/ui/entries/UserEntry.d.ts +17 -3
- package/dist/ui/entries/UserEntry.js +34 -5
- package/dist/ui/frame-differ.d.ts +46 -0
- package/dist/ui/frame-differ.js +94 -10
- package/dist/ui/gauge.d.ts +16 -1
- package/dist/ui/gauge.js +25 -6
- package/dist/ui/glyphs.d.ts +11 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/ScrollViewport.d.ts +43 -6
- package/dist/ui/layout/ScrollViewport.js +128 -21
- package/dist/ui/layout/budget.d.ts +44 -9
- package/dist/ui/layout/budget.js +42 -10
- package/dist/ui/layout/follow-state.d.ts +137 -0
- package/dist/ui/layout/follow-state.js +150 -0
- package/dist/ui/layout/virtual-window.d.ts +11 -0
- package/dist/ui/layout/virtual-window.js +85 -1
- package/dist/ui/overlays/HelpOverlay.js +9 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
- package/dist/ui/overlays/QuestionOverlay.js +5 -1
- package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
- package/dist/ui/overlays/SettingsScreen.js +74 -2
- package/dist/ui/palettes.d.ts +16 -0
- package/dist/ui/palettes.js +6 -0
- package/dist/ui/paste-frames.d.ts +87 -0
- package/dist/ui/paste-frames.js +112 -0
- package/dist/ui/paste-tokens.d.ts +90 -0
- package/dist/ui/paste-tokens.js +147 -0
- package/dist/ui/screen.d.ts +38 -0
- package/dist/ui/screen.js +113 -8
- package/dist/ui/selection/highlight.d.ts +39 -0
- package/dist/ui/selection/highlight.js +64 -0
- package/dist/ui/selection/screen-mirror.d.ts +24 -0
- package/dist/ui/selection/screen-mirror.js +35 -0
- package/dist/ui/selection/selection-controller.d.ts +88 -0
- package/dist/ui/selection/selection-controller.js +282 -0
- package/dist/ui/selection/selection.d.ts +90 -0
- package/dist/ui/selection/selection.js +223 -0
- package/dist/ui/stdout-frame-writer.d.ts +33 -0
- package/dist/ui/stdout-frame-writer.js +15 -0
- package/dist/ui/theme.d.ts +11 -0
- package/dist/ui/theme.js +2 -0
- package/dist/ui/transcript-text.js +72 -0
- package/dist/ui/use-startup-notices.d.ts +27 -1
- package/dist/ui/use-startup-notices.js +43 -11
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- package/dist/ui/use-wheel-routing.js +8 -0
- package/package.json +4 -2
- package/dist/input/stdin-mouse-filter.d.ts +0 -42
- package/dist/input/stdin-mouse-filter.js +0 -153
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import React from 'react';
|
|
18
18
|
import type { Theme } from './theme.js';
|
|
19
19
|
import type { TermCapabilities } from './capabilities.js';
|
|
20
|
+
import { pickGlyphs } from './glyphs.js';
|
|
20
21
|
import { type CommandOption } from './PromptInput.js';
|
|
21
22
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
22
23
|
export interface ComposerProps {
|
|
@@ -48,9 +49,73 @@ export interface ComposerProps {
|
|
|
48
49
|
*/
|
|
49
50
|
modeToggleKey?: string;
|
|
50
51
|
popupMaxRows?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Rows of newer output below the viewport, forwarded to the input row's chip
|
|
54
|
+
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
55
|
+
*
|
|
56
|
+
* IT IS STATE, NOT A FADEABLE HINT, and it therefore does NOT pass through the
|
|
57
|
+
* `showHint` / `hintsEnabled` gate below. Those two govern the teaching row —
|
|
58
|
+
* which retires keys the user has demonstrably learned and is dropped outright
|
|
59
|
+
* on a short terminal — and "you are 12 rows behind the newest output" is
|
|
60
|
+
* neither learnable nor optional. Same argument that keeps `esc abort`
|
|
61
|
+
* un-faded in `hintText`.
|
|
62
|
+
*/
|
|
63
|
+
scrolledLines?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Live background services (background-service-supervision §3.8), 0 when none.
|
|
66
|
+
*
|
|
67
|
+
* IT IS STATE, NOT A FADEABLE HINT, exactly like `scrolledLines` above: the
|
|
68
|
+
* clause it adds is the only visible way to stop something, and `hintText`
|
|
69
|
+
* below keeps it out of the faded form for the same reason `esc abort` is kept
|
|
70
|
+
* there.
|
|
71
|
+
*/
|
|
72
|
+
services?: number;
|
|
73
|
+
/**
|
|
74
|
+
* The composer's DRAFT row count, forwarded upward (tui-paste-handling
|
|
75
|
+
* section 5.5).
|
|
76
|
+
*
|
|
77
|
+
* `hasDraft` stays here, because the border reacts to it and routing that
|
|
78
|
+
* through `App` would re-render the transcript on the first keystroke of every
|
|
79
|
+
* message. The row count is the opposite case: `layout/budget.ts` needs it, so
|
|
80
|
+
* it has to reach `App` — and it changes on a line break rather than on a
|
|
81
|
+
* character, which is what keeps the re-render rate the same (R-6).
|
|
82
|
+
*/
|
|
83
|
+
onDraftRows?: (rows: number) => void;
|
|
84
|
+
/** Paste refusals from `PromptInput`, routed to `App`'s notice dispatch. */
|
|
85
|
+
onNotice?: (level: 'warn' | 'error', text: string) => void;
|
|
51
86
|
theme: Theme;
|
|
52
87
|
caps: TermCapabilities;
|
|
53
88
|
onSubmit: (text: string) => void;
|
|
54
89
|
onHelp?: () => void;
|
|
55
90
|
}
|
|
56
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Build the hint row.
|
|
93
|
+
*
|
|
94
|
+
* The `running` branch is NEVER abbreviated, no matter how experienced the user
|
|
95
|
+
* is. Once the status bar's hint cluster was removed, the composer became the
|
|
96
|
+
* only place that names the abort key — and fading that out after eight
|
|
97
|
+
* submissions would leave a long-running task with no visible way to stop it.
|
|
98
|
+
* Progressive disclosure is for teaching discoverable things, not for retiring
|
|
99
|
+
* an emergency exit (§4.6 / A-14 / M-10b).
|
|
100
|
+
*/
|
|
101
|
+
declare function hintText(opts: {
|
|
102
|
+
running: boolean;
|
|
103
|
+
submitCount: number;
|
|
104
|
+
glyphs: ReturnType<typeof pickGlyphs>;
|
|
105
|
+
mode: AgentMode;
|
|
106
|
+
toggleKey: string;
|
|
107
|
+
/** Live background services; 0 leaves both rows byte-identical to today's. */
|
|
108
|
+
services: number;
|
|
109
|
+
}): string;
|
|
110
|
+
/**
|
|
111
|
+
* `hintText`, for the interrupt-ladder tests (AC-43).
|
|
112
|
+
*
|
|
113
|
+
* EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
|
|
114
|
+
* the private helper stays private in the module graph and nothing in `src/`
|
|
115
|
+
* grows a second caller by accident. The row is the ONLY visible way to stop a
|
|
116
|
+
* process the agent left running, so it is worth asserting on directly rather
|
|
117
|
+
* than fishing the string out of a rendered frame.
|
|
118
|
+
*/
|
|
119
|
+
export declare const hintTextForTest: typeof hintText;
|
|
120
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
121
|
+
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -32,9 +32,28 @@ import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
|
32
32
|
* Progressive disclosure is for teaching discoverable things, not for retiring
|
|
33
33
|
* an emergency exit (§4.6 / A-14 / M-10b).
|
|
34
34
|
*/
|
|
35
|
-
function hintText(
|
|
35
|
+
function hintText(opts) {
|
|
36
|
+
const { running, submitCount, glyphs, mode, toggleKey, services } = opts;
|
|
36
37
|
const dot = ` ${glyphs.midDot} `;
|
|
38
|
+
// THE EXIT CLAUSE SURVIVES IN BOTH ROWS (P1-4). The obvious edit here is to
|
|
39
|
+
// replace `ctrl+c*2 exit` with `ctrl+c stop N` — and that would delete the
|
|
40
|
+
// only visible way to quit at exactly the moment Ctrl+C stops meaning "quit",
|
|
41
|
+
// on the screen a user reaches when something has already gone wrong. Both
|
|
42
|
+
// clauses are shown; the row is `wrap="truncate"` and they are ordered
|
|
43
|
+
// most-urgent-first, so a terminal too narrow for all of them drops the exit
|
|
44
|
+
// clause last rather than the abort key.
|
|
37
45
|
if (running) {
|
|
46
|
+
if (services > 0) {
|
|
47
|
+
return [
|
|
48
|
+
`${glyphs.enterKey} steer`,
|
|
49
|
+
// Rung two is named because the whole point of the ladder is that the
|
|
50
|
+
// first press can fail; a user who does not know there is a second press
|
|
51
|
+
// is left exactly where the reported screenshot left them.
|
|
52
|
+
`esc abort${glyphs.times}2 force`,
|
|
53
|
+
`ctrl+c stop ${services}`,
|
|
54
|
+
`ctrl+c${glyphs.times}2 exit`,
|
|
55
|
+
].join(dot);
|
|
56
|
+
}
|
|
38
57
|
return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
|
|
39
58
|
}
|
|
40
59
|
// The hint names the DESTINATION, not the current state — the correct label
|
|
@@ -45,9 +64,14 @@ function hintText(running, submitCount, glyphs, mode, toggleKey) {
|
|
|
45
64
|
// retire things the user has demonstrably learned, and `submitCount` counts
|
|
46
65
|
// submissions — a user can send fifty messages without ever discovering
|
|
47
66
|
// Shift+Tab. Same argument that keeps `esc abort` un-faded above.
|
|
67
|
+
// NEVER FADED, for the reason the props doc gives: it is the only visible way
|
|
68
|
+
// to stop a process the agent left running, and progressive disclosure retires
|
|
69
|
+
// things the user has demonstrably learned — not emergency exits.
|
|
70
|
+
const stop = services > 0 ? [`ctrl+c stop ${services}`] : [];
|
|
48
71
|
if (submitCount >= HINT_FADE_AFTER)
|
|
49
|
-
return [toggle, '? help'].join(dot);
|
|
72
|
+
return [...stop, toggle, '? help'].join(dot);
|
|
50
73
|
return [
|
|
74
|
+
...stop,
|
|
51
75
|
`${glyphs.enterKey} send`,
|
|
52
76
|
`${glyphs.shiftEnter} newline`,
|
|
53
77
|
'/ commands',
|
|
@@ -56,9 +80,22 @@ function hintText(running, submitCount, glyphs, mode, toggleKey) {
|
|
|
56
80
|
'? help',
|
|
57
81
|
].join(dot);
|
|
58
82
|
}
|
|
59
|
-
|
|
83
|
+
/**
|
|
84
|
+
* `hintText`, for the interrupt-ladder tests (AC-43).
|
|
85
|
+
*
|
|
86
|
+
* EXPORTED UNDER A TEST-FACING NAME rather than exporting `hintText` itself, so
|
|
87
|
+
* the private helper stays private in the module graph and nothing in `src/`
|
|
88
|
+
* grows a second caller by accident. The row is the ONLY visible way to stop a
|
|
89
|
+
* process the agent left running, so it is worth asserting on directly rather
|
|
90
|
+
* than fishing the string out of a rendered frame.
|
|
91
|
+
*/
|
|
92
|
+
export const hintTextForTest = hintText;
|
|
93
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
60
94
|
const [hasDraft, setHasDraft] = useState(false);
|
|
61
|
-
const onDraftChange = useCallback((next) =>
|
|
95
|
+
const onDraftChange = useCallback((next) => {
|
|
96
|
+
setHasDraft(next.hasDraft);
|
|
97
|
+
onDraftRows?.(next.rows);
|
|
98
|
+
}, [onDraftRows]);
|
|
62
99
|
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
63
100
|
// ordering is load-bearing: an in-flight run's border must stay `toolRunning`,
|
|
64
101
|
// because "is it running" is more urgent than "which mode".
|
|
@@ -73,8 +110,15 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
73
110
|
: theme.idleBorder ?? theme.border;
|
|
74
111
|
const glyphs = pickGlyphs(caps);
|
|
75
112
|
const visible = showHint && hintsEnabled;
|
|
76
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
113
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
77
114
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
115
|
// costs zero rows on a 24-row terminal.
|
|
79
|
-
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(
|
|
116
|
+
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
|
|
117
|
+
running,
|
|
118
|
+
submitCount,
|
|
119
|
+
glyphs,
|
|
120
|
+
mode: agentMode,
|
|
121
|
+
toggleKey: modeToggleKey,
|
|
122
|
+
services,
|
|
123
|
+
}) })] }))] }));
|
|
80
124
|
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
import React from 'react';
|
|
22
22
|
import type { Theme } from './theme.js';
|
|
23
23
|
import type { TermCapabilities } from './capabilities.js';
|
|
24
|
+
import { pickGlyphs } from './glyphs.js';
|
|
24
25
|
import { type EditIntent } from '../input/keymap.js';
|
|
25
26
|
import { type Suggestion } from './AutocompletePopup.js';
|
|
27
|
+
import { type EditorState, type InputSegment } from './editor-reducer.js';
|
|
26
28
|
import type { AgentMode } from '../agent/agent-mode.js';
|
|
27
29
|
export interface CommandOption {
|
|
28
30
|
name: string;
|
|
@@ -63,9 +65,86 @@ interface PromptInputProps {
|
|
|
63
65
|
bordered?: {
|
|
64
66
|
color?: string;
|
|
65
67
|
};
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Fires when the draft flips empty ⇄ non-empty (for the border color) OR when
|
|
70
|
+
* its ROW COUNT changes (for `layout/budget.ts`) — never per keystroke.
|
|
71
|
+
*
|
|
72
|
+
* Widened from `(hasDraft: boolean)` by tui-paste-handling section 5.5. The row
|
|
73
|
+
* count is what makes I-8 true: the composer's rendered height and the
|
|
74
|
+
* `draftRows` handed to `viewportRows` have to be the same number, and the only
|
|
75
|
+
* component that knows the first one is this one. It is reported ALREADY
|
|
76
|
+
* CLAMPED, so a 218-line draft and a 400-line draft report the same value and
|
|
77
|
+
* the transcript stops moving once the composer is full (R-12).
|
|
78
|
+
*/
|
|
79
|
+
onDraftChange?: (state: {
|
|
80
|
+
hasDraft: boolean;
|
|
81
|
+
rows: number;
|
|
82
|
+
}) => void;
|
|
83
|
+
/**
|
|
84
|
+
* Tell the user a paste was refused (D-11 / P1-4).
|
|
85
|
+
*
|
|
86
|
+
* `PASTE_DRAFT_MAX_BYTES` and `PASTE_MAX_BLOCKS` cannot be enforced in the
|
|
87
|
+
* filter (it does not know the draft) and cannot be enforced in the reducer (it
|
|
88
|
+
* is pure, so it can neither raise a toast nor say why it refused). This
|
|
89
|
+
* component is the only place that holds both facts.
|
|
90
|
+
*/
|
|
91
|
+
onNotice?: (level: 'warn' | 'error', text: string) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Rows of newer output hidden BELOW the viewport — `ScrollViewport`'s offset
|
|
94
|
+
* (tui-selection-and-scroll-follow §4.2 / G7).
|
|
95
|
+
*
|
|
96
|
+
* Rendered as a right-aligned chip ON THE INPUT ROW, INSIDE THE BORDER, which
|
|
97
|
+
* is what the requirement asks for (输入框内部) and what makes the hint cost
|
|
98
|
+
* zero rows. It used to take the viewport's own last row, which spent a line of
|
|
99
|
+
* the user's content exactly when the transcript was longest AND fed the number
|
|
100
|
+
* back into its own derivation (§3.3).
|
|
101
|
+
*
|
|
102
|
+
* IT IS STATE, NOT A HINT. It deliberately ignores `showHint` / `hintsEnabled`
|
|
103
|
+
* — those govern the teaching row BELOW the box, which fades with experience
|
|
104
|
+
* and is dropped on a short terminal. "You are 12 rows behind the newest
|
|
105
|
+
* output" must survive both (T-23).
|
|
106
|
+
*
|
|
107
|
+
* Only meaningful in full-screen mode, so it renders only when `bordered` is
|
|
108
|
+
* set: inline mode has no self-drawn viewport and this is always 0 there.
|
|
109
|
+
*/
|
|
110
|
+
scrolledLines?: number;
|
|
68
111
|
}
|
|
112
|
+
export interface ScrollChip {
|
|
113
|
+
text: string;
|
|
114
|
+
/** FIXED width, so the number gaining a digit cannot re-wrap the draft (P2-4). */
|
|
115
|
+
cells: number;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Build the chip, or `null` when the viewport is pinned to the newest output.
|
|
119
|
+
*
|
|
120
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
121
|
+
* console this renders `v12` rather than mojibake, and `glyphs.test.ts`'s static
|
|
122
|
+
* scan fails the build if that rule is broken here.
|
|
123
|
+
*/
|
|
124
|
+
export declare function scrollChip(scrolledLines: number, cols: number, glyphs: ReturnType<typeof pickGlyphs>): ScrollChip | null;
|
|
125
|
+
export interface OverflowChip {
|
|
126
|
+
text: string;
|
|
127
|
+
/** FIXED width, so a number gaining a digit cannot re-wrap the draft. */
|
|
128
|
+
cells: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the "there is more draft above / below this window" indicator, or `null`
|
|
132
|
+
* when the whole draft is on screen.
|
|
133
|
+
*
|
|
134
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
135
|
+
* console this renders `^3 v7` rather than mojibake, and `glyphs.test.ts`'s
|
|
136
|
+
* static scan fails the build if that rule is broken here.
|
|
137
|
+
*/
|
|
138
|
+
export declare function overflowChip(hiddenAbove: number, hiddenBelow: number, glyphs: ReturnType<typeof pickGlyphs>): OverflowChip | null;
|
|
139
|
+
/**
|
|
140
|
+
* Would these segments push the draft past a limit? Returns the refusal text, or
|
|
141
|
+
* `null` when the paste is allowed.
|
|
142
|
+
*
|
|
143
|
+
* Checked BEFORE `dispatch`, so a refusal dispatches nothing at all and the draft
|
|
144
|
+
* is left byte-for-byte unchanged (AC-9). The one-dispatch-per-key rule is
|
|
145
|
+
* unaffected for the same reason.
|
|
146
|
+
*/
|
|
147
|
+
export declare function draftLimitRefusal(segments: InputSegment[], editor: Pick<EditorState, 'pastes'>): string | null;
|
|
69
148
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
70
149
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
71
150
|
buffer: string;
|
|
@@ -94,5 +173,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
94
173
|
end: number;
|
|
95
174
|
query: string;
|
|
96
175
|
} | null;
|
|
97
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
176
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
98
177
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* PromptInput (spec §3.6 / §3.10) — a controlled multiline input on Ink's
|
|
4
4
|
* `useInput` with: full line editing (Home/End, word jump/delete, kill-line via
|
|
@@ -20,12 +20,110 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
|
20
20
|
* rather than from a key.
|
|
21
21
|
*/
|
|
22
22
|
import { useEffect, useReducer, useRef, useState } from 'react';
|
|
23
|
-
import { Box, Text, useInput } from 'ink';
|
|
23
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
24
24
|
import { glob as tinyGlob } from 'tinyglobby';
|
|
25
25
|
import { pickGlyphs } from './glyphs.js';
|
|
26
26
|
import { recognize } from '../input/keymap.js';
|
|
27
27
|
import { AutocompletePopup } from './AutocompletePopup.js';
|
|
28
|
-
import { editorReducer, INITIAL_EDITOR_STATE } from './editor-reducer.js';
|
|
28
|
+
import { editorReducer, INITIAL_EDITOR_STATE, } from './editor-reducer.js';
|
|
29
|
+
import { hasPasteFrame, splitPasteFrames } from './paste-frames.js';
|
|
30
|
+
import { allocatePasteId, expandPastes, expandRangeOverTokens } from './paste-tokens.js';
|
|
31
|
+
import { draftMaxRows } from './composer-limits.js';
|
|
32
|
+
import { layoutComposer, splitRowAtColumn } from './composer-rows.js';
|
|
33
|
+
import { PASTE_DRAFT_MAX_BYTES, PASTE_MAX_BLOCKS, formatPasteSize, } from '../input/limits.js';
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// The scroll chip (§4.2)
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/** Below this the full sentence does not fit beside a draft; the chip goes terse. */
|
|
38
|
+
const CHIP_FULL_MIN_COLS = 64;
|
|
39
|
+
/**
|
|
40
|
+
* Ceiling on the number the chip prints, so `chipCells` is a CONSTANT per branch
|
|
41
|
+
* rather than a function of how far behind the user is (P2-4). Past it the chip
|
|
42
|
+
* reads `999+`.
|
|
43
|
+
*/
|
|
44
|
+
const CHIP_MAX_N = 999;
|
|
45
|
+
/** `↓ 999+ new lines · PgDn` — the widest form the full branch can produce. */
|
|
46
|
+
const CHIP_FULL_CELLS = 24;
|
|
47
|
+
/** `↓999+` — the widest form the terse branch can produce. */
|
|
48
|
+
const CHIP_TERSE_CELLS = 6;
|
|
49
|
+
/**
|
|
50
|
+
* Build the chip, or `null` when the viewport is pinned to the newest output.
|
|
51
|
+
*
|
|
52
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
53
|
+
* console this renders `v12` rather than mojibake, and `glyphs.test.ts`'s static
|
|
54
|
+
* scan fails the build if that rule is broken here.
|
|
55
|
+
*/
|
|
56
|
+
export function scrollChip(scrolledLines, cols, glyphs) {
|
|
57
|
+
if (!Number.isFinite(scrolledLines) || scrolledLines <= 0)
|
|
58
|
+
return null;
|
|
59
|
+
const n = Math.floor(scrolledLines);
|
|
60
|
+
const shown = n > CHIP_MAX_N ? `${CHIP_MAX_N}+` : String(n);
|
|
61
|
+
if (cols < CHIP_FULL_MIN_COLS) {
|
|
62
|
+
return { text: `${glyphs.arrowDown}${shown}`, cells: CHIP_TERSE_CELLS };
|
|
63
|
+
}
|
|
64
|
+
const noun = n === 1 ? 'line' : 'lines';
|
|
65
|
+
return {
|
|
66
|
+
text: `${glyphs.arrowDown} ${shown} new ${noun} ${glyphs.midDot} PgDn`,
|
|
67
|
+
cells: CHIP_FULL_CELLS,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// The draft overflow indicator (section 5.5)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/** `^3 v7` at its widest, so the cell is a CONSTANT (the P2-4 discipline). */
|
|
74
|
+
const OVERFLOW_CELLS = 12;
|
|
75
|
+
const OVERFLOW_MAX_N = 999;
|
|
76
|
+
/**
|
|
77
|
+
* Build the "there is more draft above / below this window" indicator, or `null`
|
|
78
|
+
* when the whole draft is on screen.
|
|
79
|
+
*
|
|
80
|
+
* Every glyph comes from `pickGlyphs` and none from a literal (I-5): on a legacy
|
|
81
|
+
* console this renders `^3 v7` rather than mojibake, and `glyphs.test.ts`'s
|
|
82
|
+
* static scan fails the build if that rule is broken here.
|
|
83
|
+
*/
|
|
84
|
+
export function overflowChip(hiddenAbove, hiddenBelow, glyphs) {
|
|
85
|
+
if (hiddenAbove <= 0 && hiddenBelow <= 0)
|
|
86
|
+
return null;
|
|
87
|
+
const show = (n) => (n > OVERFLOW_MAX_N ? `${OVERFLOW_MAX_N}+` : String(n));
|
|
88
|
+
const parts = [];
|
|
89
|
+
if (hiddenAbove > 0)
|
|
90
|
+
parts.push(`${glyphs.arrowUp}${show(hiddenAbove)}`);
|
|
91
|
+
if (hiddenBelow > 0)
|
|
92
|
+
parts.push(`${glyphs.arrowDown}${show(hiddenBelow)}`);
|
|
93
|
+
return { text: parts.join(' '), cells: OVERFLOW_CELLS };
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Draft-level paste limits (section 5.3, the P1-4 enforcement point)
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
/**
|
|
99
|
+
* Would these segments push the draft past a limit? Returns the refusal text, or
|
|
100
|
+
* `null` when the paste is allowed.
|
|
101
|
+
*
|
|
102
|
+
* Checked BEFORE `dispatch`, so a refusal dispatches nothing at all and the draft
|
|
103
|
+
* is left byte-for-byte unchanged (AC-9). The one-dispatch-per-key rule is
|
|
104
|
+
* unaffected for the same reason.
|
|
105
|
+
*/
|
|
106
|
+
export function draftLimitRefusal(segments, editor) {
|
|
107
|
+
let liveBytes = 0;
|
|
108
|
+
for (const record of editor.pastes.values())
|
|
109
|
+
liveBytes += Buffer.byteLength(record.text, 'utf8');
|
|
110
|
+
let blocks = editor.pastes.size;
|
|
111
|
+
for (const segment of segments) {
|
|
112
|
+
if (segment.kind !== 'paste')
|
|
113
|
+
continue;
|
|
114
|
+
liveBytes += Buffer.byteLength(segment.text, 'utf8');
|
|
115
|
+
blocks += 1;
|
|
116
|
+
}
|
|
117
|
+
if (blocks > PASTE_MAX_BLOCKS) {
|
|
118
|
+
return (`Too many pasted blocks in one message (limit is ${PASTE_MAX_BLOCKS}). ` +
|
|
119
|
+
'Nothing was inserted.');
|
|
120
|
+
}
|
|
121
|
+
if (liveBytes > PASTE_DRAFT_MAX_BYTES) {
|
|
122
|
+
return (`Pasted content in this message would reach ${formatPasteSize(liveBytes)}; ` +
|
|
123
|
+
`limit is ${formatPasteSize(PASTE_DRAFT_MAX_BYTES)}. Nothing was inserted.`);
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
29
127
|
// ---------------------------------------------------------------------------
|
|
30
128
|
// Pure editing helpers (unit-tested in input.test.ts)
|
|
31
129
|
// ---------------------------------------------------------------------------
|
|
@@ -54,9 +152,25 @@ function nextWord(buffer, cursor) {
|
|
|
54
152
|
i += 1;
|
|
55
153
|
return i;
|
|
56
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Delete `[from, to)`, first growing the range over any token it clips (G5 /
|
|
157
|
+
* D-8).
|
|
158
|
+
*
|
|
159
|
+
* A no-op growth on a buffer with no tokens, so every pre-existing assertion in
|
|
160
|
+
* `input.test.ts` holds byte-for-byte. With a token present it is what stops
|
|
161
|
+
* `Ctrl+W` / `Ctrl+U` / `Ctrl+K` leaving half a label behind — which would be a
|
|
162
|
+
* label the user then sends INSTEAD of the text it stands for.
|
|
163
|
+
*/
|
|
164
|
+
function killRange(buffer, from, to) {
|
|
165
|
+
const range = expandRangeOverTokens(buffer, from, to);
|
|
166
|
+
return { buffer: buffer.slice(0, range.from) + buffer.slice(range.to), cursor: range.from };
|
|
167
|
+
}
|
|
57
168
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
58
169
|
export function applyEdit(buffer, cursor, intent) {
|
|
59
170
|
switch (intent) {
|
|
171
|
+
// Cursor MOVEMENT is deliberately not token-aware (D-8): arrows, Home/End
|
|
172
|
+
// and word jumps step by character, because re-deriving `moveVertical` around
|
|
173
|
+
// a synthetic unit buys a correctness the user cannot notice.
|
|
60
174
|
case 'home':
|
|
61
175
|
return { buffer, cursor: lineStart(buffer, cursor) };
|
|
62
176
|
case 'end':
|
|
@@ -65,18 +179,12 @@ export function applyEdit(buffer, cursor, intent) {
|
|
|
65
179
|
return { buffer, cursor: prevWord(buffer, cursor) };
|
|
66
180
|
case 'wordRight':
|
|
67
181
|
return { buffer, cursor: nextWord(buffer, cursor) };
|
|
68
|
-
case 'deleteWordBack':
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
case '
|
|
73
|
-
|
|
74
|
-
return { buffer: buffer.slice(0, start) + buffer.slice(cursor), cursor: start };
|
|
75
|
-
}
|
|
76
|
-
case 'killToEnd': {
|
|
77
|
-
const end = lineEnd(buffer, cursor);
|
|
78
|
-
return { buffer: buffer.slice(0, cursor) + buffer.slice(end), cursor };
|
|
79
|
-
}
|
|
182
|
+
case 'deleteWordBack':
|
|
183
|
+
return killRange(buffer, prevWord(buffer, cursor), cursor);
|
|
184
|
+
case 'killToStart':
|
|
185
|
+
return killRange(buffer, lineStart(buffer, cursor), cursor);
|
|
186
|
+
case 'killToEnd':
|
|
187
|
+
return killRange(buffer, cursor, lineEnd(buffer, cursor));
|
|
80
188
|
}
|
|
81
189
|
}
|
|
82
190
|
/**
|
|
@@ -138,7 +246,8 @@ function isControlSeq(input) {
|
|
|
138
246
|
// ---------------------------------------------------------------------------
|
|
139
247
|
// Component
|
|
140
248
|
// ---------------------------------------------------------------------------
|
|
141
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
|
|
249
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
250
|
+
const { stdout } = useStdout();
|
|
142
251
|
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
143
252
|
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
144
253
|
const [fileMatches, setFileMatches] = useState([]);
|
|
@@ -184,18 +293,62 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
184
293
|
clearTimeout(id);
|
|
185
294
|
};
|
|
186
295
|
}, [fileQuery, cwd]);
|
|
187
|
-
// ---
|
|
188
|
-
//
|
|
296
|
+
// --- Layout (section 5.5). -----------------------------------------------
|
|
297
|
+
//
|
|
298
|
+
// COMPUTED HERE, ABOVE THE REPORTING EFFECT, because the number the effect
|
|
299
|
+
// sends upward has to be the number this component actually renders (I-8).
|
|
300
|
+
// Deriving it twice — once for the frame and once for the budget — is exactly
|
|
301
|
+
// the trap `BottomStatusRow.tsx:5-27` documents.
|
|
302
|
+
const glyphs = pickGlyphs(caps);
|
|
303
|
+
const cols = stdout?.columns ?? 80;
|
|
304
|
+
const terminalRows = stdout?.rows ?? 24;
|
|
305
|
+
// Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
|
|
306
|
+
// is always 0 there and the branch is unreachable rather than merely unused.
|
|
307
|
+
// The guard is explicit so it stays that way.
|
|
308
|
+
const chip = bordered ? scrollChip(scrolledLines, cols, glyphs) : null;
|
|
309
|
+
// Err NARROW. Wrapping wider than the box Ink gives us would make Ink wrap a
|
|
310
|
+
// second time, and then `layout.rows.length` is a lie — which is the one thing
|
|
311
|
+
// this module may not be.
|
|
312
|
+
const chromeCols = bordered ? 2 /* border */ + 2 /* paddingX */ + 2 /* marker */ : 2;
|
|
313
|
+
const baseCols = Math.max(8, cols - chromeCols - (chip ? chip.cells + 1 : 0));
|
|
314
|
+
const measure = (wrapCols) => layoutComposer({
|
|
315
|
+
buffer,
|
|
316
|
+
cursor,
|
|
317
|
+
cols: wrapCols,
|
|
318
|
+
maxRows: draftMaxRows(terminalRows),
|
|
319
|
+
active: isActive,
|
|
320
|
+
});
|
|
321
|
+
// TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
|
|
322
|
+
// rides the last rendered row INSIDE the box the draft wraps in, so its cells
|
|
323
|
+
// have to come out of the wrap width. Measured at the full width instead, a
|
|
324
|
+
// last row that fills the line is wrapped a SECOND time by Ink, and the
|
|
325
|
+
// composer then draws one row more than `layout.rows.length` — which is the
|
|
326
|
+
// number this component hands to `viewportRows`. That is defect C again, in
|
|
327
|
+
// precisely the situation this feature exists for: a draft too tall to show.
|
|
328
|
+
//
|
|
329
|
+
// It settles in two. Narrowing can only ADD rows, so a draft that overflowed
|
|
330
|
+
// the first pass still overflows the second, and a third pass would measure
|
|
331
|
+
// the same width as the second.
|
|
332
|
+
const wide = measure(baseCols);
|
|
333
|
+
const overflows = wide.hiddenAbove > 0 || wide.hiddenBelow > 0;
|
|
334
|
+
const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
|
|
335
|
+
const layout = overflows ? measure(usableCols) : wide;
|
|
336
|
+
const draftRowCount = Math.max(1, layout.rows.length);
|
|
337
|
+
// --- Draft presence and height, reported only on a transition. -----------
|
|
338
|
+
// Notifying on every keystroke would re-render the whole App tree per key. The
|
|
339
|
+
// row count changes on a line break or a wrap boundary, not per character
|
|
340
|
+
// (R-6), so widening this callback does not widen how often it fires.
|
|
189
341
|
const hasDraft = buffer.length > 0;
|
|
190
342
|
const lastReportedDraft = useRef(null);
|
|
191
343
|
useEffect(() => {
|
|
192
344
|
if (!onDraftChange)
|
|
193
345
|
return;
|
|
194
|
-
|
|
346
|
+
const last = lastReportedDraft.current;
|
|
347
|
+
if (last && last.hasDraft === hasDraft && last.rows === draftRowCount)
|
|
195
348
|
return;
|
|
196
|
-
lastReportedDraft.current = hasDraft;
|
|
197
|
-
onDraftChange(hasDraft);
|
|
198
|
-
}, [hasDraft, onDraftChange]);
|
|
349
|
+
lastReportedDraft.current = { hasDraft, rows: draftRowCount };
|
|
350
|
+
onDraftChange({ hasDraft, rows: draftRowCount });
|
|
351
|
+
}, [hasDraft, draftRowCount, onDraftChange]);
|
|
199
352
|
// --- Mutators. -----------------------------------------------------------
|
|
200
353
|
// ONE DISPATCH EACH, and that is the rule §3.4 asks a reviewer to check line
|
|
201
354
|
// by line. Two dispatches from one key would put the legacy root back to two
|
|
@@ -203,8 +356,16 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
203
356
|
const insert = (text) => {
|
|
204
357
|
dispatch({ type: 'insert', text });
|
|
205
358
|
};
|
|
359
|
+
/**
|
|
360
|
+
* Expand every token back to its payload, then send (D-7 / D-9).
|
|
361
|
+
*
|
|
362
|
+
* `onSubmit`'s signature is unchanged, so `Composer`, `App.handleSubmit`,
|
|
363
|
+
* `runSlashInput` and `submitMessage` are all untouched — and a slash command
|
|
364
|
+
* therefore sees the EXPANDED text, which is what `/skill foo <pasted body>`
|
|
365
|
+
* needs. `clear` releases every payload in the same dispatch.
|
|
366
|
+
*/
|
|
206
367
|
const submit = (text) => {
|
|
207
|
-
onSubmit(text);
|
|
368
|
+
onSubmit(expandPastes(text, editor.pastes));
|
|
208
369
|
dispatch({ type: 'clear' });
|
|
209
370
|
};
|
|
210
371
|
const recallUp = () => {
|
|
@@ -345,11 +506,32 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
345
506
|
onHelp();
|
|
346
507
|
return;
|
|
347
508
|
}
|
|
509
|
+
// A PASTE (section 5.4). It sits AFTER every key branch so a paste can
|
|
510
|
+
// never be shadowed by a key test, and BEFORE `isControlSeq` because a
|
|
511
|
+
// framed string starts with NUL and would otherwise be dropped.
|
|
512
|
+
//
|
|
513
|
+
// The text runs inside `segments` are already sanitised (I-14): Ink drains
|
|
514
|
+
// its whole buffer in one `read()`, so this same string can carry the `\r`
|
|
515
|
+
// of a user who pasted and pressed Enter inside the burst window.
|
|
516
|
+
if (hasPasteFrame(input)) {
|
|
517
|
+
const runs = splitPasteFrames(input);
|
|
518
|
+
const segments = runs.map((run) => run.kind === 'paste' ? { kind: 'paste', text: run.text, id: allocatePasteId() } : run);
|
|
519
|
+
if (segments.length === 0)
|
|
520
|
+
return;
|
|
521
|
+
const refusal = draftLimitRefusal(segments, editor);
|
|
522
|
+
if (refusal) {
|
|
523
|
+
// A refusal dispatches NOTHING, so the draft is byte-for-byte
|
|
524
|
+
// unchanged (AC-9) — and it is never silent (D-11).
|
|
525
|
+
onNotice?.('warn', refusal);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
dispatch({ type: 'input', segments });
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
348
531
|
// Printable input — drop any unrecognized control/escape byte.
|
|
349
532
|
if (input && !key.tab && !isControlSeq(input))
|
|
350
533
|
insert(input);
|
|
351
534
|
}, { isActive });
|
|
352
|
-
const glyphs = pickGlyphs(caps);
|
|
353
535
|
const marker = running ? glyphs.steer : glyphs.caret;
|
|
354
536
|
const markerColor = running ? theme.toolRunning : theme.primary;
|
|
355
537
|
const placeholder = running
|
|
@@ -357,30 +539,31 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
357
539
|
: agentMode === 'plan'
|
|
358
540
|
? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
|
|
359
541
|
: `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
|
|
360
|
-
const
|
|
361
|
-
const
|
|
542
|
+
const overflow = overflowChip(layout.hiddenAbove, layout.hiddenBelow, glyphs);
|
|
543
|
+
const tokenColor = theme.hintFg ?? theme.muted;
|
|
544
|
+
const renderSegments = (segments, keyPrefix) => segments.map((segment, i) => (_jsx(Text, { color: segment.kind === 'token' ? tokenColor : undefined, children: segment.text }, `${keyPrefix}${i}`)));
|
|
545
|
+
const rowNodes = layout.rows.map((row, i) => {
|
|
546
|
+
if (i !== layout.cursorRow)
|
|
547
|
+
return _jsx(_Fragment, { children: renderSegments(row.segments, `s${i}.`) });
|
|
548
|
+
// The caret is drawn as one inverse cell, split at the column
|
|
549
|
+
// `layoutComposer` reported — the same width table it wrapped with (I-9).
|
|
550
|
+
const split = splitRowAtColumn(row, layout.cursorCol);
|
|
551
|
+
return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(Text, { inverse: true, children: split.at ? split.at.text : ' ' }), renderSegments(split.after, `a${i}.`)] }));
|
|
552
|
+
});
|
|
553
|
+
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
|
|
554
|
+
// The overflow indicator rides the LAST rendered row rather than
|
|
555
|
+
// taking one of its own: an extra row would put the composer's
|
|
556
|
+
// real height one above the `draftRows` it just reported (I-8).
|
|
557
|
+
_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { children: node }) }), _jsx(Box, { flexShrink: 0, width: overflow.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: overflow.text }) })] }, i)) : (_jsx(Text, { children: node }, i)))) }), chip && (
|
|
558
|
+
// `alignSelf="flex-start"` pins the chip to the FIRST row of a multi-line
|
|
559
|
+
// draft. Yoga's default `stretch` sizes this box to the full height of the
|
|
560
|
+
// editor; it renders on the top row either way today, and relying on that
|
|
561
|
+
// is how a later `justifyContent` edit moves it without failing anything.
|
|
562
|
+
//
|
|
563
|
+
// `width={chip.cells}` is a FIXED cell, not the chip's natural width
|
|
564
|
+
// (P2-4). The chip is the only thing on this row whose width changes while
|
|
565
|
+
// the user is typing — `↓ 9` becomes `↓ 10` becomes `↓ 100` — and every
|
|
566
|
+
// change would otherwise re-wrap a draft that fills the line.
|
|
567
|
+
_jsx(Box, { flexShrink: 0, alignSelf: "flex-start", marginLeft: 1, width: chip.cells, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: chip.text }) }))] }));
|
|
362
568
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupItems.length > 0 && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
|
|
363
569
|
}
|
|
364
|
-
/** Split the buffer into lines and render an inverse cursor at `cursor`. */
|
|
365
|
-
function renderWithCursor(buffer, cursor, active) {
|
|
366
|
-
if (!active)
|
|
367
|
-
return buffer.split('\n');
|
|
368
|
-
const before = buffer.slice(0, cursor);
|
|
369
|
-
const at = buffer[cursor] ?? ' ';
|
|
370
|
-
const after = buffer.slice(cursor + 1);
|
|
371
|
-
const beforeLines = before.split('\n');
|
|
372
|
-
const afterLines = after.split('\n');
|
|
373
|
-
const nodes = [];
|
|
374
|
-
const lastBeforeIdx = beforeLines.length - 1;
|
|
375
|
-
for (let i = 0; i < lastBeforeIdx; i += 1) {
|
|
376
|
-
nodes.push(_jsx(Text, { children: beforeLines[i] }, `b${i}`));
|
|
377
|
-
}
|
|
378
|
-
const cursorChar = at === '\n' ? ' ' : at;
|
|
379
|
-
const firstAfter = afterLines[0] ?? '';
|
|
380
|
-
nodes.push(_jsxs(Text, { children: [beforeLines[lastBeforeIdx], _jsx(Text, { inverse: true, children: cursorChar }), at === '\n' ? '' : firstAfter] }, "cursor-line"));
|
|
381
|
-
const remainingAfter = at === '\n' ? afterLines : afterLines.slice(1);
|
|
382
|
-
for (let i = 0; i < remainingAfter.length; i += 1) {
|
|
383
|
-
nodes.push(_jsx(Text, { children: remainingAfter[i] }, `a${i}`));
|
|
384
|
-
}
|
|
385
|
-
return nodes;
|
|
386
|
-
}
|