@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.
Files changed (167) hide show
  1. package/CHANGELOG.md +321 -0
  2. package/README.md +297 -2
  3. package/dist/agent/controller.d.ts +193 -1
  4. package/dist/agent/controller.js +475 -2
  5. package/dist/agent/headless.d.ts +8 -0
  6. package/dist/agent/headless.js +47 -0
  7. package/dist/agent/reducer.d.ts +189 -0
  8. package/dist/agent/reducer.js +301 -2
  9. package/dist/agent/system-prompt.d.ts +14 -0
  10. package/dist/agent/system-prompt.js +2 -0
  11. package/dist/cli.js +297 -16
  12. package/dist/commands/builtins.js +165 -24
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +118 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +355 -0
  20. package/dist/compaction/compactor.d.ts +302 -0
  21. package/dist/compaction/compactor.js +810 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +215 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +264 -0
  37. package/dist/compaction/wiring.js +577 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +5 -2
  43. package/dist/config/env.js +140 -3
  44. package/dist/config/load.d.ts +42 -0
  45. package/dist/config/load.js +101 -1
  46. package/dist/config/schema.d.ts +244 -0
  47. package/dist/config/schema.js +170 -0
  48. package/dist/config/store.js +37 -1
  49. package/dist/config/ui-state.d.ts +28 -1
  50. package/dist/config/ui-state.js +9 -0
  51. package/dist/exec/events.d.ts +50 -1
  52. package/dist/exec/runner.js +42 -0
  53. package/dist/input/limits.d.ts +72 -0
  54. package/dist/input/limits.js +63 -0
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/paste-parse.d.ts +71 -0
  58. package/dist/input/paste-parse.js +95 -0
  59. package/dist/input/stdin-filter.d.ts +81 -0
  60. package/dist/input/stdin-filter.js +379 -0
  61. package/dist/logging/install.d.ts +53 -1
  62. package/dist/logging/install.js +81 -1
  63. package/dist/logging/logger.d.ts +1 -1
  64. package/dist/proc/classify.d.ts +52 -0
  65. package/dist/proc/classify.js +143 -0
  66. package/dist/proc/kill-tree.d.ts +42 -0
  67. package/dist/proc/kill-tree.js +119 -0
  68. package/dist/proc/limits.d.ts +84 -0
  69. package/dist/proc/limits.js +84 -0
  70. package/dist/proc/log-ring.d.ts +68 -0
  71. package/dist/proc/log-ring.js +102 -0
  72. package/dist/proc/prompt.d.ts +24 -0
  73. package/dist/proc/prompt.js +49 -0
  74. package/dist/proc/readiness.d.ts +102 -0
  75. package/dist/proc/readiness.js +219 -0
  76. package/dist/proc/supervisor.d.ts +103 -0
  77. package/dist/proc/supervisor.js +465 -0
  78. package/dist/proc/types.d.ts +175 -0
  79. package/dist/proc/types.js +15 -0
  80. package/dist/session/persist.js +55 -1
  81. package/dist/team/report.js +7 -1
  82. package/dist/team/runtime.d.ts +10 -0
  83. package/dist/team/runtime.js +7 -0
  84. package/dist/team/subagent.d.ts +31 -1
  85. package/dist/team/subagent.js +42 -15
  86. package/dist/team/types.d.ts +8 -0
  87. package/dist/tools/bash-tool.d.ts +51 -2
  88. package/dist/tools/bash-tool.js +321 -51
  89. package/dist/tools/fs-tools.d.ts +17 -0
  90. package/dist/tools/index.d.ts +55 -2
  91. package/dist/tools/index.js +39 -1
  92. package/dist/tools/proc-tools.d.ts +25 -0
  93. package/dist/tools/proc-tools.js +102 -0
  94. package/dist/ui/ActivityLine.d.ts +19 -1
  95. package/dist/ui/ActivityLine.js +10 -2
  96. package/dist/ui/App.d.ts +62 -2
  97. package/dist/ui/App.js +546 -16
  98. package/dist/ui/Composer.d.ts +66 -1
  99. package/dist/ui/Composer.js +50 -6
  100. package/dist/ui/PromptInput.d.ts +82 -3
  101. package/dist/ui/PromptInput.js +232 -49
  102. package/dist/ui/StatusBar.d.ts +59 -0
  103. package/dist/ui/StatusBar.js +16 -4
  104. package/dist/ui/TeamPanel.js +7 -1
  105. package/dist/ui/Transcript.d.ts +21 -1
  106. package/dist/ui/Transcript.js +61 -3
  107. package/dist/ui/clipboard.d.ts +52 -0
  108. package/dist/ui/clipboard.js +91 -0
  109. package/dist/ui/composer-limits.d.ts +31 -0
  110. package/dist/ui/composer-limits.js +50 -0
  111. package/dist/ui/composer-rows.d.ts +73 -0
  112. package/dist/ui/composer-rows.js +201 -0
  113. package/dist/ui/editor-reducer.d.ts +37 -0
  114. package/dist/ui/editor-reducer.js +88 -12
  115. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  116. package/dist/ui/entries/CompactionCard.js +158 -0
  117. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  118. package/dist/ui/entries/ServiceCard.js +116 -0
  119. package/dist/ui/entries/UserEntry.d.ts +17 -3
  120. package/dist/ui/entries/UserEntry.js +34 -5
  121. package/dist/ui/frame-differ.d.ts +46 -0
  122. package/dist/ui/frame-differ.js +94 -10
  123. package/dist/ui/gauge.d.ts +16 -1
  124. package/dist/ui/gauge.js +25 -6
  125. package/dist/ui/glyphs.d.ts +11 -0
  126. package/dist/ui/glyphs.js +2 -0
  127. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  128. package/dist/ui/layout/ScrollViewport.js +128 -21
  129. package/dist/ui/layout/budget.d.ts +44 -9
  130. package/dist/ui/layout/budget.js +42 -10
  131. package/dist/ui/layout/follow-state.d.ts +137 -0
  132. package/dist/ui/layout/follow-state.js +150 -0
  133. package/dist/ui/layout/virtual-window.d.ts +11 -0
  134. package/dist/ui/layout/virtual-window.js +85 -1
  135. package/dist/ui/overlays/HelpOverlay.js +9 -1
  136. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  137. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  138. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  139. package/dist/ui/overlays/SettingsScreen.js +74 -2
  140. package/dist/ui/palettes.d.ts +16 -0
  141. package/dist/ui/palettes.js +6 -0
  142. package/dist/ui/paste-frames.d.ts +87 -0
  143. package/dist/ui/paste-frames.js +112 -0
  144. package/dist/ui/paste-tokens.d.ts +90 -0
  145. package/dist/ui/paste-tokens.js +147 -0
  146. package/dist/ui/screen.d.ts +38 -0
  147. package/dist/ui/screen.js +113 -8
  148. package/dist/ui/selection/highlight.d.ts +39 -0
  149. package/dist/ui/selection/highlight.js +64 -0
  150. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  151. package/dist/ui/selection/screen-mirror.js +35 -0
  152. package/dist/ui/selection/selection-controller.d.ts +88 -0
  153. package/dist/ui/selection/selection-controller.js +282 -0
  154. package/dist/ui/selection/selection.d.ts +90 -0
  155. package/dist/ui/selection/selection.js +223 -0
  156. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  157. package/dist/ui/stdout-frame-writer.js +15 -0
  158. package/dist/ui/theme.d.ts +11 -0
  159. package/dist/ui/theme.js +2 -0
  160. package/dist/ui/transcript-text.js +72 -0
  161. package/dist/ui/use-startup-notices.d.ts +27 -1
  162. package/dist/ui/use-startup-notices.js +43 -11
  163. package/dist/ui/use-wheel-routing.d.ts +1 -1
  164. package/dist/ui/use-wheel-routing.js +8 -0
  165. package/package.json +4 -2
  166. package/dist/input/stdin-mouse-filter.d.ts +0 -42
  167. package/dist/input/stdin-mouse-filter.js +0 -153
@@ -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
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
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 {};
@@ -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(running, submitCount, glyphs, mode, toggleKey) {
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
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
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) => setHasDraft(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(running, submitCount, glyphs, agentMode, modeToggleKey) })] }))] }));
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
  }
@@ -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
- /** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
67
- onDraftChange?: (hasDraft: boolean) => void;
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 {};
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
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
- const start = prevWord(buffer, cursor);
70
- return { buffer: buffer.slice(0, start) + buffer.slice(cursor), cursor: start };
71
- }
72
- case 'killToStart': {
73
- const start = lineStart(buffer, cursor);
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
- // --- Draft presence, reported only on the transition. ---------------------
188
- // Notifying on every keystroke would re-render the whole App tree per key.
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
- if (lastReportedDraft.current === hasDraft)
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 lines = renderWithCursor(buffer, cursor, isActive);
361
- const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (lines.map((line, i) => _jsx(Text, { children: line }, i))) })] }));
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
- }