@bubblebrain-ai/bubble 0.0.19 → 0.0.20

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 (59) hide show
  1. package/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
  2. package/dist/agent/internal-reminder-sanitizer.js +46 -0
  3. package/dist/agent.d.ts +9 -0
  4. package/dist/agent.js +305 -17
  5. package/dist/approval/controller.d.ts +6 -0
  6. package/dist/approval/controller.js +104 -11
  7. package/dist/debug-trace.js +4 -0
  8. package/dist/feishu/agent-host/run-driver.js +28 -0
  9. package/dist/hooks/config.d.ts +9 -0
  10. package/dist/hooks/config.js +278 -0
  11. package/dist/hooks/controller.d.ts +24 -0
  12. package/dist/hooks/controller.js +254 -0
  13. package/dist/hooks/index.d.ts +6 -0
  14. package/dist/hooks/index.js +4 -0
  15. package/dist/hooks/log.d.ts +14 -0
  16. package/dist/hooks/log.js +54 -0
  17. package/dist/hooks/runner.d.ts +5 -0
  18. package/dist/hooks/runner.js +225 -0
  19. package/dist/hooks/trust.d.ts +37 -0
  20. package/dist/hooks/trust.js +143 -0
  21. package/dist/hooks/types.d.ts +173 -0
  22. package/dist/hooks/types.js +46 -0
  23. package/dist/main.js +32 -0
  24. package/dist/memory/prompts.js +3 -1
  25. package/dist/model-catalog.js +2 -0
  26. package/dist/model-pricing.js +8 -0
  27. package/dist/network/chatgpt-transport.d.ts +0 -1
  28. package/dist/network/chatgpt-transport.js +40 -121
  29. package/dist/network/provider-transport.d.ts +32 -0
  30. package/dist/network/provider-transport.js +265 -0
  31. package/dist/network/retry.d.ts +29 -0
  32. package/dist/network/retry.js +88 -0
  33. package/dist/network/system-proxy.d.ts +18 -0
  34. package/dist/network/system-proxy.js +175 -0
  35. package/dist/provider-anthropic.d.ts +1 -0
  36. package/dist/provider-anthropic.js +127 -52
  37. package/dist/provider-openai-codex.js +19 -29
  38. package/dist/session-log.js +3 -3
  39. package/dist/slash-commands/commands.js +84 -0
  40. package/dist/slash-commands/types.d.ts +2 -0
  41. package/dist/tools/edit-apply.js +63 -3
  42. package/dist/tools/edit.js +4 -4
  43. package/dist/tui/display-history.d.ts +4 -3
  44. package/dist/tui/display-history.js +34 -57
  45. package/dist/tui/display-sanitizer.d.ts +3 -0
  46. package/dist/tui/display-sanitizer.js +38 -0
  47. package/dist/tui/paste-placeholder.d.ts +1 -0
  48. package/dist/tui/paste-placeholder.js +7 -0
  49. package/dist/tui/run.d.ts +2 -0
  50. package/dist/tui/run.js +260 -155
  51. package/dist/tui/trace-groups.js +40 -4
  52. package/dist/tui/wordmark.d.ts +1 -0
  53. package/dist/tui/wordmark.js +56 -54
  54. package/dist/tui-ink/app.js +2 -1
  55. package/dist/tui-ink/trace-groups.js +40 -4
  56. package/dist/tui-opentui/app.js +2 -1
  57. package/dist/tui-opentui/trace-groups.js +40 -4
  58. package/dist/types.d.ts +27 -0
  59. package/package.json +1 -1
package/dist/tui/run.js CHANGED
@@ -10,7 +10,7 @@ import { AgentAbortError } from "../agent.js";
10
10
  import { AgentRunInputQueue } from "../agent/input-controller.js";
11
11
  import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js";
12
12
  import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
