@bubblebrain-ai/bubble 0.0.24 → 0.0.26

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 (171) hide show
  1. package/README.md +5 -3
  2. package/dist/agent.js +1 -1
  3. package/dist/clipboard.d.ts +14 -0
  4. package/dist/clipboard.js +132 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +22 -6
  7. package/dist/goal/format.js +34 -4
  8. package/dist/goal/store.d.ts +3 -0
  9. package/dist/goal/store.js +14 -1
  10. package/dist/goal/usage.d.ts +2 -0
  11. package/dist/goal/usage.js +3 -0
  12. package/dist/main.js +23 -42
  13. package/dist/model-catalog.d.ts +3 -1
  14. package/dist/model-catalog.js +17 -28
  15. package/dist/prompt/compose.js +1 -1
  16. package/dist/provider-anthropic.d.ts +4 -0
  17. package/dist/provider-anthropic.js +31 -0
  18. package/dist/provider-ark-responses.d.ts +17 -0
  19. package/dist/provider-ark-responses.js +462 -0
  20. package/dist/provider-transform.js +7 -0
  21. package/dist/provider.d.ts +7 -0
  22. package/dist/provider.js +170 -27
  23. package/dist/slash-commands/commands.js +22 -0
  24. package/dist/tools/todo.js +22 -38
  25. package/dist/tui/detect-theme.d.ts +1 -0
  26. package/dist/tui/detect-theme.js +23 -0
  27. package/dist/tui/image-display.d.ts +13 -0
  28. package/dist/tui/image-display.js +49 -0
  29. package/dist/tui/input-history.d.ts +37 -6
  30. package/dist/tui/input-history.js +194 -23
  31. package/dist/tui/model-switch.d.ts +42 -0
  32. package/dist/tui/model-switch.js +55 -0
  33. package/dist/tui-ink/app.d.ts +32 -2
  34. package/dist/tui-ink/app.js +1409 -549
  35. package/dist/tui-ink/approval/select.js +10 -0
  36. package/dist/tui-ink/detect-theme.d.ts +1 -2
  37. package/dist/tui-ink/detect-theme.js +1 -87
  38. package/dist/tui-ink/display-history.d.ts +1 -0
  39. package/dist/tui-ink/display-history.js +11 -0
  40. package/dist/tui-ink/feedback-dialog.js +10 -0
  41. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  42. package/dist/tui-ink/footer.d.ts +1 -0
  43. package/dist/tui-ink/footer.js +8 -2
  44. package/dist/tui-ink/input-box.d.ts +71 -9
  45. package/dist/tui-ink/input-box.js +359 -121
  46. package/dist/tui-ink/input-history.d.ts +1 -16
  47. package/dist/tui-ink/input-history.js +1 -79
  48. package/dist/tui-ink/input-queue.d.ts +12 -0
  49. package/dist/tui-ink/input-queue.js +17 -0
  50. package/dist/tui-ink/key-events.d.ts +9 -0
  51. package/dist/tui-ink/key-events.js +8 -0
  52. package/dist/tui-ink/markdown.js +1 -1
  53. package/dist/tui-ink/message-list.d.ts +19 -1
  54. package/dist/tui-ink/message-list.js +111 -32
  55. package/dist/tui-ink/model-picker.d.ts +25 -2
  56. package/dist/tui-ink/model-picker.js +237 -20
  57. package/dist/tui-ink/plan-confirm.js +10 -0
  58. package/dist/tui-ink/question-dialog.js +46 -10
  59. package/dist/tui-ink/run.d.ts +10 -1
  60. package/dist/tui-ink/run.js +27 -42
  61. package/dist/tui-ink/session-picker.js +3 -0
  62. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  63. package/dist/tui-ink/submit-dedupe.js +25 -0
  64. package/dist/tui-ink/terminal-mouse.d.ts +24 -1
  65. package/dist/tui-ink/terminal-mouse.js +76 -21
  66. package/dist/tui-ink/theme.d.ts +6 -3
  67. package/dist/tui-ink/theme.js +10 -4
  68. package/dist/tui-ink/welcome.d.ts +1 -0
  69. package/dist/tui-ink/welcome.js +34 -27
  70. package/dist/variant/variant-resolver.js +4 -1
  71. package/package.json +1 -5
  72. package/dist/tui/clipboard.d.ts +0 -1
  73. package/dist/tui/clipboard.js +0 -53
  74. package/dist/tui/escape-confirmation.d.ts +0 -15
  75. package/dist/tui/escape-confirmation.js +0 -30
  76. package/dist/tui/global-key-router.d.ts +0 -3
  77. package/dist/tui/global-key-router.js +0 -87
  78. package/dist/tui/markdown-inline.d.ts +0 -22
  79. package/dist/tui/markdown-inline.js +0 -68
  80. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  81. package/dist/tui/markdown-theme-rules.js +0 -164
  82. package/dist/tui/markdown-theme.d.ts +0 -5
  83. package/dist/tui/markdown-theme.js +0 -27
  84. package/dist/tui/opencode-spinner.d.ts +0 -22
  85. package/dist/tui/opencode-spinner.js +0 -216
  86. package/dist/tui/prompt-keybindings.d.ts +0 -42
  87. package/dist/tui/prompt-keybindings.js +0 -35
  88. package/dist/tui/render-signature.d.ts +0 -1
  89. package/dist/tui/render-signature.js +0 -7
  90. package/dist/tui/run.d.ts +0 -67
  91. package/dist/tui/run.js +0 -10166
  92. package/dist/tui/sidebar-mcp.d.ts +0 -31
  93. package/dist/tui/sidebar-mcp.js +0 -62
  94. package/dist/tui/sidebar-state.d.ts +0 -12
  95. package/dist/tui/sidebar-state.js +0 -69
  96. package/dist/tui/streaming-tool-args.d.ts +0 -15
  97. package/dist/tui/streaming-tool-args.js +0 -30
  98. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  99. package/dist/tui/tool-renderers/fallback.js +0 -75
  100. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  101. package/dist/tui/tool-renderers/registry.js +0 -11
  102. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  103. package/dist/tui/tool-renderers/subagent.js +0 -135
  104. package/dist/tui/tool-renderers/types.d.ts +0 -36
  105. package/dist/tui/tool-renderers/types.js +0 -1
  106. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  107. package/dist/tui/tool-renderers/write-preview.js +0 -32
  108. package/dist/tui/tool-renderers/write.d.ts +0 -6
  109. package/dist/tui/tool-renderers/write.js +0 -88
  110. package/dist/tui/transcript-scroll.d.ts +0 -25
  111. package/dist/tui/transcript-scroll.js +0 -20
  112. package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
  113. package/dist/tui-ink/transcript-viewport-math.js +0 -17
  114. package/dist/tui-ink/transcript-viewport.d.ts +0 -24
  115. package/dist/tui-ink/transcript-viewport.js +0 -83
  116. package/dist/tui-opentui/app.d.ts +0 -54
  117. package/dist/tui-opentui/app.js +0 -1371
  118. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  119. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  120. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  121. package/dist/tui-opentui/approval/diff-view.js +0 -43
  122. package/dist/tui-opentui/approval/select.d.ts +0 -37
  123. package/dist/tui-opentui/approval/select.js +0 -91
  124. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  125. package/dist/tui-opentui/detect-theme.js +0 -87
  126. package/dist/tui-opentui/display-history.d.ts +0 -56
  127. package/dist/tui-opentui/display-history.js +0 -130
  128. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  129. package/dist/tui-opentui/edit-diff.js +0 -57
  130. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  131. package/dist/tui-opentui/feedback-dialog.js +0 -164
  132. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  133. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  134. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  135. package/dist/tui-opentui/file-mentions.js +0 -174
  136. package/dist/tui-opentui/footer.d.ts +0 -26
  137. package/dist/tui-opentui/footer.js +0 -40
  138. package/dist/tui-opentui/image-paste.d.ts +0 -54
  139. package/dist/tui-opentui/image-paste.js +0 -288
  140. package/dist/tui-opentui/input-box.d.ts +0 -32
  141. package/dist/tui-opentui/input-box.js +0 -462
  142. package/dist/tui-opentui/input-history.d.ts +0 -16
  143. package/dist/tui-opentui/input-history.js +0 -79
  144. package/dist/tui-opentui/markdown.d.ts +0 -66
  145. package/dist/tui-opentui/markdown.js +0 -127
  146. package/dist/tui-opentui/message-list.d.ts +0 -31
  147. package/dist/tui-opentui/message-list.js +0 -131
  148. package/dist/tui-opentui/model-picker.d.ts +0 -63
  149. package/dist/tui-opentui/model-picker.js +0 -450
  150. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  151. package/dist/tui-opentui/plan-confirm.js +0 -124
  152. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  153. package/dist/tui-opentui/question-dialog.js +0 -110
  154. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  155. package/dist/tui-opentui/recent-activity.js +0 -71
  156. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  157. package/dist/tui-opentui/run-session-picker.js +0 -28
  158. package/dist/tui-opentui/run.d.ts +0 -38
  159. package/dist/tui-opentui/run.js +0 -48
  160. package/dist/tui-opentui/session-picker.d.ts +0 -12
  161. package/dist/tui-opentui/session-picker.js +0 -120
  162. package/dist/tui-opentui/theme.d.ts +0 -89
  163. package/dist/tui-opentui/theme.js +0 -157
  164. package/dist/tui-opentui/todos.d.ts +0 -9
  165. package/dist/tui-opentui/todos.js +0 -45
  166. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  167. package/dist/tui-opentui/trace-groups.js +0 -455
  168. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  169. package/dist/tui-opentui/use-terminal-size.js +0 -5
  170. package/dist/tui-opentui/welcome.d.ts +0 -25
  171. package/dist/tui-opentui/welcome.js +0 -77
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
4
4
  import stringWidth from "string-width";
