@bubblebrain-ai/bubble 0.0.23 → 0.0.25

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