13
- import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
13
+ import { createStreamingInternalReminderSanitizer, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
14
14
  import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
15
15
  import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
16
16
  import { calculateUsageCost } from "../model-pricing.js";
@@ -22,7 +22,8 @@ import { registry as slashRegistry } from "../slash-commands/index.js";
22
22
  import { sourceRank } from "../slash-commands/unified.js";
23
23
  import { sidebarMcpRowsFromStates, renderMcpRowMarker } from "./sidebar-mcp.js";
24
24
  import { expandAtMentions, filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
25
- import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
25
+ import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, setUserInputStatus, snapshotDisplayParts, toolCallsFromParts, userInputStatusBadgeLabel, } from "./display-history.js";
26
+ import { sanitizeDisplayMessage, sanitizeDisplayMessages } from "./display-sanitizer.js";
26
27
  import { createMarkdownSyntaxStyle, createSubtleMarkdownSyntaxStyle } from "./markdown-theme.js";
27
28
  import { markdownInlineSegments } from "./markdown-inline.js";
28
29
  import { hashString } from "./render-signature.js";
@@ -41,6 +42,7 @@ import { createFrames } from "./opencode-spinner.js";
41
42
  import { copyTextToClipboard } from "./clipboard.js";
42
43
  import { readGitSidebarState } from "./sidebar-state.js";
43
44
  import { buildImageContentPartsFromLabels, extractImagePathTokens, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
45
+ import { createPastedContentMarker, decodePastedBytes, expandPastedContentMarkers, shouldCollapsePastedContent, } from "./paste-placeholder.js";
44
46
  import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
45
47
  import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
46
48
  import { EscapeConfirmationGate } from "./escape-confirmation.js";
@@ -71,6 +73,8 @@ const DEFAULT_THEME = {
71
73
  info: "#56b6c2",
72
74
  text: "#eeeeee",
73
75
  textMuted: "#808080",
76
+ selectionBg: "#3D5066",
77
+ selectionFg: "#eeeeee",
74
78
  background: "#0a0a0a",
75
79
  backgroundPanel: "#141414",
76
80
  backgroundElement: "#1e1e1e",
@@ -113,6 +117,8 @@ const LIGHT_THEME = {
113
117
  info: "#257E8A",
114
118
  text: "#171717",
115
119
  textMuted: "#6F7377",
120
+ selectionBg: "#B9D4F7",
121
+ selectionFg: "#171717",
116
122
  background: "#FCFCFA",
117
123
  backgroundPanel: "#F6F6F3",
118
124
  backgroundElement: "#ECEDEA",
@@ -186,6 +192,9 @@ const PROMPT_SCANNER_INTERVAL_MS = 80;
186
192
  const SESSION_SIDEBAR_WIDTH = 42;
187
193
  const SESSION_SIDEBAR_AUTO_WIDTH = 120;
188
194
  const PROVIDER_DIALOG_ROWS = 13;
195
+ const PROVIDER_DIALOG_MIN_WIDTH = 56;
196
+ const PROVIDER_DIALOG_MAX_WIDTH = 84;
197
+ const PROVIDER_DIALOG_ROW_RESERVED_WIDTH = 10;
189
198
  const QUESTION_MAX_TABS = 4;
190
199
  const QUESTION_MAX_OPTIONS = 10;
191
200
  const QUESTION_MAX_CONFIRM_ROWS = 3;
@@ -221,6 +230,7 @@ const HOME_TIPS = [
221
230
  "Use /compact to summarize long sessions near context limits",
222
231
  "Shift+Enter or Ctrl+J inserts a newline in your prompt",
223
232
  ];
233
+ const SELECTABLE_TEXT_TAGS = new Set(["text", "textarea", "code", "markdown", "diff", "input"]);
224
234
  function h(tag, props, ...children) {
225
235
  const allProps = props ?? {};
226
236
  const childList = children.length > 0 ? children : allProps.children !== undefined ? [allProps.children] : [];
@@ -232,6 +242,14 @@ function h(tag, props, ...children) {
232
242
  }
233
243
  const element = createElement(tag);
234
244
  const { children: _children, ...rest } = allProps;
245
+ // Without explicit selection colors OpenTUI inverts fg/bg; with our
246
+ // transparent backgrounds that degrades to black-on-black on light themes.
247
+ if (SELECTABLE_TEXT_TAGS.has(tag)) {
248
+ if (rest.selectionBg === undefined)
249
+ rest.selectionBg = theme.selectionBg;
250
+ if (rest.selectionFg === undefined)
251
+ rest.selectionFg = theme.selectionFg;
252
+ }
235
253
  spread(element, rest, false);
236
254
  if (childList.length === 1)
237
255
  insert(element, childList[0]);
@@ -239,6 +257,27 @@ function h(tag, props, ...children) {
239
257
  insert(element, childList);
240
258
  return element;
241
259
  }
260
+ // OpenTUI hardcodes updateCursor=true for mouse-driven selection, so dragging
261
+ // a selection yanks the editor cursor to the drag focus. Keep plain clicks
262
+ // (empty selection) positioning the cursor and keyboard selection intact, but
263
+ // freeze the cursor while a real range is being dragged.
264
+ function preserveCursorOnMouseSelection(ref) {
265
+ const editor = ref?.editorView;
266
+ if (!editor || editor.__bubbleSelectionCursorPatch)
267
+ return;
268
+ editor.__bubbleSelectionCursorPatch = true;
269
+ for (const method of ["setLocalSelection", "updateLocalSelection"]) {
270
+ const original = editor[method]?.bind(editor);
271
+ if (!original)
272
+ continue;
273
+ editor[method] = (anchorX, anchorY, focusX, focusY, bg, fg, updateCursor, followCursor) => {
274
+ const keyboardDriven = ref?._keyboardSelectionActive === true;
275
+ const emptySelection = anchorX === focusX && anchorY === focusY;
276
+ const allowCursorMove = keyboardDriven || emptySelection;
277
+ return original(anchorX, anchorY, focusX, focusY, bg, fg, allowCursorMove ? updateCursor : false, allowCursorMove ? followCursor : false);
278
+ };
279
+ }
280
+ }
242
281
  function isDestroyedRenderable(ref) {
243
282
  return !ref || ref.isDestroyed === true;
244
283
  }
@@ -438,6 +477,11 @@ function OpenTuiApp(props) {
438
477
  let promptHistory = initialPromptHistory(displayMessages);
439
478
  let nextImageAttachmentIndex = nextImageLabelIndex(displayMessages);
440
479
  const pendingImageAttachments = new Map();
480
+ // Long pastes are collapsed to "[Pasted text #N +M lines]" in the composer
481
+ // and expanded back to the full content when the message is submitted,
482
+ // mirroring how image attachments use "[Image #N]" labels.
483
+ const pendingPastedTexts = new Map();
484
+ let nextPastedTextIndex = 1;
441
485
  let composerImageResolutionSeq = 0;
442
486
  let applyingComposerImageReplacement = false;
443
487
  let promptHistoryIndex;
@@ -467,7 +511,7 @@ function OpenTuiApp(props) {
467
511
  let copyToastRoot;
468
512
  let copyToastText;
469
513
  const [sessionActive, setSessionActive] = createSignal(false);
470
- const [sidebarMode, setSidebarModeState] = createSignal("auto");
514
+ const [sidebarMode, setSidebarModeState] = createSignal("collapsed");
471
515
  const [sidebarTick, setSidebarTick] = createSignal(0);
472
516
  // Sidebar MCP section collapsed state. Persisted across sidebarTick bumps,
473
517
  // only reset on actual mount. Collapse toggle exposed when > 2 servers.
@@ -502,7 +546,6 @@ function OpenTuiApp(props) {
502
546
  let providerDialogModelItems;
503
547
  let providerDialogModelRefreshId = 0;
504
548
  let previousPickerForKey;
505
- let homePromptRef;
506
549
  let sessionPromptRef;
507
550
  let scrollbox;
508
551
  let transcriptScrollFollowing = true;
@@ -518,7 +561,6 @@ function OpenTuiApp(props) {
518
561
  defaultWritesExpanded: false,
519
562
  };
520
563
  let dock;
521
- let homeComposerShell;
522
564
  let sessionComposerShell;
523
565
  const promptScannerSyncs = new Set();
524
566
  let approvalRoot;
@@ -619,9 +661,7 @@ function OpenTuiApp(props) {
619
661
  const sidebarFileAdditions = [];
620
662
  const sidebarFileDeletions = [];
621
663
  let sidebarFileSection;
622
- const activePrompt = () => isHomeSurfaceActive()
623
- ? homePromptRef ?? sessionPromptRef
624
- : sessionPromptRef ?? homePromptRef;
664
+ const activePrompt = () => sessionPromptRef;
625
665
  function setPromptText(value) {
626
666
  promptText = value;
627
667
  const prompt = activePrompt();
@@ -703,7 +743,6 @@ function OpenTuiApp(props) {
703
743
  return true;
704
744
  }
705
745
  function blurInputsForModal() {
706
- homePromptRef?.blur();
707
746
  sessionPromptRef?.blur();
708
747
  questionCustomInput?.blur();
709
748
  providerDialogInput?.blur();
@@ -760,9 +799,7 @@ function OpenTuiApp(props) {
760
799
  activePrompt()?.focus();
761
800
  }, 0);
762
801
  }
763
- const activeComposerShell = () => isHomeSurfaceActive()
764
- ? homeComposerShell ?? sessionComposerShell
765
- : sessionComposerShell ?? homeComposerShell;
802
+ const activeComposerShell = () => sessionComposerShell;
766
803
  onCleanup(() => {
767
804
  uiDisposed = true;
768
805
  if (copyToastClearTimer)
@@ -894,6 +931,23 @@ function OpenTuiApp(props) {
894
931
  return dimensions().width > SESSION_SIDEBAR_AUTO_WIDTH;
895
932
  };
896
933
  const contentWidth = () => Math.max(20, dimensions().width - (sidebarVisible() ? SESSION_SIDEBAR_WIDTH : 0) - 4);
934
+ const liveTerminalDimensions = () => {
935
+ const reactive = dimensions();
936
+ // Some terminal split-pane flows leave OpenTUI's resize signal stale. Node's
937
+ // TTY size is sampled on demand, so use it for modal geometry when present.
938
+ const stdoutWidth = process.stdout.columns;
939
+ const stdoutHeight = process.stdout.rows;
940
+ const width = Number.isFinite(stdoutWidth) && stdoutWidth && stdoutWidth > 0
941
+ ? stdoutWidth
942
+ : reactive.width;
943
+ const height = Number.isFinite(stdoutHeight) && stdoutHeight && stdoutHeight > 0
944
+ ? stdoutHeight
945
+ : reactive.height;
946
+ return {
947
+ width: Math.max(1, Math.floor(width)),
948
+ height: Math.max(1, Math.floor(height)),
949
+ };
950
+ };
897
951
  const bumpSidebar = () => {
898
952
  setSidebarTick((value) => value + 1);
899
953
  syncSidebarContext();
@@ -1090,7 +1144,6 @@ function OpenTuiApp(props) {
1090
1144
  if (!safeSetText(footerModeBadge, footerModeText()))
1091
1145
  footerModeBadge = undefined;
1092
1146
  }
1093
- safeRequestRender(homeComposerShell);
1094
1147
  safeRequestRender(sessionComposerShell);
1095
1148
  safeRequestRender(rootBox);
1096
1149
  }
@@ -1119,7 +1172,6 @@ function OpenTuiApp(props) {
1119
1172
  if (!safeSetText(label, promptModelTitle()))
1120
1173
  promptModelLabels.delete(label);
1121
1174
  }
1122
- safeRequestRender(homeComposerShell);
1123
1175
  safeRequestRender(sessionComposerShell);
1124
1176
  safeRequestRender(rootBox);
1125
1177
  };
@@ -1367,7 +1419,7 @@ function OpenTuiApp(props) {
1367
1419
  redrawApprovalPanel();
1368
1420
  if (approval || plan)
1369
1421
  focusApprovalPanel();
1370
- redrawTranscript();
1422
+ redrawTranscript(streamingDisplay, displayMessages, { forceFollow: !!approval });
1371
1423
  };
1372
1424
  function questionStateFromRequest(request) {
1373
1425
  return {
@@ -2327,18 +2379,18 @@ function OpenTuiApp(props) {
2327
2379
  function isHomeSurfaceActive(extra) {
2328
2380
  return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
2329
2381
  }
2382
+ function isComposerHiddenByModal() {
2383
+ return !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
2384
+ }
2330
2385
  function syncPromptSurfaces(focus = false) {
2331
2386
  const homeActive = isHomeSurfaceActive(streamingDisplay);
2332
2387
  const nextSessionActive = !homeActive;
2333
2388
  const surfaceChanged = sessionActive() !== nextSessionActive;
2334
2389
  setSessionActive(nextSessionActive);
2335
- const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
2336
2390
  if (homeSurfaceShell)
2337
2391
  homeSurfaceShell.visible = homeActive;
2338
- if (homeComposerShell)
2339
- homeComposerShell.visible = homeActive && !modalComposerHidden;
2340
2392
  if (sessionComposerShell)
2341
- sessionComposerShell.visible = !homeActive && !modalComposerHidden;
2393
+ sessionComposerShell.visible = !isComposerHiddenByModal();
2342
2394
  syncSidebarChrome();
2343
2395
  if (focus || surfaceChanged)
2344
2396
  setTimeout(() => activePrompt()?.focus(), 0);
@@ -2362,7 +2414,6 @@ function OpenTuiApp(props) {
2362
2414
  }
2363
2415
  }
2364
2416
  try {
2365
- homeComposerShell?.requestRender();
2366
2417
  sessionComposerShell?.requestRender();
2367
2418
  rootBox?.requestRender();
2368
2419
  }
@@ -2412,22 +2463,28 @@ function OpenTuiApp(props) {
2412
2463
  function redrawTranscriptWithQueuedDisplays() {
2413
2464
  redrawTranscript(streamingDisplay, displayMessages);
2414
2465
  }
2415
- function addQueuedUserDisplay(input) {
2466
+ function addUserInputStatusDisplay(input, inputStatus) {
2416
2467
  const displayId = `queued-${++nextQueuedDisplayId}`;
2417
2468
  queuedDisplayMessages = [
2418
2469
  ...queuedDisplayMessages,
2419
- { role: "user", content: input, clientId: displayId, queued: true },
2470
+ { role: "user", content: input, clientId: displayId, inputStatus },
2420
2471
  ];
2421
2472
  redrawTranscriptWithQueuedDisplays();
2422
2473
  return displayId;
2423
2474
  }
2424
- function updateQueuedUserDisplay(displayId, queued) {
2475
+ function addQueuedUserDisplay(input) {
2476
+ return addUserInputStatusDisplay(input, "queued");
2477
+ }
2478
+ function addPendingSteerUserDisplay(input) {
2479
+ return addUserInputStatusDisplay(input, "pending_steer");
2480
+ }
2481
+ function updateUserInputDisplayStatus(displayId, inputStatus) {
2425
2482
  let changed = false;
2426
2483
  const update = (message) => {
2427
2484
  if (message.clientId !== displayId)
2428
2485
  return message;
2429
2486
  changed = true;
2430
- return { ...message, queued };
2487
+ return setUserInputStatus(message, inputStatus);
2431
2488
  };
2432
2489
  displayMessages = displayMessages.map(update);
2433
2490
  queuedDisplayMessages = queuedDisplayMessages.map(update);
@@ -2452,11 +2509,14 @@ function OpenTuiApp(props) {
2452
2509
  return false;
2453
2510
  const index = queuedDisplayMessages.findIndex((message) => message.clientId === displayId);
2454
2511
  if (index === -1) {
2455
- return updateQueuedUserDisplay(displayId, false);
2512
+ return updateUserInputDisplayStatus(displayId);
2456
2513
  }
2457
2514
  const message = queuedDisplayMessages[index];
2458
2515
  queuedDisplayMessages = queuedDisplayMessages.filter((_, itemIndex) => itemIndex !== index);
2459
- displayMessages = [...displayMessages, { ...message, content: message.content || fallbackContent || " ", queued: false }];
2516
+ displayMessages = [
2517
+ ...displayMessages,
2518
+ setUserInputStatus({ ...message, content: message.content || fallbackContent || " " }),
2519
+ ];
2460
2520
  redrawTranscriptWithQueuedDisplays();
2461
2521
  return true;
2462
2522
  }
@@ -2490,7 +2550,7 @@ function OpenTuiApp(props) {
2490
2550
  }
2491
2551
  function requeueRejectedSteer(input, displayId) {
2492
2552
  const queuedDisplayId = displayId ?? addQueuedUserDisplay(input);
2493
- updateQueuedUserDisplay(queuedDisplayId, true);
2553
+ updateUserInputDisplayStatus(queuedDisplayId, "queued");
2494
2554
  rejectedSteerInputs.push({ input, displayId: queuedDisplayId });
2495
2555
  syncQueuedComposerInputCount();
2496
2556
  if (!isRunning())
@@ -2583,9 +2643,12 @@ function OpenTuiApp(props) {
2583
2643
  queueComposerInput(input, { showInTranscript: true });
2584
2644
  return;
2585
2645
  }
2586
- const displayId = addQueuedUserDisplay(input);
2587
- const pendingInput = run.inputController.enqueue(input);
2588
- pendingSteerInputs.push({ id: pendingInput.id, input, displayId });
2646
+ // Expand here because steer inputs bypass handleInput; keep the expanded
2647
+ // text in the record so a rejected steer requeues without stale markers.
2648
+ const expandedInput = expandComposerPastedTexts(input);
2649
+ const displayId = addPendingSteerUserDisplay(expandedInput);
2650
+ const pendingInput = run.inputController.enqueue(expandedInput);
2651
+ pendingSteerInputs.push({ id: pendingInput.id, input: expandedInput, displayId });
2589
2652
  syncPendingSteerInputCount();
2590
2653
  setNotice("Steer pending for next model call");
2591
2654
  }
@@ -2883,12 +2946,16 @@ function OpenTuiApp(props) {
2883
2946
  updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
2884
2947
  syncPromptSurfaces();
2885
2948
  }
2886
- function redrawTranscript(extra, baseMessages = displayMessages) {
2949
+ function redrawTranscript(extra, baseMessages = displayMessages, options = {}) {
2887
2950
  streamingDisplay = extra;
2888
- renderTranscriptNow(streamingDisplay, baseMessages);
2951
+ renderTranscriptNow(streamingDisplay, baseMessages, options);
2889
2952
  }
2890
- function renderTranscriptNow(extra, baseMessages = displayMessages) {
2891
- const shouldFollow = shouldFollowTranscriptBeforeUpdate();
2953
+ function renderTranscriptNow(extra, baseMessages = displayMessages, options = {}) {
2954
+ const shouldFollow = options.forceFollow ? true : shouldFollowTranscriptBeforeUpdate();
2955
+ if (options.forceFollow) {
2956
+ transcriptScrollFollowing = true;
2957
+ transcriptScrollInitialized = true;
2958
+ }
2892
2959
  const nextMessages = compactDisplayMessages([
2893
2960
  ...baseMessages,
2894
2961
  ...(extra ? [extra] : []),
@@ -2906,6 +2973,7 @@ function OpenTuiApp(props) {
2906
2973
  syncSidebarChrome();
2907
2974
  redrawQuestionPanel();
2908
2975
  redrawStatsPanel();
2976
+ redrawProviderDialog();
2909
2977
  redrawFeishuSetupPanel();
2910
2978
  scrollbox?.requestRender();
2911
2979
  scheduleTranscriptScrollAfterUpdate(shouldFollow);
@@ -3131,11 +3199,13 @@ function OpenTuiApp(props) {
3131
3199
  providerDialogRoot.requestRender();
3132
3200
  return;
3133
3201
  }
3134
- const width = Math.max(56, Math.min(76, dimensions().width - 4));
3202
+ const terminal = liveTerminalDimensions();
3203
+ const width = providerDialogPanelWidth(terminal.width);
3135
3204
  const height = PROVIDER_DIALOG_ROWS + 7;
3205
+ const columnWidths = providerDialogColumnWidths(state, width);
3136
3206
  providerDialogRoot.visible = true;
3137
- providerDialogRoot.width = dimensions().width;
3138
- providerDialogRoot.height = dimensions().height;
3207
+ providerDialogRoot.width = terminal.width;
3208
+ providerDialogRoot.height = terminal.height;
3139
3209
  providerDialogRoot.left = 0;
3140
3210
  providerDialogRoot.top = 0;
3141
3211
  providerDialogRoot.backgroundColor = modalBackdropColor();
@@ -3143,8 +3213,8 @@ function OpenTuiApp(props) {
3143
3213
  providerDialogPanel.visible = true;
3144
3214
  providerDialogPanel.width = width;
3145
3215
  providerDialogPanel.height = height;
3146
- providerDialogPanel.left = Math.max(0, Math.floor((dimensions().width - width) / 2));
3147
- providerDialogPanel.top = Math.max(0, Math.floor(dimensions().height / 4));
3216
+ providerDialogPanel.left = Math.max(0, Math.floor((terminal.width - width) / 2));
3217
+ providerDialogPanel.top = Math.max(0, Math.floor(terminal.height / 4));
3148
3218
  providerDialogPanel.backgroundColor = theme.backgroundPanel;
3149
3219
  providerDialogPanel.borderColor = theme.backgroundPanel;
3150
3220
  providerDialogPanel.requestRender();
@@ -3218,20 +3288,20 @@ function OpenTuiApp(props) {
3218
3288
  gutter.fg = active ? activeText : providerDialogGutterColor(row.item.gutter ?? (isCurrentModelItem(row.item) ? "●" : undefined));
3219
3289
  }
3220
3290
  if (label) {
3221
- label.content = truncate(row.item.label, providerDialogLabelWidth(state));
3291
+ label.content = truncate(row.item.label, columnWidths.label);
3222
3292
  label.fg = active ? activeText : isCurrentModelItem(row.item) ? theme.primary : theme.text;
3223
3293
  }
3224
3294
  if (detail) {
3225
3295
  const detailText = state.query.trim() && state.step === "models"
3226
3296
  ? row.item.category ?? row.item.detail ?? ""
3227
3297
  : row.item.detail ?? "";
3228
- detail.width = providerDialogDetailWidth(state);
3229
- detail.content = truncate(detailText, providerDialogDetailWidth(state));
3298
+ detail.width = columnWidths.detail;
3299
+ detail.content = truncate(detailText, columnWidths.detail);
3230
3300
  detail.fg = active ? activeText : theme.textMuted;
3231
3301
  }
3232
3302
  if (footer) {
3233
- footer.width = providerDialogFooterWidth(state);
3234
- footer.content = row.item.footer ?? "";
3303
+ footer.width = columnWidths.footer;
3304
+ footer.content = truncate(row.item.footer ?? "", columnWidths.footer);
3235
3305
  footer.fg = active ? activeText : theme.textMuted;
3236
3306
  }
3237
3307
  }
@@ -3279,14 +3349,17 @@ function OpenTuiApp(props) {
3279
3349
  return theme.warning;
3280
3350
  return theme.textMuted;
3281
3351
  }
3282
- function providerDialogLabelWidth(state) {
3283
- return state.step === "skills" ? 22 : 37;
3284
- }
3285
- function providerDialogDetailWidth(state) {
3286
- return state.step === "skills" ? 26 : 16;
3352
+ function providerDialogPanelWidth(terminalWidth) {
3353
+ return Math.max(PROVIDER_DIALOG_MIN_WIDTH, Math.min(PROVIDER_DIALOG_MAX_WIDTH, terminalWidth - 4));
3287
3354
  }
3288
- function providerDialogFooterWidth(state) {
3289
- return state.step === "skills" ? 9 : 8;
3355
+ function providerDialogColumnWidths(state, panelWidth) {
3356
+ const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
3357
+ const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
3358
+ const minLabel = state.step === "skills" ? 18 : 24;
3359
+ const desiredDetail = state.step === "skills" ? 30 : state.step === "providers" ? 24 : 16;
3360
+ const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
3361
+ const label = Math.max(8, contentWidth - detail - footer);
3362
+ return { label, detail, footer };
3290
3363
  }
3291
3364
  function isCurrentModelItem(item) {
3292
3365
  return item.value === props.agent.model || item.detail?.includes("current");
@@ -4597,6 +4670,28 @@ function OpenTuiApp(props) {
4597
4670
  applyingComposerImageReplacement = false;
4598
4671
  }
4599
4672
  }
4673
+ // Replaces pasted-text markers with their full content. Runs after @mention
4674
+ // expansion so mention-like tokens inside pasted content stay literal.
4675
+ // References stay registered for the whole session so prompt-history recall
4676
+ // and requeued drafts containing a marker expand again on resend.
4677
+ function expandComposerPastedTexts(text) {
4678
+ if (pendingPastedTexts.size === 0 || !text.includes("[Pasted text #"))
4679
+ return text;
4680
+ const references = [...pendingPastedTexts.entries()].map(([marker, content]) => ({ marker, content }));
4681
+ return expandPastedContentMarkers(text, references);
4682
+ }
4683
+ function handleComposerPaste(event) {
4684
+ const text = typeof event.text === "string" ? event.text : decodePastedBytes(event.bytes);
4685
+ if (!text || !shouldCollapsePastedContent(text))
4686
+ return;
4687
+ event.preventDefault?.();
4688
+ const marker = createPastedContentMarker(text, nextPastedTextIndex);
4689
+ nextPastedTextIndex += 1;
4690
+ pendingPastedTexts.set(marker, text);
4691
+ const prompt = activePrompt();
4692
+ prompt?.insertText(marker);
4693
+ onPromptContentChange(readPromptText());
4694
+ }
4600
4695
  async function expandTextParts(parts) {
4601
4696
  const expandedParts = [];
4602
4697
  for (const part of parts) {
@@ -4607,9 +4702,11 @@ function OpenTuiApp(props) {
4607
4702
  const expansion = await expandAtMentions(part.text, props.args.cwd);
4608
4703
  if (expansion.missing.length)
4609
4704
  addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
4610
- for (const skipped of expansion.skipped)
4611
- addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
4612
- expandedParts.push({ type: "text", text: expansion.text });
4705
+ for (const skipped of expansion.skipped) {
4706
+ if (skipped.reason !== "too large")
4707
+ addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
4708
+ }
4709
+ expandedParts.push({ type: "text", text: expandComposerPastedTexts(expansion.text) });
4613
4710
  }
4614
4711
  return expandedParts;
4615
4712
  }
@@ -4635,7 +4732,7 @@ function OpenTuiApp(props) {
4635
4732
  if (input.startsWith("/")) {
4636
4733
  const skillInvocation = parseSkillInvocation(input, skills);
4637
4734
  if (skillInvocation) {
4638
- await runAgentInput(skillInvocation.actualPrompt, input, options);
4735
+ await runAgentInput(expandComposerPastedTexts(skillInvocation.actualPrompt), input, options);
4639
4736
  return;
4640
4737
  }
4641
4738
  const handled = await executeSlash(input, options);
@@ -4645,9 +4742,11 @@ function OpenTuiApp(props) {
4645
4742
  const expansion = await expandAtMentions(input, props.args.cwd);
4646
4743
  if (expansion.missing.length)
4647
4744
  addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
4648
- for (const skipped of expansion.skipped)
4649
- addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
4650
- await runAgentInput(expansion.text, input, options);
4745
+ for (const skipped of expansion.skipped) {
4746
+ if (skipped.reason !== "too large")
4747
+ addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
4748
+ }
4749
+ await runAgentInput(expandComposerPastedTexts(expansion.text), input, options);
4651
4750
  }
4652
4751
  async function executeSlash(input, options = {}) {
4653
4752
  if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
@@ -4680,6 +4779,7 @@ function OpenTuiApp(props) {
4680
4779
  skillRegistry: skills,
4681
4780
  bashAllowlist: props.options.bashAllowlist,
4682
4781
  settingsManager: props.options.settingsManager,
4782
+ hookController: props.options.hookController,
4683
4783
  mcpManager: props.options.mcpManager,
4684
4784
  lspService,
4685
4785
  flushMemory: props.options.flushMemory,
@@ -5106,10 +5206,13 @@ function OpenTuiApp(props) {
5106
5206
  return;
5107
5207
  }
5108
5208
  rememberPromptHistory(displayInput);
5109
- const reusedQueuedDisplay = promoteQueuedUserDisplay(options.displayId, displayInput);
5209
+ // History keeps the short marker (it expands again on resend); the
5210
+ // transcript shows the full pasted content once the message is sent.
5211
+ const displayContent = expandComposerPastedTexts(displayInput);
5212
+ const reusedQueuedDisplay = promoteQueuedUserDisplay(options.displayId, displayContent);
5110
5213
  const nextMessages = reusedQueuedDisplay
5111
5214
  ? displayMessages
5112
- : [...displayMessages, { role: "user", content: displayInput }];
5215
+ : [...displayMessages, { role: "user", content: displayContent }];
5113
5216
  if (!reusedQueuedDisplay)
5114
5217
  displayMessages = nextMessages;
5115
5218
  streamingDisplay = undefined;
@@ -5127,6 +5230,8 @@ function OpenTuiApp(props) {
5127
5230
  }, { surface: "tui" });
5128
5231
  let assistantContent = "";
5129
5232
  let assistantReasoning = "";
5233
+ let textDisplaySanitizer = createStreamingInternalReminderSanitizer();
5234
+ let reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
5130
5235
  const toolCalls = [];
5131
5236
  const assistantParts = [];
5132
5237
  let turnStartedAt;
@@ -5141,7 +5246,7 @@ function OpenTuiApp(props) {
5141
5246
  const buildStreamingDisplay = (status) => {
5142
5247
  const currentParts = snapshotDisplayParts(assistantParts);
5143
5248
  const partContent = assistantContent || contentFromParts(currentParts);
5144
- return {
5249
+ return sanitizeDisplayMessage({
5145
5250
  role: "assistant",
5146
5251
  content: partContent,
5147
5252
  reasoning: assistantReasoning || undefined,
@@ -5150,7 +5255,7 @@ function OpenTuiApp(props) {
5150
5255
  status,
5151
5256
  streaming: true,
5152
5257
  turnStartedAt,
5153
- };
5258
+ });
5154
5259
  };
5155
5260
  const flushStreamingRedraw = () => {
5156
5261
  if (pendingStreamingRedrawTimer === undefined)
@@ -5180,6 +5285,8 @@ function OpenTuiApp(props) {
5180
5285
  if (event.type === "turn_start") {
5181
5286
  assistantContent = "";
5182
5287
  assistantReasoning = "";
5288
+ textDisplaySanitizer = createStreamingInternalReminderSanitizer();
5289
+ reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
5183
5290
  toolCalls.length = 0;
5184
5291
  assistantParts.length = 0;
5185
5292
  turnStartedAt = Date.now();
@@ -5192,22 +5299,42 @@ function OpenTuiApp(props) {
5192
5299
  });
5193
5300
  }
5194
5301
  else if (event.type === "text_delta") {
5195
- assistantContent += event.content;
5196
- appendTextPart(assistantParts, event.content);
5197
- scheduleStreamingRedraw();
5302
+ const content = textDisplaySanitizer.push(event.content);
5303
+ if (content) {
5304
+ assistantContent += content;
5305
+ appendTextPart(assistantParts, content);
5306
+ scheduleStreamingRedraw();
5307
+ }
5198
5308
  }
5199
5309
  else if (event.type === "reasoning_delta") {
5310
+ const content = reasoningDisplaySanitizer.push(event.content);
5311
+ if (!content)
5312
+ continue;
5200
5313
  debugReasoningStream({
5201
5314
  stage: "ui_append",
5202
5315
  providerId: props.agent.providerId,
5203
5316
  modelId: props.agent.apiModel,
5204
5317
  beforeLength: assistantReasoning.length,
5205
- delta: summarizeDebugText(event.content),
5206
- afterLength: assistantReasoning.length + event.content.length,
5318
+ delta: summarizeDebugText(content),
5319
+ afterLength: assistantReasoning.length + content.length,
5207
5320
  });
5208
- assistantReasoning += event.content;
5321
+ assistantReasoning += content;
5209
5322
  scheduleStreamingRedraw();
5210
5323
  }
5324
+ else if (event.type === "hook_start") {
5325
+ setNotice(`Hook ${event.eventName}: ${event.hookId}`);
5326
+ }
5327
+ else if (event.type === "hook_end") {
5328
+ if (event.decision === "deny") {
5329
+ setNotice(event.reason ?? `Hook ${event.hookId} denied ${event.eventName}`);
5330
+ }
5331
+ }
5332
+ else if (event.type === "hook_error") {
5333
+ setNotice(`Hook ${event.hookId} error: ${event.error}`);
5334
+ }
5335
+ else if (event.type === "provider_retry") {
5336
+ setNotice(`Connection interrupted — retrying (${event.attempt}/${event.maxAttempts})…`);
5337
+ }
5211
5338
  else if (event.type === "tool_call_start") {
5212
5339
  // Insert a streaming placeholder so the user sees feedback the moment
5213
5340
  // the model commits to a tool call, instead of waiting for the args
@@ -5336,6 +5463,15 @@ function OpenTuiApp(props) {
5336
5463
  clearTimeout(pendingStreamingRedrawTimer);
5337
5464
  pendingStreamingRedrawTimer = undefined;
5338
5465
  }
5466
+ const flushedText = textDisplaySanitizer.flush();
5467
+ if (flushedText) {
5468
+ assistantContent += flushedText;
5469
+ appendTextPart(assistantParts, flushedText);
5470
+ }
5471
+ const flushedReasoning = reasoningDisplaySanitizer.flush();
5472
+ if (flushedReasoning) {
5473
+ assistantReasoning += flushedReasoning;
5474
+ }
5339
5475
  if (event.usage) {
5340
5476
  setSidebarUsage((current) => ({
5341
5477
  contextTokens: event.usage.promptTokens || current.contextTokens,
@@ -5351,20 +5487,21 @@ function OpenTuiApp(props) {
5351
5487
  }
5352
5488
  bumpSidebar();
5353
5489
  const currentParts = snapshotDisplayParts(assistantParts);
5354
- const finalContent = assistantContent || contentFromParts(currentParts);
5490
+ const finalContent = sanitizeInternalReminderBlocks(assistantContent || contentFromParts(currentParts));
5491
+ const finalReasoning = sanitizeInternalReasoningText(assistantReasoning);
5355
5492
  const finalToolCalls = toolCalls.length > 0
5356
5493
  ? [...toolCalls]
5357
5494
  : toolCallsFromParts(currentParts);
5358
- const assistantMessage = {
5495
+ const assistantMessage = sanitizeDisplayMessage({
5359
5496
  role: "assistant",
5360
5497
  content: finalContent,
5361
- reasoning: assistantReasoning || undefined,
5498
+ reasoning: finalReasoning || undefined,
5362
5499
  toolCalls: finalToolCalls.length ? finalToolCalls : undefined,
5363
5500
  parts: currentParts.length ? currentParts : undefined,
5364
5501
  turnStartedAt,
5365
5502
  turnCompletedAt: Date.now(),
5366
5503
  turnUsage: event.usage,
5367
- };
5504
+ });
5368
5505
  const nextMessages = hasRenderableMessage(assistantMessage)
5369
5506
  ? [...displayMessages, assistantMessage]
5370
5507
  : displayMessages;
@@ -5464,16 +5601,16 @@ function OpenTuiApp(props) {
5464
5601
  return h("box", {
5465
5602
  ref: (ref) => {
5466
5603
  sessionComposerShell = ref;
5467
- ref.visible = !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5604
+ ref.visible = !isComposerHiddenByModal();
5468
5605
  },
5469
5606
  width: "100%",
5470
5607
  paddingLeft: 2,
5471
5608
  paddingRight: 2,
5472
5609
  flexShrink: 0,
5473
- visible: !isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5610
+ visible: !isComposerHiddenByModal(),
5474
5611
  }, renderPrompt({
5475
5612
  ref: (ref) => { sessionPromptRef = ref; },
5476
- focused: !isHomeSurfaceActive(streamingDisplay),
5613
+ focused: !isComposerHiddenByModal(),
5477
5614
  onSubmit: submitPrompt,
5478
5615
  isFallbackNewlineKey: isTrackedShiftReturn,
5479
5616
  onFallbackNewline: () => canInsertPromptNewline() && (activePrompt()?.newLine() ?? false),
@@ -5488,6 +5625,7 @@ function OpenTuiApp(props) {
5488
5625
  model: promptModelTitle,
5489
5626
  interruptHint: promptStatusText,
5490
5627
  tabHint: () => isRunning() ? "queue" : "mode",
5628
+ onPaste: handleComposerPaste,
5491
5629
  placeholder: () => {
5492
5630
  const approvalState = pendingApproval();
5493
5631
  if (approvalState)
@@ -5508,7 +5646,6 @@ function OpenTuiApp(props) {
5508
5646
  }));
5509
5647
  }
5510
5648
  function renderHomeSurface() {
5511
- const homeHeight = Math.max(16, dimensions().height - 4);
5512
5649
  const logoLines = bubbleWordmarkForWidth(dimensions().width);
5513
5650
  return h("box", {
5514
5651
  ref: (ref) => {
@@ -5516,7 +5653,8 @@ function OpenTuiApp(props) {
5516
5653
  ref.visible = isHomeSurfaceActive(streamingDisplay);
5517
5654
  },
5518
5655
  visible: isHomeSurfaceActive(streamingDisplay),
5519
- height: homeHeight,
5656
+ height: "100%",
5657
+ minHeight: 0,
5520
5658
  flexDirection: "column",
5521
5659
  alignItems: "center",
5522
5660
  justifyContent: "center",
@@ -5528,57 +5666,6 @@ function OpenTuiApp(props) {
5528
5666
  ...(props.options.updateNotice
5529
5667
  ? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
5530
5668
  : []),
5531
- h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
5532
- h("box", {
5533
- ref: (ref) => {
5534
- homeComposerShell = ref;
5535
- ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
5536
- },
5537
- width: "100%",
5538
- maxWidth: 75,
5539
- zIndex: 1000,
5540
- paddingTop: 1,
5541
- flexShrink: 0,
5542
- visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
5543
- }, renderPrompt({
5544
- ref: (ref) => {
5545
- homePromptRef = ref;
5546
- if (isHomeSurfaceActive(streamingDisplay))
5547
- setTimeout(() => ref.focus(), 0);
5548
- },
5549
- focused: isHomeSurfaceActive(streamingDisplay),
5550
- onSubmit: submitPrompt,
5551
- isFallbackNewlineKey: isTrackedShiftReturn,
5552
- onFallbackNewline: () => canInsertPromptNewline() && (activePrompt()?.newLine() ?? false),
5553
- onContentChange: onPromptContentChange,
5554
- onKeyDown: handlePickerKey,
5555
- onUiKeyDown: promptUiKeyDown,
5556
- getText: readPromptText,
5557
- disabled: () => !!pendingFeedback() || !!statsPanel,
5558
- mode,
5559
- registerModeLabel: registerPromptModeLabel,
5560
- registerModelLabel: registerPromptModelLabel,
5561
- model: promptModelTitle,
5562
- interruptHint: promptStatusText,
5563
- tabHint: () => isRunning() ? "queue" : "mode",
5564
- placeholder: () => {
5565
- const approvalState = pendingApproval();
5566
- if (approvalState)
5567
- return "Press Enter to approve or Esc to reject";
5568
- if (pendingQuestion())
5569
- return "Answer the question below";
5570
- if (pendingFeedback())
5571
- return "Describe feedback below";
5572
- if (statsPanel)
5573
- return "Stats panel is open";
5574
- const plan = pendingPlan();
5575
- if (plan)
5576
- return "Press Enter to approve plan or Esc to reject";
5577
- if (isRunning())
5578
- return "Steer current run...";
5579
- return `Ask anything... "${homePrompt}"`;
5580
- },
5581
- })),
5582
5669
  ]);
5583
5670
  }
5584
5671
  function renderQuestionPanelHost() {
@@ -5656,7 +5743,10 @@ function OpenTuiApp(props) {
5656
5743
  visible: false,
5657
5744
  flexShrink: 0,
5658
5745
  }, h("textarea", {
5659
- ref: (ref) => { questionCustomInput = ref; },
5746
+ ref: (ref) => {
5747
+ preserveCursorOnMouseSelection(ref);
5748
+ questionCustomInput = ref;
5749
+ },
5660
5750
  placeholder: "Type your own answer",
5661
5751
  placeholderColor: theme.textMuted,
5662
5752
  textColor: theme.text,
@@ -5745,7 +5835,10 @@ function OpenTuiApp(props) {
5745
5835
  wrapMode: "word",
5746
5836
  content: "Creates a public GitHub issue at DylanDDeng/bubble. Review before sending.",
5747
5837
  }), h("textarea", {
5748
- ref: (ref) => { feedbackInput = ref; },
5838
+ ref: (ref) => {
5839
+ preserveCursorOnMouseSelection(ref);
5840
+ feedbackInput = ref;
5841
+ },
5749
5842
  placeholder: "Describe what happened",
5750
5843
  placeholderColor: theme.textMuted,
5751
5844
  textColor: theme.text,
@@ -6657,11 +6750,9 @@ function OpenTuiApp(props) {
6657
6750
  visible: !!approval,
6658
6751
  focusable: true,
6659
6752
  onKeyDown: handleApprovalKey,
6660
- position: "absolute",
6661
- left: 2,
6662
- right: 2,
6663
- bottom: 4,
6664
- zIndex: 200,
6753
+ width: "100%",
6754
+ flexShrink: 0,
6755
+ marginTop: 1,
6665
6756
  backgroundColor: theme.backgroundPanel,
6666
6757
  border: ["left"],
6667
6758
  borderColor: theme.warning,
@@ -6841,7 +6932,10 @@ function OpenTuiApp(props) {
6841
6932
  function renderPrompt(input) {
6842
6933
  const transparentBackground = "#00000000";
6843
6934
  return h("box", { flexDirection: "column", flexShrink: 0, marginTop: 1 }, h("box", { width: "100%", border: true, borderColor: theme.border, backgroundColor: transparentBackground }, h("box", { flexDirection: "column", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, backgroundColor: transparentBackground }, h("textarea", {
6844
- ref: input.ref,
6935
+ ref: (ref) => {
6936
+ preserveCursorOnMouseSelection(ref);
6937
+ input.ref(ref);
6938
+ },
6845
6939
  focused: input.focused,
6846
6940
  placeholder: input.placeholder(),
6847
6941
  placeholderColor: theme.textMuted,
@@ -6849,8 +6943,10 @@ function renderPrompt(input) {
6849
6943
  focusedTextColor: theme.text,
6850
6944
  backgroundColor: transparentBackground,
6851
6945
  focusedBackgroundColor: transparentBackground,
6946
+ cursorColor: theme.primary,
6852
6947
  minHeight: 1,
6853
6948
  maxHeight: 6,
6949
+ ...(input.onPaste ? { onPaste: input.onPaste } : {}),
6854
6950
  onContentChange: () => input.onContentChange(input.getText()),
6855
6951
  keyBindings: PROMPT_TEXTAREA_KEYBINDINGS,
6856
6952
  onKeyDown: (event) => {
@@ -6965,8 +7061,9 @@ function renderUserMessage(message, index) {
6965
7061
  const userChildren = [
6966
7062
  h("text", { fg: theme.messageUserText, wrapMode: "word" }, message.content || " "),
6967
7063
  ];
6968
- if (message.queued) {
6969
- userChildren.push(h("box", { paddingTop: 1 }, h("text", { fg: theme.textMuted }, h("span", { bg: theme.primary, fg: theme.background, bold: true }, " QUEUED "))));
7064
+ const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
7065
+ if (inputBadge) {
7066
+ userChildren.push(h("box", { paddingTop: 1 }, h("text", { fg: theme.textMuted }, h("span", { bg: theme.primary, fg: theme.background, bold: true }, ` ${inputBadge} `))));
6970
7067
  }
6971
7068
  return h("box", {
6972
7069
  border: ["left"],
@@ -6977,17 +7074,20 @@ function renderUserMessage(message, index) {
6977
7074
  }, h("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: theme.backgroundPanel, flexShrink: 0, flexDirection: "column" }, ...userChildren));
6978
7075
  }
6979
7076
  function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false, width = 80) {
7077
+ message = sanitizeDisplayMessage(message);
6980
7078
  const visibleReasoning = showThinking
6981
- ? sanitizeInternalReminderBlocks(message.reasoning ?? "").trim()
7079
+ ? sanitizeInternalReasoningText(message.reasoning ?? "").trim()
6982
7080
  : "";
6983
- const modelSwitch = parseModelSwitchMessage(message.content);
7081
+ const sanitizedContent = sanitizeInternalReminderBlocks(message.content);
7082
+ const modelSwitch = parseModelSwitchMessage(sanitizedContent);
6984
7083
  if (modelSwitch && !visibleReasoning && !(message.toolCalls?.length)) {
6985
7084
  return renderModelSwitchMessage(modelSwitch);
6986
7085
  }
6987
7086
  const children = [];
6988
7087
  const parts = message.parts ?? [];
6989
7088
  const hasParts = parts.length > 0;
6990
- if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length) && !hasParts) {
7089
+ const trimmedContent = sanitizedContent.trim();
7090
+ if (message.status && !visibleReasoning && !trimmedContent && !(message.toolCalls?.length) && !hasParts) {
6991
7091
  children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { fg: theme.messageThinkingText }, assistantStatusLabel(message))));
6992
7092
  }
6993
7093
  if (visibleReasoning) {
@@ -7003,7 +7103,6 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
7003
7103
  fg: theme.messageThinkingContentText,
7004
7104
  })));
7005
7105
  }
7006
- const trimmedContent = message.content.trim();
7007
7106
  if (hasParts) {
7008
7107
  renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, message.streaming === true);
7009
7108
  }
@@ -7041,7 +7140,7 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
7041
7140
  function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, streaming) {
7042
7141
  for (const part of parts) {
7043
7142
  if (part.type === "text") {
7044
- const content = part.content.trim();
7143
+ const content = sanitizeInternalReminderBlocks(part.content).trim();
7045
7144
  if (!content)
7046
7145
  continue;
7047
7146
  children.push(h("box", {
@@ -7067,7 +7166,7 @@ function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace,
7067
7166
  }
7068
7167
  function lastPartHasText(parts) {
7069
7168
  const last = parts[parts.length - 1];
7070
- return last?.type === "text" && !!last.content.trim();
7169
+ return last?.type === "text" && !!sanitizeInternalReminderBlocks(last.content).trim();
7071
7170
  }
7072
7171
  function parseModelSwitchMessage(content) {
7073
7172
  const match = content.trim().match(/^Model switched to (.+)\.$/);
@@ -7121,7 +7220,7 @@ function renderMarkdownContent(content, syntaxStyle, options) {
7121
7220
  function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtleSyntaxStyle) {
7122
7221
  const showThinking = options?.showThinking ?? true;
7123
7222
  const verboseTrace = options?.verboseTrace ?? false;
7124
- const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
7223
+ const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
7125
7224
  const ctx = host.ctx;
7126
7225
  const nextEntries = [];
7127
7226
  if (!visibleMessages.length && !options?.plan) {
@@ -7200,6 +7299,7 @@ function transcriptMessageKey(message, index) {
7200
7299
  return `${index}:${message.role}`;
7201
7300
  }
7202
7301
  function transcriptMessageSignature(message, compactionExpanded = false) {
7302
+ message = sanitizeDisplayMessage(message);
7203
7303
  if (message.role !== "assistant")
7204
7304
  return message.role;
7205
7305
  if (message.syntheticKind === "ui_compact_card") {
@@ -7217,12 +7317,13 @@ function transcriptMessageSignature(message, compactionExpanded = false) {
7217
7317
  }
7218
7318
  function updateMessageEntry(entry, message, showThinking = true, compactionExpanded = false, assistantOptions) {
7219
7319
  if (message.role === "user") {
7320
+ const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
7220
7321
  if (entry.refs.userText)
7221
7322
  entry.refs.userText.content = message.content || " ";
7222
7323
  if (entry.refs.userQueuedBox)
7223
- entry.refs.userQueuedBox.visible = message.queued === true;
7324
+ entry.refs.userQueuedBox.visible = !!inputBadge;
7224
7325
  if (entry.refs.userQueuedText)
7225
- entry.refs.userQueuedText.content = message.queued ? " QUEUED " : "";
7326
+ entry.refs.userQueuedText.content = inputBadge ? ` ${inputBadge} ` : "";
7226
7327
  return;
7227
7328
  }
7228
7329
  if (message.role === "error") {
@@ -7249,6 +7350,7 @@ function updateMessageEntry(entry, message, showThinking = true, compactionExpan
7249
7350
  }
7250
7351
  }
7251
7352
  function updateAssistantEntry(entry, message, showThinking, options) {
7353
+ message = sanitizeDisplayMessage(message);
7252
7354
  const content = message.content.trim();
7253
7355
  const visibleReasoning = showThinking ? message.reasoning?.trim() ?? "" : "";
7254
7356
  const tools = message.toolCalls ?? [];
@@ -7345,7 +7447,7 @@ function updateAssistantPartEntries(entry, parts, options, streaming) {
7345
7447
  const key = `part:${index}:${part.type}`;
7346
7448
  const previous = previousEntries.get(key);
7347
7449
  if (part.type === "text") {
7348
- const content = part.content.trim();
7450
+ const content = sanitizeInternalReminderBlocks(part.content).trim();
7349
7451
  let ref;
7350
7452
  if (previous?.kind === "text") {
7351
7453
  ref = previous;
@@ -7896,14 +7998,15 @@ function createUserEntry(ctx, message, index, key, signature) {
7896
7998
  wrapMode: "word",
7897
7999
  });
7898
8000
  refs.userText = text;
7899
- const queuedText = createText(ctx, message.queued ? " QUEUED " : "", {
8001
+ const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
8002
+ const queuedText = createText(ctx, inputBadge ? ` ${inputBadge} ` : "", {
7900
8003
  fg: theme.background,
7901
8004
  bg: theme.primary,
7902
8005
  });
7903
8006
  refs.userQueuedText = queuedText;
7904
8007
  const queuedBox = createBox(ctx, {
7905
8008
  paddingTop: 1,
7906
- visible: message.queued === true,
8009
+ visible: !!inputBadge,
7907
8010
  }, [queuedText]);
7908
8011
  refs.userQueuedBox = queuedBox;
7909
8012
  const node = createBox(ctx, {
@@ -7944,6 +8047,7 @@ function createErrorEntry(ctx, message, key, signature) {
7944
8047
  return { key, signature, node, refs };
7945
8048
  }
7946
8049
  function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, verboseTrace = false, expandedWrites = new Set(), onToggleWrite) {
8050
+ message = sanitizeDisplayMessage(message);
7947
8051
  const modelSwitch = parseModelSwitchMessage(message.content);
7948
8052
  if (modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length)) {
7949
8053
  return createModelSwitchEntry(ctx, modelSwitch, key, signature);
@@ -8109,7 +8213,7 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
8109
8213
  statsParts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
8110
8214
  if (meta?.messages)
8111
8215
  statsParts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
8112
- const statsLine = statsParts.length > 0 ? statsParts.join(" · ") : "Compacted";
8216
+ const statsLine = statsParts.length > 0 ? `${statsParts.join(" · ")} collapsed` : "Collapsed";
8113
8217
  const children = [];
8114
8218
  const headerRow = createBox(ctx, {
8115
8219
  flexDirection: "row",
@@ -8118,8 +8222,8 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
8118
8222
  alignItems: "center",
8119
8223
  }, [
8120
8224
  createText(ctx, new StyledText([
8121
- fg(theme.info)(bold("◈ Context Compacted")),
8122
- ]), { width: 20 }),
8225
+ fg(theme.info)(bold("◈ Earlier Conversation")),
8226
+ ]), { width: 23 }),
8123
8227
  createText(ctx, new StyledText([
8124
8228
  fg(theme.textMuted)(`─ ${statsLine}`),
8125
8229
  ])),
@@ -8716,7 +8820,7 @@ function renderTranscript(messages, options, syntaxStyle, subtleSyntaxStyle) {
8716
8820
  return items;
8717
8821
  }
8718
8822
  function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false) {
8719
- const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
8823
+ const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
8720
8824
  if (!visibleMessages.length)
8721
8825
  return null;
8722
8826
  return visibleMessages.map((message, index) => renderMessage(message, index, syntaxStyle, subtleSyntaxStyle, showThinking, verboseTrace));
@@ -8724,7 +8828,7 @@ function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThi
8724
8828
  function formatTranscript(messages, options) {
8725
8829
  const showThinking = options?.showThinking ?? true;
8726
8830
  const verboseTrace = options?.verboseTrace ?? false;
8727
- const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
8831
+ const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
8728
8832
  const chunks = [];
8729
8833
  const append = (content, color = theme.text) => {
8730
8834
  if (content)
@@ -8901,6 +9005,7 @@ function renderHomeState(input) {
8901
9005
  }, h("box", { flexDirection: "column", flexShrink: 0, width: "100%" }, h("text", { fg: theme.text }, ""), h("text", { fg: theme.text }, ""), ...logoLines.map((line) => renderHomeLogoLine(line, width)), h("text", { fg: theme.text }, ""), h("text", { fg: theme.warning }, centerLine(`● Tip ${input.tip}`, width)), cwd ? h("text", { fg: theme.textMuted }, centerLine(` ${cwd}`, width)) : null));
8902
9006
  }
8903
9007
  function hasRenderableMessage(message, showThinking = true) {
9008
+ message = sanitizeDisplayMessage(message);
8904
9009
  if (message.role === "error")
8905
9010
  return !!message.content.trim();
8906
9011
  if (message.role === "user")