@@ -7,10 +7,13 @@ import { registry as slashRegistry } from "../slash-commands/index.js";
7
7
  import { useTheme } from "./theme.js";
8
8
  import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
9
9
  import { bareImageFilenameFromPaste, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
10
- import { appendHistoryEntry, loadHistorySync, pushHistoryEntry, stepHistory, } from "./input-history.js";
10
+ import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
11
+ import { isKeyReleaseEvent } from "./key-events.js";
11
12
  import { stripTerminalMouseSequences } from "./terminal-mouse.js";
13
+ import { submitPayloadFingerprint } from "./submit-dedupe.js";
12
14
  export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
13
15
  import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
16
+ import { imageDisplayLabel } from "../tui/image-display.js";
14
17
  const MIN_VISIBLE_LINES = 3;
15
18
  const MAX_VISIBLE_LINES = 6;
16
19
  const PADDING_X = 1;
@@ -37,15 +40,43 @@ export function resolveCursorRowCompensation(input) {
37
40
  export function isCtrlCInput(input, key) {
38
41
  return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
39
42
  }
43
+ export function shouldUseLineComposerFrame(_background) {
44
+ return true;
45
+ }
46
+ export function composerSurfaceBackground(lineFrame, background, inputBg) {
47
+ return lineFrame ? background : inputBg;
48
+ }
49
+ export function shouldUseHardwareComposerCursor(env = process.env) {
50
+ return env.BUBBLE_HARDWARE_CURSOR === "1";
51
+ }
52
+ export function composerVerticalArrowDirection(key) {
53
+ if (key.upArrow)
54
+ return "up";
55
+ if (key.downArrow)
56
+ return "down";
57
+ return undefined;
58
+ }
59
+ export function resolveSoftwareCursorCellStyle(input) {
60
+ if (input.visible) {
61
+ return {
62
+ backgroundColor: input.cursorBackground,
63
+ color: input.cursorForeground,
64
+ };
65
+ }
66
+ return {
67
+ backgroundColor: input.rowBackground,
68
+ color: input.textColor,
69
+ };
70
+ }
40
71
  /**
41
72
  * Split a composer line around the cursor so the cell under it can render as
42
73
  * an inverse-video software cursor. The visible cursor must not depend on the
43
74
  * real terminal cursor: Ink only re-arms its one-shot cursor escape when the
44
75
  * component owning useCursor re-commits, so frames produced by other
45
76
  * components' local state (the waiting spinner, viewport scrolling) hide the
46
- * hardware cursor for most of an agent run. Drawing the cell ourselves keeps
47
- * the cursor visible on every frame; the real (mostly hidden) cursor is still
48
- * positioned for IME anchoring.
77
+ * hardware cursor for most of an agent run. Drawing and blinking the cell
78
+ * ourselves keeps it visible while preserving normal typing feedback; the real
79
+ * cursor is still positioned for IME anchoring.
49
80
  */
50
81
  export function splitLineAtCursor(lineText, charOffset) {
51
82
  const offset = Math.max(0, Math.min(charOffset, lineText.length));
@@ -132,6 +163,52 @@ function visualToCursor(visualLines, row, col) {
132
163
  }
133
164
  return vl.absStart + charOffset;
134
165
  }
166
+ export function resolveSlashCommandHighlightRange(input, commandNames) {
167
+ if (!input.startsWith("/"))
168
+ return null;
169
+ const match = /^\/([^\s]+)/.exec(input);
170
+ if (!match)
171
+ return null;
172
+ const commandName = match[1]?.toLowerCase();
173
+ if (!commandName)
174
+ return null;
175
+ for (const name of commandNames) {
176
+ if (name.toLowerCase() === commandName) {
177
+ return { start: 0, end: match[0].length };
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+ function splitHighlightedText(text, absStart, highlight) {
183
+ if (!text)
184
+ return [];
185
+ if (!highlight)
186
+ return [{ kind: "normal", text }];
187
+ const start = Math.max(0, highlight.start - absStart);
188
+ const end = Math.min(text.length, highlight.end - absStart);
189
+ if (start >= end)
190
+ return [{ kind: "normal", text }];
191
+ const segments = [];
192
+ if (start > 0)
193
+ segments.push({ kind: "normal", text: text.slice(0, start) });
194
+ segments.push({ kind: "command", text: text.slice(start, end) });
195
+ if (end < text.length)
196
+ segments.push({ kind: "normal", text: text.slice(end) });
197
+ return segments;
198
+ }
199
+ export function splitComposerTextSegments(input) {
200
+ if (input.cursorOffset === undefined) {
201
+ return splitHighlightedText(input.text, input.absStart, input.highlight);
202
+ }
203
+ const cursorOffset = Math.max(0, Math.min(input.text.length, input.cursorOffset));
204
+ const cursorSegments = splitLineAtCursor(input.text, cursorOffset);
205
+ const cursorConsumesSource = cursorOffset < input.text.length;
206
+ return [
207
+ ...splitHighlightedText(cursorSegments.before, input.absStart, input.highlight),
208
+ { kind: "cursor", text: cursorSegments.at },
209
+ ...splitHighlightedText(cursorSegments.after, input.absStart + cursorOffset + (cursorConsumesSource ? cursorSegments.at.length : 0), input.highlight),
210
+ ];
211
+ }
135
212
  export function shouldSubmitExactSlashSuggestion(input, suggestionName) {
136
213
  if (!suggestionName)
137
214
  return false;
@@ -171,18 +248,101 @@ export function insertNewlineAtCursor(text, cursor) {
171
248
  cursor: clampedCursor + 1,
172
249
  };
173
250
  }
174
- export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, terminalColumns, cwd, }) {
251
+ export function previousWordBoundary(text, cursor) {
252
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
253
+ if (clampedCursor === 0)
254
+ return 0;
255
+ let index = clampedCursor - 1;
256
+ while (index > 0 && /\s/.test(text[index]))
257
+ index--;
258
+ while (index > 0 && !/\s/.test(text[index - 1]))
259
+ index--;
260
+ return index;
261
+ }
262
+ export function nextWordBoundary(text, cursor) {
263
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
264
+ if (clampedCursor === text.length)
265
+ return text.length;
266
+ let index = clampedCursor;
267
+ while (index < text.length && /\s/.test(text[index]))
268
+ index++;
269
+ while (index < text.length && !/\s/.test(text[index]))
270
+ index++;
271
+ return index;
272
+ }
273
+ export function lineStartBoundary(text, cursor) {
274
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
275
+ return text.lastIndexOf("\n", clampedCursor - 1) + 1;
276
+ }
277
+ export function lineEndBoundary(text, cursor) {
278
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
279
+ const lineEnd = text.indexOf("\n", clampedCursor);
280
+ return lineEnd === -1 ? text.length : lineEnd;
281
+ }
282
+ export function deleteToLineStart(text, cursor) {
283
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
284
+ const lineStart = lineStartBoundary(text, clampedCursor);
285
+ return {
286
+ text: text.slice(0, lineStart) + text.slice(clampedCursor),
287
+ cursor: lineStart,
288
+ };
289
+ }
290
+ export function deleteToLineEnd(text, cursor) {
291
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
292
+ const lineEnd = lineEndBoundary(text, clampedCursor);
293
+ return {
294
+ text: text.slice(0, clampedCursor) + text.slice(lineEnd),
295
+ cursor: clampedCursor,
296
+ };
297
+ }
298
+ export function deleteAtCursor(text, cursor) {
299
+ const clampedCursor = Math.max(0, Math.min(text.length, cursor));
300
+ if (clampedCursor >= text.length)
301
+ return { text, cursor: clampedCursor };
302
+ return {
303
+ text: text.slice(0, clampedCursor) + text.slice(clampedCursor + 1),
304
+ cursor: clampedCursor,
305
+ };
306
+ }
307
+ export function resolveComposerEditAction(input, key) {
308
+ if (key.home)
309
+ return "line-start";
310
+ if (key.end)
311
+ return "line-end";
312
+ const wordModifier = key.ctrl || key.meta;
313
+ if (wordModifier && key.leftArrow)
314
+ return "word-left";
315
+ if (wordModifier && key.rightArrow)
316
+ return "word-right";
317
+ const lowerInput = input.toLowerCase();
318
+ if ((key.ctrl && lowerInput === "a") || input === "\x01")
319
+ return "line-start";
320
+ if ((key.ctrl && lowerInput === "e") || input === "\x05")
321
+ return "line-end";
322
+ if ((key.ctrl && lowerInput === "u") || input === "\x15")
323
+ return "delete-line-start";
324
+ if ((key.ctrl && lowerInput === "k") || input === "\x0b")
325
+ return "delete-line-end";
326
+ return null;
327
+ }
328
+ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
175
329
  const theme = useTheme();
176
330
  const width = terminalColumns;
331
+ const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
332
+ const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
177
333
  const [text, setText] = useState("");
178
334
  const [cursor, setCursor] = useState(0);
335
+ const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
179
336
  const [selectedIndex, setSelectedIndex] = useState(0);
180
337
  const [projectFiles, setProjectFiles] = useState(null);
181
338
  const [attachments, setAttachments] = useState([]);
339
+ const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
182
340
  const [pastedContentRefs, setPastedContentRefs] = useState([]);
183
- const [history, setHistory] = useState(() => loadHistorySync());
341
+ const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
184
342
  const [historyIndex, setHistoryIndex] = useState(null);
185
343
  const historyDraftRef = useRef("");
344
+ const historyScopeRef = useRef(historyScope);
345
+ const submittedPayloadFingerprintRef = useRef(null);
186
346
  const loadingFilesRef = useRef(false);
187
347
  const nextPastedContentIndexRef = useRef(1);
188
348
  // Paste and the keystrokes that follow can arrive inside the same stdin chunk
@@ -192,6 +352,16 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
192
352
  // This ref flips synchronously at paste-start and clears after the paste
193
353
  // commit has been flushed — useInput's Enter handler bails while it's set.
194
354
  const pastePendingRef = useRef(false);
355
+ historyScopeRef.current = historyScope;
356
+ const ensureImageLabelStart = React.useCallback(() => {
357
+ setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
358
+ }, [nextImageLabelStart]);
359
+ useEffect(() => {
360
+ setHistory(loadHistoryEntriesSync({ scope: historyScope }));
361
+ setHistoryIndex(null);
362
+ setImageLabelStartOverride(null);
363
+ historyDraftRef.current = "";
364
+ }, [historyScope]);
195
365
  const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
196
366
  const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
197
367
  const atContext = useMemo(() => (isSlashContext ? null : findAtContext(text, cursor)), [text, cursor, isSlashContext]);
@@ -201,23 +371,31 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
201
371
  loadingFilesRef.current = true;
202
372
  listProjectFiles(cwd).then((files) => setProjectFiles(files), () => setProjectFiles([]));
203
373
  }, [atContext, cwd, projectFiles]);
204
- // Request a steady (non-blinking) block cursor via DECSCUSR while this
205
- // component is mounted. Terminals default to a blinking cursor, which is
206
- // distracting in an input that you'd glance away from. Restore the
207
- // terminal default on unmount so the user's shell isn't left with our
208
- // choice sticking around.
374
+ // The rendered inverse-video cell below is the visible cursor. Keep Ink's
375
+ // terminal cursor hidden by default so it can't race the software cursor; the
376
+ // hardware cursor can be enabled for IME diagnostics with BUBBLE_HARDWARE_CURSOR=1.
209
377
  useEffect(() => {
378
+ if (!hardwareCursorEnabled)
379
+ return;
210
380
  if (!process.stdout.isTTY)
211
381
  return;
212
- process.stdout.write("\x1b[2 q"); // steady block
382
+ process.stdout.write("\x1b[1 q"); // blinking block
213
383
  return () => {
214
384
  process.stdout.write("\x1b[0 q"); // reset to terminal default
215
385
  };
216
- }, []);
386
+ }, [hardwareCursorEnabled]);
217
387
  const slashSuggestions = useMemo(() => {
218
388
  if (!isSlashContext)
219
389
  return [];
220
- const commandSuggestions = slashRegistry.list().map((command) => ({
390
+ const commands = new Map();
391
+ for (const command of localSlashCommands) {
392
+ commands.set(command.name, command);
393
+ }
394
+ for (const command of slashRegistry.list()) {
395
+ if (!commands.has(command.name))
396
+ commands.set(command.name, command);
397
+ }
398
+ const commandSuggestions = [...commands.values()].map((command) => ({
221
399
  type: "command",
222
400
  name: command.name,
223
401
  description: command.description,
@@ -229,7 +407,18 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
229
407
  }));
230
408
  const all = [...commandSuggestions, ...skillSuggestions];
231
409
  return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
232
- }, [isSlashContext, slashPrefix, skillRegistry]);
410
+ }, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
411
+ const knownSlashCommandNames = useMemo(() => {
412
+ const names = new Set();
413
+ for (const command of localSlashCommands)
414
+ names.add(command.name);
415
+ for (const command of slashRegistry.list())
416
+ names.add(command.name);
417
+ for (const skill of skillRegistry?.summaries() ?? [])
418
+ names.add(skill.name);
419
+ return names;
420
+ }, [skillRegistry, localSlashCommands]);
421
+ const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
233
422
  const fileSuggestions = useMemo(() => {
234
423
  if (!atContext || !projectFiles)
235
424
  return [];
@@ -259,8 +448,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
259
448
  setCursor((c) => c + insertion.length);
260
449
  }, [cursor]);
261
450
  const addAttachment = React.useCallback((att) => {
451
+ ensureImageLabelStart();
262
452
  setAttachments((prev) => [...prev, att]);
263
- }, []);
453
+ }, [ensureImageLabelStart]);
264
454
  const notice = React.useCallback((msg) => {
265
455
  onPasteNotice?.(msg);
266
456
  }, [onPasteNotice]);
