@bubblebrain-ai/bubble 0.0.34 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +27 -7
  2. package/dist/agent/abort-errors.d.ts +2 -2
  3. package/dist/agent/budget-ledger.d.ts +4 -45
  4. package/dist/agent/budget-ledger.js +5 -75
  5. package/dist/agent/categories.d.ts +66 -2
  6. package/dist/agent/categories.js +120 -19
  7. package/dist/agent/child-runner.d.ts +1 -3
  8. package/dist/agent/child-runner.js +6 -37
  9. package/dist/agent/discovery-barrier.js +1 -1
  10. package/dist/agent/profiles.d.ts +1 -2
  11. package/dist/agent/profiles.js +25 -14
  12. package/dist/agent/routing-catalog.d.ts +113 -0
  13. package/dist/agent/routing-catalog.js +240 -0
  14. package/dist/agent/subagent-control.d.ts +5 -12
  15. package/dist/agent/subagent-control.js +0 -2
  16. package/dist/agent/subagent-store.d.ts +1 -1
  17. package/dist/agent/subagent-store.js +1 -3
  18. package/dist/agent/workflow/control.d.ts +21 -0
  19. package/dist/agent/workflow/control.js +50 -8
  20. package/dist/agent/workflow/runtime.d.ts +15 -2
  21. package/dist/agent/workflow/runtime.js +112 -30
  22. package/dist/agent.d.ts +82 -48
  23. package/dist/agent.js +360 -232
  24. package/dist/approval/controller.js +28 -0
  25. package/dist/approval/tool-helper.js +2 -0
  26. package/dist/approval/types.d.ts +21 -1
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +2 -1
  29. package/dist/config.d.ts +5 -3
  30. package/dist/config.js +10 -5
  31. package/dist/context/compact-llm.d.ts +2 -2
  32. package/dist/external-runtime/grok-acp.d.ts +56 -0
  33. package/dist/external-runtime/grok-acp.js +386 -0
  34. package/dist/external-runtime/grok-binary.d.ts +25 -0
  35. package/dist/external-runtime/grok-binary.js +108 -0
  36. package/dist/external-runtime/grok-errors.d.ts +7 -0
  37. package/dist/external-runtime/grok-errors.js +15 -0
  38. package/dist/external-runtime/grok-input-policy.d.ts +58 -0
  39. package/dist/external-runtime/grok-input-policy.js +52 -0
  40. package/dist/external-runtime/grok-ndjson.d.ts +14 -0
  41. package/dist/external-runtime/grok-ndjson.js +193 -0
  42. package/dist/external-runtime/grok-network.d.ts +15 -0
  43. package/dist/external-runtime/grok-network.js +81 -0
  44. package/dist/external-runtime/grok-preflight.d.ts +28 -0
  45. package/dist/external-runtime/grok-preflight.js +194 -0
  46. package/dist/external-runtime/grok-process.d.ts +26 -0
  47. package/dist/external-runtime/grok-process.js +184 -0
  48. package/dist/external-runtime/grok-profile.d.ts +32 -0
  49. package/dist/external-runtime/grok-profile.js +419 -0
  50. package/dist/external-runtime/grok-provider.d.ts +31 -0
  51. package/dist/external-runtime/grok-provider.js +41 -0
  52. package/dist/external-runtime/grok-runtime.d.ts +91 -0
  53. package/dist/external-runtime/grok-runtime.js +882 -0
  54. package/dist/external-runtime/index.d.ts +13 -0
  55. package/dist/external-runtime/index.js +13 -0
  56. package/dist/external-runtime/session-policy.d.ts +11 -0
  57. package/dist/external-runtime/session-policy.js +30 -0
  58. package/dist/external-runtime/types.d.ts +65 -0
  59. package/dist/external-runtime/types.js +1 -0
  60. package/dist/feishu/agent-host/approval-card.js +21 -0
  61. package/dist/feishu/agent-host/run-driver.js +19 -3
  62. package/dist/feishu/serve.js +1 -0
  63. package/dist/goal/tools.d.ts +6 -3
  64. package/dist/goal/tools.js +7 -19
  65. package/dist/main.js +114 -28
  66. package/dist/memory/db.d.ts +2 -0
  67. package/dist/memory/db.js +10 -0
  68. package/dist/memory/index.d.ts +1 -0
  69. package/dist/memory/index.js +1 -0
  70. package/dist/memory/phase1.js +14 -2
  71. package/dist/memory/phase2.js +21 -2
  72. package/dist/memory/prompts.js +1 -1
  73. package/dist/memory/session-policy.d.ts +25 -0
  74. package/dist/memory/session-policy.js +55 -0
  75. package/dist/memory/storage.d.ts +1 -0
  76. package/dist/memory/storage.js +13 -0
  77. package/dist/model-catalog.d.ts +18 -0
  78. package/dist/model-catalog.js +92 -52
  79. package/dist/model-config.d.ts +3 -1
  80. package/dist/model-config.js +11 -0
  81. package/dist/oauth/grok.d.ts +25 -0
  82. package/dist/oauth/grok.js +218 -0
  83. package/dist/oauth/index.d.ts +1 -0
  84. package/dist/oauth/index.js +1 -0
  85. package/dist/oauth/storage.d.ts +9 -0
  86. package/dist/oauth/storage.js +25 -0
  87. package/dist/orchestrator/default-hooks.js +23 -7
  88. package/dist/prompt/compose.d.ts +3 -0
  89. package/dist/prompt/compose.js +3 -0
  90. package/dist/prompt/delegation.d.ts +1 -1
  91. package/dist/prompt/delegation.js +41 -8
  92. package/dist/prompt/environment.js +0 -4
  93. package/dist/prompt/reminders.d.ts +0 -1
  94. package/dist/prompt/reminders.js +1 -11
  95. package/dist/prompt/routing.d.ts +14 -0
  96. package/dist/prompt/routing.js +71 -0
  97. package/dist/prompt/runtime.js +1 -1
  98. package/dist/prompt/task-reminders.d.ts +20 -0
  99. package/dist/prompt/task-reminders.js +73 -1
  100. package/dist/provider-ai-sdk.js +2 -2
  101. package/dist/provider-grok.d.ts +26 -0
  102. package/dist/provider-grok.js +59 -0
  103. package/dist/provider-openai-codex.d.ts +17 -1
  104. package/dist/provider-openai-codex.js +101 -29
  105. package/dist/provider-registry.d.ts +72 -0
  106. package/dist/provider-registry.js +336 -59
  107. package/dist/provider-transform.d.ts +1 -1
  108. package/dist/provider-transform.js +35 -17
  109. package/dist/provider.d.ts +3 -0
  110. package/dist/provider.js +14 -0
  111. package/dist/session-types.d.ts +19 -1
  112. package/dist/session.d.ts +1 -0
  113. package/dist/session.js +4 -0
  114. package/dist/slash-commands/commands.js +224 -14
  115. package/dist/slash-commands/types.d.ts +14 -1
  116. package/dist/system-prompt.d.ts +2 -0
  117. package/dist/tools/agent-lifecycle.d.ts +1 -9
  118. package/dist/tools/agent-lifecycle.js +42 -272
  119. package/dist/tools/bash.js +1 -1
  120. package/dist/tools/child-tools.js +2 -0
  121. package/dist/tools/index.d.ts +2 -2
  122. package/dist/tools/index.js +7 -5
  123. package/dist/tools/lsp.js +1 -0
  124. package/dist/tools/memory.d.ts +7 -2
  125. package/dist/tools/memory.js +28 -43
  126. package/dist/tools/server.js +4 -0
  127. package/dist/tools/tool-search.d.ts +1 -1
  128. package/dist/tools/tool-search.js +23 -3
  129. package/dist/tui/model-switch.d.ts +7 -0
  130. package/dist/tui/model-switch.js +15 -5
  131. package/dist/tui-ink/app.d.ts +3 -1
  132. package/dist/tui-ink/app.js +672 -243
  133. package/dist/tui-ink/approval/approval-dialog.js +37 -1
  134. package/dist/tui-ink/approval/diff-view.js +4 -4
  135. package/dist/tui-ink/approval/select.js +2 -2
  136. package/dist/tui-ink/composer-buffer.d.ts +55 -0
  137. package/dist/tui-ink/composer-buffer.js +317 -0
  138. package/dist/tui-ink/feedback-dialog.js +2 -2
  139. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  140. package/dist/tui-ink/footer.d.ts +1 -0
  141. package/dist/tui-ink/footer.js +7 -2
  142. package/dist/tui-ink/input-box.d.ts +36 -7
  143. package/dist/tui-ink/input-box.js +229 -157
  144. package/dist/tui-ink/markdown.js +11 -9
  145. package/dist/tui-ink/message-list.d.ts +5 -1
  146. package/dist/tui-ink/message-list.js +15 -2
  147. package/dist/tui-ink/model-picker.d.ts +61 -4
  148. package/dist/tui-ink/model-picker.js +464 -128
  149. package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
  150. package/dist/tui-ink/paste-operation-tracker.js +25 -0
  151. package/dist/tui-ink/plan-confirm.js +1 -1
  152. package/dist/tui-ink/run.d.ts +3 -0
  153. package/dist/tui-ink/run.js +1 -1
  154. package/dist/tui-ink/subagent-view.d.ts +34 -1
  155. package/dist/tui-ink/subagent-view.js +82 -1
  156. package/dist/tui-ink/theme.d.ts +19 -2
  157. package/dist/tui-ink/theme.js +25 -8
  158. package/dist/tui-ink/welcome.d.ts +3 -2
  159. package/dist/tui-ink/welcome.js +10 -6
  160. package/dist/types.d.ts +8 -31
  161. package/dist/types.js +1 -1
  162. package/dist/variant/thinking-level.d.ts +2 -2
  163. package/dist/variant/thinking-level.js +2 -1
  164. package/dist/variant/variant-resolver.d.ts +22 -0
  165. package/dist/variant/variant-resolver.js +43 -0
  166. package/package.json +6 -2
  167. package/dist/tools/task.d.ts +0 -2
  168. package/dist/tools/task.js +0 -58