@@ -394,24 +584,38 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
394
584
  if (expandedText.trim().length === 0 && attachments.length === 0)
395
585
  return;
396
586
  const deliver = target === "queue" && onQueue ? onQueue : onSubmit;
397
- deliver({
587
+ const payload = {
398
588
  text: expandedText,
399
589
  displayText: expandedText === submittedText ? undefined : submittedText,
400
590
  images: attachments,
401
- });
591
+ imageDisplayStart: attachments.length > 0 ? (imageLabelStartOverride ?? nextImageLabelStart) : undefined,
592
+ };
593
+ const fingerprint = submitPayloadFingerprint(payload);
594
+ if (submittedPayloadFingerprintRef.current === fingerprint)
595
+ return;
596
+ submittedPayloadFingerprintRef.current = fingerprint;
597
+ deliver(payload);
402
598
  // A collapsed marker cannot be safely replayed from history once its
403
599
  // in-memory paste reference is gone; skip those entries instead.
404
- if (expandedText.trim().length > 0 && expandedText === submittedText) {
405
- const nextHistory = pushHistoryEntry(history, expandedText);
406
- if (nextHistory !== history) {
407
- setHistory(nextHistory);
408
- appendHistoryEntry(expandedText);
409
- }
600
+ if (expandedText === submittedText && (expandedText.trim().length > 0 || attachments.length > 0)) {
601
+ const historyEntry = {
602
+ text: expandedText,
603
+ images: attachments,
604
+ ...(attachments.length > 0 ? { imageDisplayStart: imageLabelStartOverride ?? nextImageLabelStart } : {}),
605
+ };
606
+ setHistory((current) => {
607
+ const nextHistory = pushHistoryEntry(current, historyEntry);
608
+ if (nextHistory !== current) {
609
+ appendHistoryEntry(historyEntry, { scope: historyScopeRef.current });
610
+ }
611
+ return nextHistory;
612
+ });
410
613
  }
411
614
  setText("");
412
615
  setCursor(0);
413
616
  setSelectedIndex(0);
414
617
  setAttachments([]);
618
+ setImageLabelStartOverride(null);
415
619
  setPastedContentRefs([]);
416
620
  nextPastedContentIndexRef.current = 1;
417
621
  setHistoryIndex(null);
@@ -432,6 +636,8 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
432
636
  return false;
433
637
  };
434
638
  useInput((input, key) => {
639
+ if (isKeyReleaseEvent(key))
640
+ return;
435
641
  const strippedInput = stripTerminalMouseSequences(input);
436
642
  if (strippedInput !== input && !strippedInput) {
437
643
  return;
@@ -479,13 +685,14 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
479
685
  submitInput(nextText);
480
686
  return;
481
687
  }
688
+ const composerArrowDirection = composerVerticalArrowDirection(key);
482
689
  // Autocomplete navigation
483
690
  if (showSuggestions) {
484
- if (navigable && key.upArrow) {
691
+ if (navigable && composerArrowDirection === "up") {
485
692
  setSelectedIndex((i) => (i - 1 + activeCount) % activeCount);
486
693
  return;
487
694
  }
488
- if (navigable && key.downArrow) {
695
+ if (navigable && composerArrowDirection === "down") {
489
696
  setSelectedIndex((i) => (i + 1) % activeCount);
490
697
  return;
491
698
  }
@@ -535,7 +742,34 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
535
742
  submitInput(text);
536
743
  return;
537
744
  }
538
- if (key.backspace || key.delete) {
745
+ const editAction = resolveComposerEditAction(input, key);
746
+ if (editAction) {
747
+ if (editAction === "word-left") {
748
+ setCursor(previousWordBoundary(text, cursor));
749
+ }
750
+ else if (editAction === "word-right") {
751
+ setCursor(nextWordBoundary(text, cursor));
752
+ }
753
+ else if (editAction === "line-start") {
754
+ setCursor(lineStartBoundary(text, cursor));
755
+ }
756
+ else if (editAction === "line-end") {
757
+ setCursor(lineEndBoundary(text, cursor));
758
+ }
759
+ else if (editAction === "delete-line-start") {
760
+ const next = deleteToLineStart(text, cursor);
761
+ setText(next.text);
762
+ setCursor(next.cursor);
763
+ }
764
+ else {
765
+ const next = deleteToLineEnd(text, cursor);
766
+ setText(next.text);
767
+ setCursor(next.cursor);
768
+ }
769
+ setSelectedIndex(0);
770
+ return;
771
+ }
772
+ if (key.backspace) {
539
773
  if (cursor > 0) {
540
774
  const before = text.slice(0, cursor - 1);
541
775
  const after = text.slice(cursor);
@@ -546,7 +780,21 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
546
780
  else if (attachments.length > 0) {
547
781
  // Backspace at position 0 drops the most recent attachment so users
548
782
  // can undo a misfired paste without submitting the message.
549
- setAttachments((prev) => prev.slice(0, -1));
783
+ setAttachments((prev) => {
784
+ const next = prev.slice(0, -1);
785
+ if (next.length === 0)
786
+ setImageLabelStartOverride(null);
787
+ return next;
788
+ });
789
+ }
790
+ return;
791
+ }
792
+ if (key.delete) {
793
+ if (cursor < text.length) {
794
+ const next = deleteAtCursor(text, cursor);
795
+ setText(next.text);
796
+ setCursor(next.cursor);
797
+ setSelectedIndex(0);
550
798
  }
551
799
  return;
552
800
  }
@@ -561,7 +809,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
561
809
  return;
562
810
  }
563
811
  if (key.upArrow || key.downArrow) {
564
- classifyVerticalArrow(key.upArrow ? "up" : "down", key.eventType);
812
+ if (composerArrowDirection) {
813
+ classifyVerticalArrow(composerArrowDirection);
814
+ }
565
815
  return;
566
816
  }
567
817
  // Ctrl/meta chords are app-level shortcuts (Ctrl+S selection mode,
@@ -619,12 +869,19 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
619
869
  setText(draftText);
620
870
  setCursor(draftText.length);
621
871
  setSelectedIndex(0);
872
+ setAttachments([]);
873
+ setImageLabelStartOverride(null);
622
874
  setPastedContentRefs([]);
623
875
  nextPastedContentIndexRef.current = 1;
624
876
  setHistoryIndex(null);
625
877
  historyDraftRef.current = "";
626
878
  onDraftApplied?.();
627
879
  }, [draftEpoch, draftText, onDraftApplied]);
880
+ useEffect(() => {
881
+ if (text || attachments.length > 0) {
882
+ submittedPayloadFingerprintRef.current = null;
883
+ }
884
+ }, [text, attachments.length]);
628
885
  // After a terminal resize the previous-frame refs reference a layout that no
629
886
  // longer exists; carrying them forward makes `needsCursorRowCompensation`
630
887
  // compare new yoga heights against stale ones and offsets the cursor by a
@@ -642,40 +899,50 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
642
899
  }, [width]);
643
900
  const contentWidth = Math.max(1, width - PADDING_X * 2);
644
901
  const lineWidth = Math.max(1, contentWidth - PROMPT.length);
645
- const visualLines = useMemo(() => computeVisualLines(text, lineWidth), [text, lineWidth]);
646
- const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, cursor);
647
- // ---- Wheel-vs-keyboard classification for Up/Down arrows ----
902
+ const imageLabelStart = imageLabelStartOverride ?? nextImageLabelStart;
903
+ const attachmentLabels = useMemo(() => attachments.map((_, index) => imageDisplayLabel(imageLabelStart + index)), [attachments, imageLabelStart]);
904
+ const imageInlinePrefix = attachmentLabels.length > 0 ? `${attachmentLabels.join(" ")} ` : "";
905
+ const displayText = imageInlinePrefix + text;
906
+ const displayCursor = cursor + imageInlinePrefix.length;
907
+ const displayCursorToSourceCursor = (value) => Math.max(0, Math.min(text.length, value - imageInlinePrefix.length));
908
+ // Steady (non-blinking) cursor on purpose. The composer lives in the live
909
+ // (repainting) region; a blink timer would rewrite these rows ~twice a second
910
+ // even at idle, and the terminal drops any in-progress text selection every
911
+ // time the underlying cells are rewritten — which is why composer text could
912
+ // not be highlighted/copied while agent answers (committed to <Static>, never
913
+ // repainted) could. Keeping the cursor steady leaves the idle composer frame
914
+ // static, so native selection works. We still hide it while disabled.
915
+ useEffect(() => {
916
+ setSoftwareCursorVisible(!disabled);
917
+ }, [disabled]);
918
+ const visualLines = useMemo(() => computeVisualLines(displayText, lineWidth), [displayText, lineWidth]);
919
+ const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, displayCursor);
920
+ // ---- Up/Down arrow handling in the composer ----
648
921
  //