@@ -6,15 +6,76 @@ import { appendFileSync } from "node:fs";
6
6
  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
- import { bareImageFilenameFromPaste, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
9
+ import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
10
10
  import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
11
+ import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, hasActiveComposerPastes, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
11
12
  import { isKeyReleaseEvent } from "./key-events.js";
13
+ import { PasteOperationTracker } from "./paste-operation-tracker.js";
12
14
  import { stripTerminalMouseSequences } from "./terminal-mouse.js";
13
15
  import { submitPayloadFingerprint } from "./submit-dedupe.js";
14
16
  export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
15
- import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
17
+ import { shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
16
18
  import { imageDisplayLabel, stripInlineImageLabels } from "../tui/image-display.js";
17
- const MIN_VISIBLE_LINES = 3;
19
+ export function resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions = true) {
20
+ return !allowWorkspaceMentions || isSlashContext ? null : findAtContext(text, cursor);
21
+ }
22
+ export function createComposerDraftSnapshot(input) {
23
+ return {
24
+ buffer: cloneComposerBuffer(input.buffer),
25
+ attachments: input.attachments.map((attachment) => ({ ...attachment })),
26
+ imageLabelStartOverride: input.imageLabelStartOverride,
27
+ };
28
+ }
29
+ /**
30
+ * Browse persisted history without turning marker-shaped text into live paste
31
+ * tokens, while preserving the exact unsent draft in memory for the final
32
+ * Down transition back out of history.
33
+ */
34
+ export function stepComposerHistory(state, history, direction) {
35
+ const plainDraft = state.draftSnapshot
36
+ ? {
37
+ text: state.draftSnapshot.buffer.text,
38
+ images: state.draftSnapshot.attachments,
39
+ ...(state.draftSnapshot.imageLabelStartOverride !== null
40
+ ? { imageDisplayStart: state.draftSnapshot.imageLabelStartOverride }
41
+ : {}),
42
+ }
43
+ : { text: "", images: [] };
44
+ const result = stepHistory({ history, index: state.historyIndex, draft: plainDraft }, direction, {
45
+ text: state.buffer.text,
46
+ images: state.attachments,
47
+ ...(state.imageLabelStartOverride !== null
48
+ ? { imageDisplayStart: state.imageLabelStartOverride }
49
+ : {}),
50
+ });
51
+ if (!result.changed)
52
+ return { ...state, changed: false };
53
+ let draftSnapshot = state.draftSnapshot;
54
+ if (state.historyIndex === null && result.index !== null && draftSnapshot === null) {
55
+ draftSnapshot = createComposerDraftSnapshot({
56
+ buffer: state.buffer,
57
+ attachments: state.attachments,
58
+ imageLabelStartOverride: state.imageLabelStartOverride,
59
+ });
60
+ }
61
+ if (result.index === null && draftSnapshot) {
62
+ const restored = createComposerDraftSnapshot(draftSnapshot);
63
+ return {
64
+ ...restored,
65
+ historyIndex: null,
66
+ draftSnapshot: null,
67
+ changed: true,
68
+ };
69
+ }
70
+ return {
71
+ buffer: createComposerBuffer(result.text),
72
+ attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
73
+ imageLabelStartOverride: result.imageDisplayStart ?? null,
74
+ historyIndex: result.index,
75
+ draftSnapshot: result.index === null ? null : draftSnapshot,
76
+ changed: true,
77
+ };
78
+ }
18
79
  const MAX_VISIBLE_LINES = 6;
19
80
  const PADDING_X = 1;
20
81
  const PROMPT = " > ";
@@ -47,12 +108,6 @@ export function isCtrlLetterInput(input, key, letter) {
47
108
  export function isCtrlCInput(input, key) {
48
109
  return isCtrlLetterInput(input, key, "c");
49
110
  }
50
- export function shouldUseLineComposerFrame(_background) {
51
- return true;
52
- }
53
- export function composerSurfaceBackground(lineFrame, background, inputBg) {
54
- return lineFrame ? background : inputBg;
55
- }
56
111
  export function shouldUseHardwareComposerCursor(env = process.env) {
57
112
  return env.BUBBLE_HARDWARE_CURSOR === "1";
58
113
  }
@@ -64,16 +119,10 @@ export function composerVerticalArrowDirection(key) {
64
119
  return undefined;
65
120
  }
66
121
  export function resolveSoftwareCursorCellStyle(input) {
67
- if (input.visible) {
68
- return {
69
- backgroundColor: input.cursorBackground,
70
- color: input.cursorForeground,
71
- };
72
- }
73
- return {
74
- backgroundColor: input.rowBackground,
75
- color: input.textColor,
76
- };
122
+ // Inverse video swaps the cell's real foreground/background at the terminal
123
+ // level, so the cursor block stays visible on any terminal palette without
124
+ // guessing a contrasting pair.
125
+ return { inverse: input.visible, color: input.textColor };
77
126
  }
78
127
  /**
79
128
  * Split a composer line around the cursor so the cell under it can render as
@@ -249,6 +298,9 @@ export function resolveInkEnterIntent(input, key) {
249
298
  return "newline";
250
299
  return "submit";
251
300
  }
301
+ export function shouldDeferComposerCommitWhilePastePending(hasPendingPaste, enterIntent, key) {
302
+ return hasPendingPaste && (enterIntent === "submit" || key.tab === true);
303
+ }
252
304
  export function insertNewlineAtCursor(text, cursor) {
253
305
  const clampedCursor = Math.max(0, Math.min(text.length, cursor));
254
306
  return {
@@ -333,49 +385,69 @@ export function resolveComposerEditAction(input, key) {
333
385
  return "delete-line-end";
334
386
  return null;
335
387
  }
336
- export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDownAtBottom, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
388
+ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDownAtBottom, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], allowedSlashCommands, allowWorkspaceMentions = true, allowImageAttachments = true, terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
337
389
  const theme = useTheme();
338
390
  const width = terminalColumns;
339
391
  const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
340
392
  const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
341
- const [text, setText] = useState("");
342
- const [cursor, setCursor] = useState(0);
393
+ const [composerBuffer, setComposerBuffer] = useState(() => createComposerBuffer());
394
+ const composerBufferRef = useRef(composerBuffer);
395
+ composerBufferRef.current = composerBuffer;
396
+ const updateComposerBuffer = React.useCallback((transition) => {
397
+ const next = transition(composerBufferRef.current);
398
+ composerBufferRef.current = next;
399
+ setComposerBuffer(next);
400
+ return next;
401
+ }, []);
402
+ const { text, cursor } = composerBuffer;
343
403
  const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
344
404
  const [selectedIndex, setSelectedIndex] = useState(0);
345
405
  const [projectFiles, setProjectFiles] = useState(null);
346
406
  const [attachments, setAttachments] = useState([]);
347
407
  const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
348
- const [pastedContentRefs, setPastedContentRefs] = useState([]);
349
408
  const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
350
409
  const [historyIndex, setHistoryIndex] = useState(null);
351
- const historyDraftRef = useRef("");
410
+ const historyDraftRef = useRef(null);
352
411
  const historyScopeRef = useRef(historyScope);
412
+ const historyScopeKey = `${historyScope.cwd}\0${historyScope.sessionFile ?? ""}`;
413
+ const previousHistoryScopeKeyRef = useRef(historyScopeKey);
414
+ const draftIdentityEpochRef = useRef(0);
415
+ const pasteOperationsRef = useRef(new PasteOperationTracker());
353
416
  const submittedPayloadFingerprintRef = useRef(null);
354
417
  const loadingFilesRef = useRef(false);
355
- const nextPastedContentIndexRef = useRef(1);
356
418
  // Kept equal to attachments.length so a synchronous multi-image paste loop
357
419
  // assigns each image its correct (distinct) inline label index.
358
420
  const attachmentCountRef = useRef(0);
359
- // Paste and the keystrokes that follow can arrive inside the same stdin chunk
360
- // and dispatch within one discreteUpdates batch. If the Enter that a user
361
- // typed after a paste fires before React commits the paste-driven setState,
362
- // useInput's Enter branch reads stale `text` and submits without the paste.
363
- // This ref flips synchronously at paste-start and clears after the paste
364
- // commit has been flushed — useInput's Enter handler bails while it's set.
365
- const pastePendingRef = useRef(false);
366
421
  historyScopeRef.current = historyScope;
422
+ const invalidateDraftAsyncWork = React.useCallback(() => {
423
+ draftIdentityEpochRef.current += 1;
424
+ pasteOperationsRef.current.invalidateAll();
425
+ }, []);
367
426
  const ensureImageLabelStart = React.useCallback(() => {
368
427
  setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
369
428
  }, [nextImageLabelStart]);
370
429
  useEffect(() => {
430
+ const scopeChanged = previousHistoryScopeKeyRef.current !== historyScopeKey;
431
+ previousHistoryScopeKeyRef.current = historyScopeKey;
371
432
  setHistory(loadHistoryEntriesSync({ scope: historyScope }));
372
433
  setHistoryIndex(null);
373
434
  setImageLabelStartOverride(null);
374
- historyDraftRef.current = "";
375
- }, [historyScope]);
435
+ historyDraftRef.current = null;
436
+ if (scopeChanged) {
437
+ updateComposerBuffer(() => createComposerBuffer());
438
+ setAttachments([]);
439
+ attachmentCountRef.current = 0;
440
+ setProjectFiles(null);
441
+ loadingFilesRef.current = false;
442
+ }
443
+ invalidateDraftAsyncWork();
444
+ }, [historyScope, historyScopeKey, invalidateDraftAsyncWork, updateComposerBuffer]);
445
+ useEffect(() => () => {
446
+ invalidateDraftAsyncWork();
447
+ }, [invalidateDraftAsyncWork]);
376
448
  const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
377
449
  const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
378
- const atContext = useMemo(() => (isSlashContext ? null : findAtContext(text, cursor)), [text, cursor, isSlashContext]);
450
+ const atContext = useMemo(() => resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions), [allowWorkspaceMentions, text, cursor, isSlashContext]);
379
451
  useEffect(() => {
380
452
  if (!atContext || projectFiles !== null || loadingFilesRef.current)
381
453
  return;
@@ -403,6 +475,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
403
475
  commands.set(command.name, command);
404
476
  }
405
477
  for (const command of slashRegistry.list()) {
478
+ if (allowedSlashCommands && !allowedSlashCommands.has(command.name))
479
+ continue;
406
480
  if (!commands.has(command.name))
407
481
  commands.set(command.name, command);
408
482
  }
@@ -411,24 +485,28 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
411
485
  name: command.name,
412
486
  description: command.description,
413
487
  }));
414
- const skillSuggestions = (skillRegistry?.summaries() ?? []).map((skill) => ({
488
+ const skillSuggestions = (allowedSlashCommands ? [] : (skillRegistry?.summaries() ?? [])).map((skill) => ({
415
489
  type: "skill",
416
490
  name: skill.name,
417
491
  description: skill.description,
418
492
  }));
419
493
  const all = [...commandSuggestions, ...skillSuggestions];
420
494
  return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
421
- }, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
495
+ }, [allowedSlashCommands, isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
422
496
  const knownSlashCommandNames = useMemo(() => {
423
497
  const names = new Set();
424
498
  for (const command of localSlashCommands)
425
499
  names.add(command.name);
426
- for (const command of slashRegistry.list())
427
- names.add(command.name);
428
- for (const skill of skillRegistry?.summaries() ?? [])
429
- names.add(skill.name);
500
+ for (const command of slashRegistry.list()) {
501
+ if (!allowedSlashCommands || allowedSlashCommands.has(command.name))
502
+ names.add(command.name);
503
+ }
504
+ if (!allowedSlashCommands) {
505
+ for (const skill of skillRegistry?.summaries() ?? [])
506
+ names.add(skill.name);
507
+ }
430
508
  return names;
431
- }, [skillRegistry, localSlashCommands]);
509
+ }, [allowedSlashCommands, skillRegistry, localSlashCommands]);
432
510
  const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
433
511
  const fileSuggestions = useMemo(() => {
434
512
  if (!atContext || !projectFiles)
@@ -450,14 +528,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
450
528
  const insertTextAtCursor = React.useCallback((insertion) => {
451
529
  if (!insertion)
452
530
  return;
453
- setText((prev) => {
454
- const c = cursor;
455
- const before = prev.slice(0, c);
456
- const after = prev.slice(c);
457
- return before + insertion + after;
458
- });
459
- setCursor((c) => c + insertion.length);
460
- }, [cursor]);
531
+ updateComposerBuffer((current) => insertComposerText(current, insertion));
532
+ }, [updateComposerBuffer]);
461
533
  const addAttachment = React.useCallback((att) => {
462
534
  const base = imageLabelStartOverride ?? nextImageLabelStart;
463
535
  const index = attachmentCountRef.current;
@@ -476,8 +548,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
476
548
  // terminal has nothing textual to deliver. Probe the clipboard; if it yields
477
549
  // an image, treat the paste as an image attachment. macOS only — Linux/Win
478
550
  // terminals don't reliably emit empty pastes on image-only clipboards.
479
- const tryClipboardImage = React.useCallback(async () => {
551
+ const tryClipboardImage = React.useCallback(async (expectedDraftEpoch) => {
480
552
  const { attachment, error } = await ingestClipboardImage();
553
+ if (draftIdentityEpochRef.current !== expectedDraftEpoch)
554
+ return false;
481
555
  if (attachment) {
482
556
  addAttachment(attachment);
483
557
  return true;
@@ -488,12 +562,13 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
488
562
  return false;
489
563
  }, [addAttachment, notice]);
490
564
  usePaste((pasted) => {
491
- pastePendingRef.current = true;
565
+ const pasteOperationId = pasteOperationsRef.current.begin();
566
+ const pasteDraftEpoch = draftIdentityEpochRef.current;
492
567
  // Clear the ref after React has committed the paste-driven setState.
493
568
  // setTimeout with 0 runs after the current discreteUpdates batch flushes.
494
569
  const clearPending = () => {
495
570
  setTimeout(() => {
496
- pastePendingRef.current = false;
571
+ pasteOperationsRef.current.finish(pasteOperationId);
497
572
  }, 0);
498
573
  };
499
574
  // Strip orphaned focus-event tails that can appear if focus reporting
@@ -506,10 +581,29 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
506
581
  .replace(/\x1b\[I$/, "")
507
582
  .replace(/\x1b\[O$/, "")
508
583
  .replace(/\r\n?/g, "\n");
584
+ if (!allowImageAttachments) {
585
+ // A constrained text-only runtime must not probe the clipboard or read
586
+ // pasted filesystem paths. Do not leak image path strings as chat text.
587
+ const containsImagePath = clean.length > 0 && (bareImageFilenameFromPaste(clean) !== null
588
+ || extractImagePathTokens(clean).length > 0);
589
+ if (clean.length === 0 || containsImagePath) {
590
+ notice("image attachments are disabled in this runtime session");
591
+ }
592
+ else {
593
+ if (shouldCollapsePastedContent(clean)) {
594
+ updateComposerBuffer((current) => insertComposerPaste(current, clean));
595
+ }
596
+ else {
597
+ insertTextAtCursor(clean);
598
+ }
599
+ }
600
+ clearPending();
601
+ return;
602
+ }
509
603
  // Empty paste on macOS usually means "Cmd+V with an image on the clipboard".
510
604
  if (clean.length === 0) {
511
605
  if (process.platform === "darwin") {
512
- void tryClipboardImage().finally(clearPending);
606
+ void tryClipboardImage(pasteDraftEpoch).finally(clearPending);
513
607
  }
514
608
  else {
515
609
  clearPending();
@@ -522,10 +616,11 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
522
616
  // quietly.
523
617
  const bareName = bareImageFilenameFromPaste(clean);
524
618
  if (bareName && process.platform === "darwin") {
525
- void tryClipboardImage()
619
+ void tryClipboardImage(pasteDraftEpoch)
526
620
  .then((attached) => {
527
- if (!attached)
621
+ if (draftIdentityEpochRef.current === pasteDraftEpoch && !attached) {
528
622
  insertTextAtCursor(clean);
623
+ }
529
624
  })
530
625
  .finally(clearPending);
531
626
  return;
@@ -538,9 +633,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
538
633
  if (imageTokens.length === 0) {
539
634
  // Plain text paste — insert into the input at the cursor.
540
635
  if (shouldCollapsePastedContent(clean)) {
541
- const marker = createPastedContentMarker(clean, nextPastedContentIndexRef.current++);
542
- setPastedContentRefs((prev) => [...prev, { marker, content: clean }]);
543
- insertTextAtCursor(marker);
636
+ updateComposerBuffer((current) => insertComposerPaste(current, clean));
544
637
  }
545
638
  else {
546
639
  insertTextAtCursor(clean);
@@ -550,6 +643,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
550
643
  }
551
644
  const handle = async () => {
552
645
  const results = await Promise.all(imageTokens.map((t) => ingestImagePath(t)));
646
+ if (draftIdentityEpochRef.current !== pasteDraftEpoch)
647
+ return;
553
648
  const successful = [];
554
649
  const errors = [];
555
650
  for (let i = 0; i < results.length; i++) {
@@ -567,10 +662,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
567
662
  if (successful.length === 0 &&
568
663
  process.platform === "darwin" &&
569
664
  imageTokens.some(isScreenshotTempPath)) {
570
- const clipOk = await tryClipboardImage();
665
+ const clipOk = await tryClipboardImage(pasteDraftEpoch);
571
666
  if (clipOk)
572
667
  return;
573
668
  }
669
+ if (draftIdentityEpochRef.current !== pasteDraftEpoch)
670
+ return;
574
671
  for (const att of successful)
575
672
  addAttachment(att);
576
673
  const nonImageLines = tokens.filter((t) => !isImageFilePath(t));
@@ -589,20 +686,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
589
686
  const applyFileSuggestion = (selectedPath) => {
590
687
  if (!atContext)
591
688
  return;
592
- const before = text.slice(0, atContext.start);
593
- const after = text.slice(atContext.end);
594
689
  const insert = `@${selectedPath} `;
595
- const newText = before + insert + after;
596
- setText(newText);
597
- setCursor(before.length + insert.length);
690
+ updateComposerBuffer((current) => replaceComposerRange(current, atContext.start, atContext.end, insert));
598
691
  setSelectedIndex(0);
599
692
  };
600
- const submitInput = (submittedText, target = "submit") => {
693
+ const submitInput = (submittedBuffer, target = "submit") => {
601
694
  const labelStartForSubmit = imageLabelStartOverride ?? nextImageLabelStart;
602
695
  const inlineLabels = attachments.map((_, index) => imageDisplayLabel(labelStartForSubmit + index));
603
696
  // Text-paste markers expand to their content (not replayable); image labels
604
697
  // are a composer-only affordance stripped here (replayable via attachments).
605
- const pasteExpanded = expandPastedContentMarkers(submittedText, pastedContentRefs);
698
+ const pasteExpanded = expandComposerBuffer(submittedBuffer);
606
699
  const expandedText = stripInlineImageLabels(pasteExpanded, inlineLabels);
607
700
  if (expandedText.trim().length === 0 && attachments.length === 0)
608
701
  return;
@@ -626,7 +719,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
626
719
  // A collapsed text-paste marker cannot be safely replayed once its
627
720
  // in-memory reference is gone; skip those. Image-label stripping is fine to
628
721
  // replay (the attachments are stored on the history entry).
629
- if (pasteExpanded === submittedText && (expandedText.trim().length > 0 || attachments.length > 0)) {
722
+ if (!hasActiveComposerPastes(submittedBuffer) && (expandedText.trim().length > 0 || attachments.length > 0)) {
630
723
  const historyEntry = {
631
724
  text: expandedText,
632
725
  images: attachments,
@@ -640,26 +733,23 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
640
733
  return nextHistory;
641
734
  });
642
735
  }
643
- setText("");
644
- setCursor(0);
736
+ updateComposerBuffer(() => createComposerBuffer());
645
737
  setSelectedIndex(0);
646
738
  setAttachments([]);
647
739
  attachmentCountRef.current = 0;
648
740
  setImageLabelStartOverride(null);
649
- setPastedContentRefs([]);
650
- nextPastedContentIndexRef.current = 1;
651
741
  setHistoryIndex(null);
652
- historyDraftRef.current = "";
742
+ historyDraftRef.current = null;
743
+ invalidateDraftAsyncWork();
653
744
  };
654
- const applySlashEnterAction = (submittedText) => {
655
- const action = resolveSlashEnterAction(submittedText, slashSuggestions, selectedIndex);
745
+ const applySlashEnterAction = (submittedBuffer) => {
746
+ const action = resolveSlashEnterAction(submittedBuffer.text, slashSuggestions, selectedIndex);
656
747
  if (action.kind === "submit") {
657
- submitInput(submittedText);
748
+ submitInput(submittedBuffer);
658
749
  return true;
659
750
  }
660
751
  if (action.kind === "complete") {
661
- setText(action.text);
662
- setCursor(action.text.length);
752
+ updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, action.text));
663
753
  setSelectedIndex(0);
664
754
  return true;
665
755
  }
@@ -689,18 +779,19 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
689
779
  catch { }
690
780
  }
691
781
  const enterIntent = resolveInkEnterIntent(input, key);
782
+ if (shouldDeferComposerCommitWhilePastePending(pasteOperationsRef.current.hasPending, enterIntent, key)) {
783
+ return;
784
+ }
692
785
  if (enterIntent === "newline") {
693
- const next = insertNewlineAtCursor(text, cursor);
694
- setText(next.text);
695
- setCursor(next.cursor);
786
+ updateComposerBuffer((current) => insertComposerText(current, "\n"));
696
787
  setSelectedIndex(0);
697
788
  return;
698
789
  }
699
790
  if (enterIntent === "submit" && input && /[\r\n]/.test(input)) {
700
791
  const beforeReturn = input.split(/[\r\n]/)[0] ?? "";
701
- const nextText = text.slice(0, cursor) + beforeReturn + text.slice(cursor);
792
+ const nextBuffer = insertComposerText(composerBufferRef.current, beforeReturn);
702
793
  if (showSuggestions) {
703
- if (mode === "slash" && navigable && applySlashEnterAction(nextText)) {
794
+ if (mode === "slash" && navigable && applySlashEnterAction(nextBuffer)) {
704
795
  return;
705
796
  }
706
797
  if (mode === "file") {
@@ -712,7 +803,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
712
803
  return;
713
804
  }
714
805
  }
715
- submitInput(nextText);
806
+ submitInput(nextBuffer);
716
807
  return;
717
808
  }
718
809
  const composerArrowDirection = composerVerticalArrowDirection(key);
@@ -733,13 +824,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
733
824
  if (key.return || key.tab) {
734
825
  if (mode === "slash" && navigable) {
735
826
  if (key.return)
736
- applySlashEnterAction(text);
827
+ applySlashEnterAction(composerBufferRef.current);
737
828
  if (key.tab) {
738
829
  const suggestion = slashSuggestions[selectedIndex];
739
830
  if (suggestion) {
740
831
  const newText = `/${suggestion.name} `;
741
- setText(newText);
742
- setCursor(newText.length);
832
+ updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, newText));
743
833
  setSelectedIndex(0);
744
834
  }
745
835
  }
@@ -759,55 +849,44 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
759
849
  // While the agent runs, Tab queues the composer content for the next
760
850
  // turn (Enter steers — handled by the app-level submit routing).
761
851
  if (key.tab && !key.shift && onQueue && !showSuggestions) {
762
- if (pastePendingRef.current)
763
- return;
764
- submitInput(text, "queue");
852
+ submitInput(composerBufferRef.current, "queue");
765
853
  return;
766
854
  }
767
855
  if (enterIntent === "submit") {
768
856
  // A paste is still mid-flight — dropping this Enter avoids submitting
769
857
  // an input state that doesn't yet include the paste.
770
- if (pastePendingRef.current)
771
- return;
772
- submitInput(text);
858
+ submitInput(composerBufferRef.current);
773
859
  return;
774
860
  }
775
861
  const editAction = resolveComposerEditAction(input, key);
776
862
  if (editAction) {
777
863
  if (editAction === "word-left") {
778
- setCursor(previousWordBoundary(text, cursor));
864
+ updateComposerBuffer((current) => moveComposerCursor(current, previousWordBoundary(current.text, current.cursor), "left"));
779
865
  }
780
866
  else if (editAction === "word-right") {
781
- setCursor(nextWordBoundary(text, cursor));
867
+ updateComposerBuffer((current) => moveComposerCursor(current, nextWordBoundary(current.text, current.cursor), "right"));
782
868
  }
783
869
  else if (editAction === "line-start") {
784
- setCursor(lineStartBoundary(text, cursor));
870
+ updateComposerBuffer((current) => moveComposerCursor(current, lineStartBoundary(current.text, current.cursor), "left"));
785
871
  }
786
872
  else if (editAction === "line-end") {
787
- setCursor(lineEndBoundary(text, cursor));
873
+ updateComposerBuffer((current) => moveComposerCursor(current, lineEndBoundary(current.text, current.cursor), "right"));
788
874
  }
789
875
  else if (editAction === "delete-line-start") {
790
- const next = deleteToLineStart(text, cursor);
791
- setText(next.text);
792
- setCursor(next.cursor);
876
+ updateComposerBuffer((current) => replaceComposerRange(current, lineStartBoundary(current.text, current.cursor), current.cursor));
793
877
  }
794
878
  else {
795
- const next = deleteToLineEnd(text, cursor);
796
- setText(next.text);
797
- setCursor(next.cursor);
879
+ updateComposerBuffer((current) => replaceComposerRange(current, current.cursor, lineEndBoundary(current.text, current.cursor)));
798
880
  }
799
881
  setSelectedIndex(0);
800
882
  return;
801
883
  }
802
884
  if (key.backspace) {
803
- if (cursor > 0) {
804
- const before = text.slice(0, cursor - 1);
805
- const after = text.slice(cursor);
806
- setText(before + after);
807
- setCursor(cursor - 1);
885
+ if (composerBufferRef.current.cursor > 0) {
886
+ updateComposerBuffer((current) => deleteComposerBackward(current));
808
887
  setSelectedIndex(0);
809
888
  }
810
- else if (attachments.length > 0) {
889
+ else if (attachmentCountRef.current > 0) {
811
890
  // Backspace at position 0 drops the most recent attachment so users
812
891
  // can undo a misfired paste without submitting the message.
813
892
  setAttachments((prev) => {
@@ -821,21 +900,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
821
900
  return;
822
901
  }
823
902
  if (key.delete) {
824
- if (cursor < text.length) {
825
- const next = deleteAtCursor(text, cursor);
826
- setText(next.text);
827
- setCursor(next.cursor);
828
- setSelectedIndex(0);
829
- }
903
+ updateComposerBuffer((current) => deleteComposerForward(current));
904
+ setSelectedIndex(0);
830
905
  return;
831
906
  }
832
907
  if (key.leftArrow) {
833
- setCursor(Math.max(0, cursor - 1));
908
+ updateComposerBuffer((current) => moveComposerCursor(current, current.cursor - 1, "left"));
834
909
  setSelectedIndex(0);
835
910
  return;
836
911
  }
837
912
  if (key.rightArrow) {
838
- setCursor(Math.min(text.length, cursor + 1));
913
+ updateComposerBuffer((current) => moveComposerCursor(current, current.cursor + 1, "right"));
839
914
  setSelectedIndex(0);
840
915
  return;
841
916
  }
@@ -854,10 +929,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
854
929
  const printable = input.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
855
930
  if (!printable)
856
931
  return;
857
- const before = text.slice(0, cursor);
858
- const after = text.slice(cursor);
859
- setText(before + printable + after);
860
- setCursor(cursor + printable.length);
932
+ updateComposerBuffer((current) => insertComposerText(current, printable));
861
933
  setSelectedIndex(0);
862
934
  }
863
935
  });
@@ -895,18 +967,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
895
967
  if (lastDraftEpochRef.current === draftEpoch)
896
968
  return;
897
969
  lastDraftEpochRef.current = draftEpoch;
898
- if (!draftText)
970
+ if (draftText === undefined)
899
971
  return;
900
- setText(draftText);
901
- setCursor(draftText.length);
972
+ updateComposerBuffer(() => createComposerBuffer(draftText));
902
973
  setSelectedIndex(0);
903
974
  setAttachments([]);
904
975
  attachmentCountRef.current = 0;
905
976
  setImageLabelStartOverride(null);
906
- setPastedContentRefs([]);
907
- nextPastedContentIndexRef.current = 1;
908
977
  setHistoryIndex(null);
909
- historyDraftRef.current = "";
978
+ historyDraftRef.current = null;
979
+ invalidateDraftAsyncWork();
910
980
  onDraftApplied?.();
911
981
  }, [draftEpoch, draftText, onDraftApplied]);
912
982
  useEffect(() => {
@@ -961,30 +1031,40 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
961
1031
  // bottom edge (Down → next message, then back to the in-progress draft).
962
1032
  const performVerticalArrowRef = useRef(() => { });
963
1033
  performVerticalArrowRef.current = (direction) => {
1034
+ // A paste and the following key can arrive in one stdin batch. Wait for
1035
+ // that paste transition to commit before moving or snapshotting history.
1036
+ if (pasteOperationsRef.current.hasPending)
1037
+ return;
964
1038
  if (direction === "up") {
965
1039
  if (cursorVisualRow > 0) {
966
- setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol)));
1040
+ const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
1041
+ updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
967
1042
  return;
968
1043
  }
969
1044
  }
970
1045
  else {
971
1046
  if (cursorVisualRow < visualLines.length - 1) {
972
- setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol)));
1047
+ const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
1048
+ updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
973
1049
  return;
974
1050
  }
975
1051
  }
976
- const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, { text, images: attachments });
1052
+ const result = stepComposerHistory({
1053
+ buffer: composerBufferRef.current,
1054
+ attachments,
1055
+ imageLabelStartOverride,
1056
+ historyIndex,
1057
+ draftSnapshot: historyDraftRef.current,
1058
+ }, history, direction);
977
1059
  if (result.changed) {
978
- setText(result.text);
979
- setCursor(result.text.length);
980
- setAttachments(result.images ?? []);
981
- attachmentCountRef.current = (result.images ?? []).length;
982
- setImageLabelStartOverride(result.imageDisplayStart ?? null);
983
- setHistoryIndex(result.index);
984
- historyDraftRef.current = result.draft;
1060
+ updateComposerBuffer(() => result.buffer);
1061
+ setAttachments(result.attachments);
1062
+ attachmentCountRef.current = result.attachments.length;
1063
+ setImageLabelStartOverride(result.imageLabelStartOverride);
1064
+ setHistoryIndex(result.historyIndex);
1065
+ historyDraftRef.current = result.draftSnapshot;
985
1066
  setSelectedIndex(0);
986
- setPastedContentRefs([]);
987
- nextPastedContentIndexRef.current = 1;
1067
+ invalidateDraftAsyncWork();
988
1068
  }
989
1069
  else if (direction === "down") {
990
1070
  // At the bottom edge with nothing newer in history: hand Down to the
@@ -995,8 +1075,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
995
1075
  const classifyVerticalArrow = (direction) => {
996
1076
  performVerticalArrowRef.current(direction);
997
1077
  };
998
- const lineFrame = shouldUseLineComposerFrame(theme.background);
999
- const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
1078
+ const minVisibleLines = 1;
1000
1079
  const totalLines = Math.max(visualLines.length, 1);
1001
1080
  const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
1002
1081
  let scrollOffset = 0;
@@ -1130,25 +1209,18 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1130
1209
  });
1131
1210
  // Reference cursorTick so the effect re-runs on the forced render pass.
1132
1211
  void cursorTick;
1133
- const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
1134
- const composerBg = composerSurfaceBackground(lineFrame, theme.background, inputBg);
1135
- const rowBg = lineFrame ? undefined : inputBg;
1136
- const cursorFg = lineFrame ? theme.background : inputBg;
1137
1212
  const cursorCellStyle = resolveSoftwareCursorCellStyle({
1138
1213
  visible: softwareCursorVisible,
1139
- cursorBackground: theme.inputText,
1140
- cursorForeground: cursorFg,
1141
1214
  textColor: theme.inputText,
1142
- rowBackground: rowBg,
1143
1215
  });
1144
1216
  const moreBelow = totalLines - scrollOffset - visibleLines;
1145
1217
  const filledLine = (value) => {
1146
1218
  const visibleWidth = visualWidth(value);
1147
1219
  return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
1148
1220
  };
1149
- 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) => {
1221
+ return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) }), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: theme.background, children: [hasMoreAbove && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
1150
1222
  if (row.kind === "pad") {
1151
- return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
1223
+ return (_jsx(Text, { children: " ".repeat(contentWidth) }, row.key));
1152
1224
  }
1153
1225
  const { text: line, visualIdx } = row;
1154
1226
  const visualLine = visualLines[visualIdx];
@@ -1167,17 +1239,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1167
1239
  });
1168
1240
  const renderedLine = renderedSegments.map((segment) => segment.text).join("");
1169
1241
  const fill = " ".repeat(Math.max(0, lineWidth - visualWidth(renderedLine)));
1170
- return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: composerBg, ref: isCursorLine
1242
+ return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: theme.background, ref: isCursorLine
1171
1243
  ? (el) => {
1172
1244
  cursorLineRef.current = el;
1173
1245
  }
1174
- : undefined, children: [_jsx(Text, { backgroundColor: rowBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1246
+ : undefined, children: [_jsx(Text, { color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1175
1247
  if (segment.kind === "cursor") {
1176
- return (_jsx(Text, { backgroundColor: cursorCellStyle.backgroundColor, color: cursorCellStyle.color, children: segment.text }, index));
1248
+ return (_jsx(Text, { inverse: cursorCellStyle.inverse, color: cursorCellStyle.color, children: segment.text }, index));
1177
1249
  }
1178
- return (_jsx(Text, { backgroundColor: rowBg, color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
1179
- }), _jsx(Text, { backgroundColor: rowBg, children: fill })] }, visualIdx));
1180
- }), 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
1250
+ return (_jsx(Text, { color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
1251
+ }), _jsx(Text, { children: fill })] }, visualIdx));
1252
+ }), hasMoreBelow && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), _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
1181
1253
  .slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
1182
1254
  .map((cmd, visibleIndex) => {
1183
1255
  const i = suggestionOffset + visibleIndex;
@@ -1190,6 +1262,6 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1190
1262
  const i = suggestionOffset + visibleIndex;
1191
1263
  const maxWidth = Math.max(10, Math.min(80, contentWidth - 2));
1192
1264
  const label = s.path.length > maxWidth ? "…" + s.path.slice(-(maxWidth - 1)) : s.path;
1193
- return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, { backgroundColor: "white", color: "black", children: ` ${label} ` })) : (_jsx(Text, { children: ` ${label}` })) }, s.path));
1265
+ return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, { color: theme.accent, bold: true, children: `› ${label}` })) : (_jsx(Text, { children: ` ${label}` })) }, s.path));
1194
1266
  }), fileSuggestions.length > MAX_VISIBLE_SUGGESTIONS && (_jsx(Text, { color: theme.muted, children: `Showing ${suggestionOffset + 1}-${Math.min(suggestionOffset + MAX_VISIBLE_SUGGESTIONS, fileSuggestions.length)} of ${fileSuggestions.length}` }))] }))] }));
1195
1267
  }