649
- // With mouse reporting off (so native drag-select/copy works), terminals
650
- // translate the wheel into Up/Down arrow keys while in the alternate
651
- // screen. Those synthetic arrows must scroll the transcript, not move the
652
- // composer cursor or browse history. Three signals, in priority order:
653
- // 1. kitty keyboard protocol: real key presses carry `eventType`;
654
- // synthetic wheel arrows are bare legacy sequences. Once one enhanced
655
- // arrow is seen, bare arrows are classified as wheel with no delay.
656
- // 2. burst heuristic (non-kitty terminals): a wheel notch delivers
657
- // several arrows within a few ms; a keypress delivers one. The first
658
- // arrow is briefly deferred to see whether siblings follow.
659
- // 3. wheel session: shortly after a wheel burst, single arrows continue
660
- // to scroll, so slow trackpad scrolling doesn't fall back to history.
922
+ // Scrolling is the terminal's job now (native scrollback), so the composer
923
+ // owns Up/Down unconditionally: move within multiline input first, then
924
+ // browse prompt history at the top edge (Up previous sent message) or the
925
+ // bottom edge (Down next message, then back to the in-progress draft).
661
926
  const performVerticalArrowRef = useRef(() => { });
662
927
  performVerticalArrowRef.current = (direction) => {
663
928
  if (direction === "up") {
664
929
  if (cursorVisualRow > 0) {
665
- setCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
930
+ setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol)));
666
931
  return;
667
932
  }
668
933
  }
669
934
  else {
670
935
  if (cursorVisualRow < visualLines.length - 1) {
671
- setCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
936
+ setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol)));
672
937
  return;
673
938
  }
674
939
  }
675
- const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, text);
940
+ const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, { text, images: attachments });
676
941
  if (result.changed) {
677
942
  setText(result.text);
678
943
  setCursor(result.text.length);
944
+ setAttachments(result.images ?? []);
945
+ setImageLabelStartOverride(result.imageDisplayStart ?? null);
679
946
  setHistoryIndex(result.index);
680
947
  historyDraftRef.current = result.draft;
681
948
  setSelectedIndex(0);
@@ -683,66 +950,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
683
950
  nextPastedContentIndexRef.current = 1;
684
951
  }
685
952
  };
686
- const kittyArrowsSeenRef = useRef(false);
687
- const arrowBurstRef = useRef(null);
688
- const lastWheelFlushRef = useRef(0);
689
- const ARROW_BURST_WINDOW_MS = 20;
690
- const WHEEL_SESSION_MS = 300;
691
- const flushArrowBurst = (burst) => {
692
- if (burst.count > 1 && onWheelScroll) {
693
- lastWheelFlushRef.current = Date.now();
694
- onWheelScroll(burst.direction, burst.count);
695
- }
696
- else {
697
- performVerticalArrowRef.current(burst.direction);
698
- }
699
- };
700
- const classifyVerticalArrow = (direction, eventType) => {
701
- if (eventType) {
702
- kittyArrowsSeenRef.current = true;
703
- performVerticalArrowRef.current(direction);
704
- return;
705
- }
706
- if (!onWheelScroll) {
707
- performVerticalArrowRef.current(direction);
708
- return;
709
- }
710
- if (kittyArrowsSeenRef.current) {
711
- lastWheelFlushRef.current = Date.now();
712
- onWheelScroll(direction, 1);
713
- return;
714
- }
715
- const pending = arrowBurstRef.current;
716
- if (pending) {
717
- if (pending.direction === direction) {
718
- pending.count += 1;
719
- return;
720
- }
721
- clearTimeout(pending.timer);
722
- arrowBurstRef.current = null;
723
- flushArrowBurst(pending);
724
- }
725
- if (Date.now() - lastWheelFlushRef.current < WHEEL_SESSION_MS) {
726
- lastWheelFlushRef.current = Date.now();
727
- onWheelScroll(direction, 1);
728
- return;
729
- }
730
- const burst = {
731
- direction,
732
- count: 1,
733
- timer: setTimeout(() => {
734
- arrowBurstRef.current = null;
735
- flushArrowBurst(burst);
736
- }, ARROW_BURST_WINDOW_MS),
737
- };
738
- arrowBurstRef.current = burst;
953
+ const classifyVerticalArrow = (direction) => {
954
+ performVerticalArrowRef.current(direction);
739
955
  };
740
- useEffect(() => () => {
741
- if (arrowBurstRef.current)
742
- clearTimeout(arrowBurstRef.current.timer);
743
- }, []);
956
+ const lineFrame = shouldUseLineComposerFrame(theme.background);
957
+ const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
744
958
  const totalLines = Math.max(visualLines.length, 1);
745
- const visibleLines = Math.min(Math.max(totalLines, MIN_VISIBLE_LINES), MAX_VISIBLE_LINES);
959
+ const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
746
960
  let scrollOffset = 0;
747
961
  if (totalLines > visibleLines) {
748
962
  scrollOffset = Math.min(Math.max(cursorVisualRow - Math.floor(visibleLines / 2), 0), totalLines - visibleLines);
@@ -772,6 +986,7 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
772
986
  const inputFrameSignature = [
773
987
  disabled ? "disabled" : "active",
774
988
  text,
989
+ imageInlinePrefix,
775
990
  scrollOffset.toString(),
776
991
  visibleLines.toString(),
777
992
  attachments.map((att) => `${att.filename ?? "clipboard"}:${att.bytes}`).join(","),
@@ -791,6 +1006,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
791
1006
  // flicker every time streaming output above it re-lays out the frame, so
792
1007
  // we hide it entirely until input is active again.
793
1008
  useLayoutEffect(() => {
1009
+ if (!hardwareCursorEnabled) {
1010
+ if (lastCursorRef.current !== null) {
1011
+ lastCursorRef.current = null;
1012
+ }
1013
+ setCursorPosition(undefined);
1014
+ return;
1015
+ }
794
1016
  let node = cursorLineRef.current ?? undefined;
795
1017
  if (!node?.yogaNode) {
796
1018
  if (disabled && lastCursorRef.current !== null) {
@@ -867,37 +1089,53 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
867
1089
  // Reference cursorTick so the effect re-runs on the forced render pass.
868
1090
  void cursorTick;
869
1091
  const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
1092
+ const composerBg = composerSurfaceBackground(lineFrame, theme.background, inputBg);
1093
+ const rowBg = lineFrame ? undefined : inputBg;
1094
+ const cursorFg = lineFrame ? theme.background : inputBg;
1095
+ const cursorCellStyle = resolveSoftwareCursorCellStyle({
1096
+ visible: softwareCursorVisible,
1097
+ cursorBackground: theme.inputText,
1098
+ cursorForeground: cursorFg,
1099
+ textColor: theme.inputText,
1100
+ rowBackground: rowBg,
1101
+ });
870
1102
  const moreBelow = totalLines - scrollOffset - visibleLines;
871
1103
  const filledLine = (value) => {
872
1104
  const visibleWidth = stringWidth(value);
873
1105
  return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
874
1106
  };
875
- return (_jsxs(Box, { flexDirection: "column", children: [attachments.length > 0 && (_jsx(Box, { flexDirection: "row", flexWrap: "wrap", paddingX: PADDING_X, marginBottom: 0, children: attachments.map((att, i) => {
876
- const label = att.filename || "clipboard";
877
- const kb = Math.max(1, Math.round(att.bytes / 1024));
878
- return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: theme.accent, children: `[img${attachments.length > 1 ? ` ${i + 1}` : ""}: ${label} · ${kb}KB]` }) }, i));
879
- }) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: inputBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
1107
+ return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "".repeat(contentWidth) }) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: composerBg, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
880
1108
  if (row.kind === "pad") {
881
- return (_jsx(Text, { backgroundColor: inputBg, children: " ".repeat(contentWidth) }, row.key));
1109
+ return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
882
1110
  }
883
1111
  const { text: line, visualIdx } = row;
1112
+ const visualLine = visualLines[visualIdx];
884
1113
  const lineText = line.length === 0 ? " " : line;
885
1114
  const isFirst = visualIdx === 0;
886
1115
  const isCursorLine = visualIdx === cursorVisualRow;
887
1116
  const prompt = isFirst ? PROMPT : " ".repeat(PROMPT.length);
888
- const cursorSegments = isCursorLine && !disabled
889
- ? splitLineAtCursor(lineText, cursor - (visualLines[cursorVisualRow]?.absStart ?? 0))
890
- : null;
891
- const renderedLine = cursorSegments
892
- ? cursorSegments.before + cursorSegments.at + cursorSegments.after
893
- : lineText;
1117
+ const highlight = imageInlinePrefix ? null : slashCommandHighlight;
1118
+ const renderedSegments = splitComposerTextSegments({
1119
+ text: lineText,
1120
+ absStart: visualLine?.absStart ?? 0,
1121
+ highlight,
1122
+ cursorOffset: isCursorLine && !disabled
1123
+ ? displayCursor - (visualLines[cursorVisualRow]?.absStart ?? 0)
1124
+ : undefined,
1125
+ });
1126
+ const renderedLine = renderedSegments.map((segment) => segment.text).join("");
894
1127
  const fill = " ".repeat(Math.max(0, lineWidth - stringWidth(renderedLine)));
895
- return (_jsxs(Box, { height: 1, overflow: "hidden", ref: isCursorLine
1128
+ return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: composerBg, ref: isCursorLine
896
1129
  ? (el) => {
897
1130
  cursorLineRef.current = el;
898
1131
  }
899
- : undefined, children: [_jsx(Text, { backgroundColor: inputBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), cursorSegments ? (_jsxs(_Fragment, { children: [cursorSegments.before && (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: cursorSegments.before })), _jsx(Text, { backgroundColor: theme.inputText, color: inputBg, children: cursorSegments.at }), cursorSegments.after && (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: cursorSegments.after }))] })) : (_jsx(Text, { backgroundColor: inputBg, color: theme.inputText, children: lineText })), _jsx(Text, { backgroundColor: inputBg, children: fill })] }, visualIdx));
900
- }), hasMoreBelow && (_jsx(Text, { backgroundColor: inputBg, color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), showSuggestions && mode === "slash" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 4, children: [slashSuggestions
1132
+ : undefined, children: [_jsx(Text, { backgroundColor: rowBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1133
+ if (segment.kind === "cursor") {
1134
+ return (_jsx(Text, { backgroundColor: cursorCellStyle.backgroundColor, color: cursorCellStyle.color, children: segment.text }, index));
1135
+ }
1136
+ return (_jsx(Text, { backgroundColor: rowBg, color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
1137
+ }), _jsx(Text, { backgroundColor: rowBg, children: fill })] }, visualIdx));
1138
+ }), hasMoreBelow && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) })), showSuggestions && mode === "slash" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 4, children: [slashSuggestions
901
1139
  .slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
902
1140
  .map((cmd, visibleIndex) => {
903
1141
  const i = suggestionOffset + visibleIndex;