@bubblebrain-ai/bubble 0.0.33 → 0.0.35

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 (61) hide show
  1. package/dist/agent/abort-errors.d.ts +2 -2
  2. package/dist/agent/budget-ledger.d.ts +4 -45
  3. package/dist/agent/budget-ledger.js +5 -75
  4. package/dist/agent/child-runner.d.ts +1 -3
  5. package/dist/agent/child-runner.js +6 -37
  6. package/dist/agent/profiles.d.ts +0 -2
  7. package/dist/agent/profiles.js +0 -3
  8. package/dist/agent/subagent-control.d.ts +5 -12
  9. package/dist/agent/subagent-control.js +0 -2
  10. package/dist/agent/subagent-store.d.ts +1 -1
  11. package/dist/agent/subagent-store.js +1 -3
  12. package/dist/agent/workflow/control.d.ts +7 -0
  13. package/dist/agent/workflow/control.js +22 -1
  14. package/dist/agent/workflow/runtime.d.ts +15 -2
  15. package/dist/agent/workflow/runtime.js +112 -30
  16. package/dist/agent.d.ts +7 -43
  17. package/dist/agent.js +50 -176
  18. package/dist/config.d.ts +1 -3
  19. package/dist/config.js +6 -5
  20. package/dist/context/overflow.js +2 -0
  21. package/dist/main.js +9 -1
  22. package/dist/model-catalog.d.ts +1 -1
  23. package/dist/model-catalog.js +25 -18
  24. package/dist/orchestrator/default-hooks.js +7 -1
  25. package/dist/prompt/delegation.d.ts +1 -1
  26. package/dist/prompt/delegation.js +21 -7
  27. package/dist/prompt/task-reminders.d.ts +1 -0
  28. package/dist/prompt/task-reminders.js +24 -1
  29. package/dist/provider-ai-sdk.d.ts +56 -0
  30. package/dist/provider-ai-sdk.js +518 -0
  31. package/dist/provider-registry.js +49 -3
  32. package/dist/provider-transform.d.ts +1 -1
  33. package/dist/provider-transform.js +8 -13
  34. package/dist/provider.js +4 -0
  35. package/dist/slash-commands/commands.js +3 -7
  36. package/dist/slash-commands/types.d.ts +1 -1
  37. package/dist/tools/agent-lifecycle.d.ts +1 -9
  38. package/dist/tools/agent-lifecycle.js +35 -263
  39. package/dist/tui-ink/app.js +51 -13
  40. package/dist/tui-ink/approval/approval-dialog.js +1 -1
  41. package/dist/tui-ink/approval/diff-view.js +4 -4
  42. package/dist/tui-ink/approval/select.js +2 -2
  43. package/dist/tui-ink/feedback-dialog.js +2 -2
  44. package/dist/tui-ink/feishu-setup-picker.js +3 -3
  45. package/dist/tui-ink/input-box.d.ts +1 -6
  46. package/dist/tui-ink/input-box.js +14 -35
  47. package/dist/tui-ink/markdown.js +11 -9
  48. package/dist/tui-ink/message-list.d.ts +5 -1
  49. package/dist/tui-ink/message-list.js +15 -2
  50. package/dist/tui-ink/model-picker.d.ts +3 -1
  51. package/dist/tui-ink/model-picker.js +25 -14
  52. package/dist/tui-ink/plan-confirm.js +1 -1
  53. package/dist/tui-ink/subagent-view.d.ts +3 -1
  54. package/dist/tui-ink/theme.d.ts +19 -2
  55. package/dist/tui-ink/theme.js +25 -8
  56. package/dist/tui-ink/welcome.d.ts +3 -2
  57. package/dist/tui-ink/welcome.js +10 -6
  58. package/dist/types.d.ts +16 -30
  59. package/dist/variant/variant-resolver.d.ts +13 -0
  60. package/dist/variant/variant-resolver.js +26 -0
  61. package/package.json +9 -3
@@ -234,7 +234,7 @@ function renderBody(stage, theme) {
234
234
  case "done":
235
235
  return (_jsx(Box, { flexDirection: "column", children: stage.summary.split("\n").map((line, i) => (_jsx(Text, { children: line }, i))) }));
236
236
  case "error":
237
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "red", children: ["\u274C ", stage.message] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u6309 Enter \u5173\u95ED\u3002\u53EF\u4EE5\u7A0D\u540E\u518D /feishu setup \u91CD\u8BD5\u3002" }) })] }));
237
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.error, children: ["\u274C ", stage.message] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u6309 Enter \u5173\u95ED\u3002\u53EF\u4EE5\u7A0D\u540E\u518D /feishu setup \u91CD\u8BD5\u3002" }) })] }));
238
238
  }
239
239
  }
240
240
  function BindingForm({ stage, theme }) {
@@ -258,8 +258,8 @@ function BindingForm({ stage, theme }) {
258
258
  const isActive = stage.field === field;
259
259
  const isDone = !isActive && value && fieldOrderIndex(stage.field) > fieldOrderIndex(field);
260
260
  const marker = isActive ? "› " : isDone ? "✓ " : " ";
261
- return (_jsxs(Box, { flexDirection: "column", marginBottom: isActive ? 1 : 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? theme.accent : isDone ? "green" : theme.muted, children: [marker, meta.label, ":"] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [value, isActive ? "▌" : ""] }) })] }), isActive && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.muted, children: meta.hint }) }))] }, field));
262
- }), stage.error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "red", children: stage.error }) }))] }));
261
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: isActive ? 1 : 0, children: [_jsxs(Box, { children: [_jsxs(Text, { color: isActive ? theme.accent : isDone ? theme.success : theme.muted, children: [marker, meta.label, ":"] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { children: [value, isActive ? "▌" : ""] }) })] }), isActive && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: theme.muted, children: meta.hint }) }))] }, field));
262
+ }), stage.error && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.error, children: stage.error }) }))] }));
263
263
  }
264
264
  function fieldOrderIndex(field) {
265
265
  return field === "chatId" ? 0 : field === "cwd" ? 1 : 2;
@@ -53,8 +53,6 @@ export declare function isCtrlLetterInput(input: string, key: {
53
53
  export declare function isCtrlCInput(input: string, key: {
54
54
  ctrl?: boolean;
55
55
  }): boolean;
56
- export declare function shouldUseLineComposerFrame(_background: string): boolean;
57
- export declare function composerSurfaceBackground(lineFrame: boolean, background: string, inputBg: string): string;
58
56
  export declare function shouldUseHardwareComposerCursor(env?: Record<string, string | undefined>): boolean;
59
57
  export declare function composerVerticalArrowDirection(key: {
60
58
  upArrow?: boolean;
@@ -63,12 +61,9 @@ export declare function composerVerticalArrowDirection(key: {
63
61
  }): "up" | "down" | undefined;
64
62
  export declare function resolveSoftwareCursorCellStyle(input: {
65
63
  visible: boolean;
66
- cursorBackground: string;
67
- cursorForeground: string;
68
64
  textColor: string;
69
- rowBackground?: string;
70
65
  }): {
71
- backgroundColor?: string;
66
+ inverse: boolean;
72
67
  color: string;
73
68
  };
74
69
  /**
@@ -14,7 +14,6 @@ import { submitPayloadFingerprint } from "./submit-dedupe.js";
14
14
  export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
15
15
  import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
16
16
  import { imageDisplayLabel, stripInlineImageLabels } from "../tui/image-display.js";
17
- const MIN_VISIBLE_LINES = 3;
18
17
  const MAX_VISIBLE_LINES = 6;
19
18
  const PADDING_X = 1;
20
19
  const PROMPT = " > ";
@@ -47,12 +46,6 @@ export function isCtrlLetterInput(input, key, letter) {
47
46
  export function isCtrlCInput(input, key) {
48
47
  return isCtrlLetterInput(input, key, "c");
49
48
  }
50
- export function shouldUseLineComposerFrame(_background) {
51
- return true;
52
- }
53
- export function composerSurfaceBackground(lineFrame, background, inputBg) {
54
- return lineFrame ? background : inputBg;
55
- }
56
49
  export function shouldUseHardwareComposerCursor(env = process.env) {
57
50
  return env.BUBBLE_HARDWARE_CURSOR === "1";
58
51
  }
@@ -64,16 +57,10 @@ export function composerVerticalArrowDirection(key) {
64
57
  return undefined;
65
58
  }
66
59
  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
- };
60
+ // Inverse video swaps the cell's real foreground/background at the terminal
61
+ // level, so the cursor block stays visible on any terminal palette without
62
+ // guessing a contrasting pair.
63
+ return { inverse: input.visible, color: input.textColor };
77
64
  }
78
65
  /**
79
66
  * Split a composer line around the cursor so the cell under it can render as
@@ -995,8 +982,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
995
982
  const classifyVerticalArrow = (direction) => {
996
983
  performVerticalArrowRef.current(direction);
997
984
  };
998
- const lineFrame = shouldUseLineComposerFrame(theme.background);
999
- const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
985
+ const minVisibleLines = 1;
1000
986
  const totalLines = Math.max(visualLines.length, 1);
1001
987
  const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
1002
988
  let scrollOffset = 0;
@@ -1130,25 +1116,18 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1130
1116
  });
1131
1117
  // Reference cursorTick so the effect re-runs on the forced render pass.
1132
1118
  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
1119
  const cursorCellStyle = resolveSoftwareCursorCellStyle({
1138
1120
  visible: softwareCursorVisible,
1139
- cursorBackground: theme.inputText,
1140
- cursorForeground: cursorFg,
1141
1121
  textColor: theme.inputText,
1142
- rowBackground: rowBg,
1143
1122
  });
1144
1123
  const moreBelow = totalLines - scrollOffset - visibleLines;
1145
1124
  const filledLine = (value) => {
1146
1125
  const visibleWidth = visualWidth(value);
1147
1126
  return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
1148
1127
  };
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) => {
1128
+ 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
1129
  if (row.kind === "pad") {
1151
- return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
1130
+ return (_jsx(Text, { children: " ".repeat(contentWidth) }, row.key));
1152
1131
  }
1153
1132
  const { text: line, visualIdx } = row;
1154
1133
  const visualLine = visualLines[visualIdx];
@@ -1167,17 +1146,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1167
1146
  });
1168
1147
  const renderedLine = renderedSegments.map((segment) => segment.text).join("");
1169
1148
  const fill = " ".repeat(Math.max(0, lineWidth - visualWidth(renderedLine)));
1170
- return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: composerBg, ref: isCursorLine
1149
+ return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: theme.background, ref: isCursorLine
1171
1150
  ? (el) => {
1172
1151
  cursorLineRef.current = el;
1173
1152
  }
1174
- : undefined, children: [_jsx(Text, { backgroundColor: rowBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1153
+ : undefined, children: [_jsx(Text, { color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
1175
1154
  if (segment.kind === "cursor") {
1176
- return (_jsx(Text, { backgroundColor: cursorCellStyle.backgroundColor, color: cursorCellStyle.color, children: segment.text }, index));
1155
+ return (_jsx(Text, { inverse: cursorCellStyle.inverse, color: cursorCellStyle.color, children: segment.text }, index));
1177
1156
  }
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
1157
+ return (_jsx(Text, { color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
1158
+ }), _jsx(Text, { children: fill })] }, visualIdx));
1159
+ }), 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
1160
  .slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
1182
1161
  .map((cmd, visibleIndex) => {
1183
1162
  const i = suggestionOffset + visibleIndex;
@@ -1190,6 +1169,6 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
1190
1169
  const i = suggestionOffset + visibleIndex;
1191
1170
  const maxWidth = Math.max(10, Math.min(80, contentWidth - 2));
1192
1171
  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));
1172
+ return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, { color: theme.accent, bold: true, children: `› ${label}` })) : (_jsx(Text, { children: ` ${label}` })) }, s.path));
1194
1173
  }), 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
1174
  }
@@ -320,16 +320,17 @@ function appendInlineSegment(segments, text, style) {
320
320
  segments.push(next);
321
321
  }
322
322
  }
323
- function renderSegmentNodes(segments, keyPrefix) {
324
- return segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`)));
323
+ function renderSegmentNodes(segments, keyPrefix, codeColor) {
324
+ return segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? codeColor : undefined, children: segment.text }, `${keyPrefix}-${index}`)));
325
325
  }
326
- function renderInlineSegments(text, keyPrefix, style = {}) {
327
- return renderSegmentNodes(parseMarkdownInlineSegments(text, style), keyPrefix);
326
+ function renderInlineSegments(text, keyPrefix, codeColor, style = {}) {
327
+ return renderSegmentNodes(parseMarkdownInlineSegments(text, style), keyPrefix, codeColor);
328
328
  }
329
329
  function inlinePlainText(text) {
330
330
  return parseMarkdownInlineSegments(text).map((segment) => segment.text).join("");
331
331
  }
332
332
  function InlineText({ text, maxWidth }) {
333
+ const theme = useTheme();
333
334
  const { columns } = useTerminalSize();
334
335
  // Pre-wrap CJK-aware so Ink's space-only wrap-ansi can't chop "、"-joined runs
335
336
  // mid-token. A caller-supplied maxWidth is exact (it already nets out the
@@ -338,10 +339,10 @@ function InlineText({ text, maxWidth }) {
338
339
  // wrapping a few cells early is invisible, a mid-token chop is not.
339
340
  const effectiveWidth = maxWidth ?? (columns ? Math.max(20, columns - 10) : undefined);
340
341
  if (effectiveWidth === undefined) {
341
- return _jsx(Text, { children: renderInlineSegments(text, "inline") });
342
+ return _jsx(Text, { children: renderInlineSegments(text, "inline", theme.inlineCode) });
342
343
  }
343
344
  const wrapped = wrapInlineSegments(parseMarkdownInlineSegments(text), effectiveWidth);
344
- return (_jsx(_Fragment, { children: wrapped.map((segments, li) => (_jsx(Text, { children: segments.length ? renderSegmentNodes(segments, `inline-${li}`) : " " }, li))) }));
345
+ return (_jsx(_Fragment, { children: wrapped.map((segments, li) => (_jsx(Text, { children: segments.length ? renderSegmentNodes(segments, `inline-${li}`, theme.inlineCode) : " " }, li))) }));
345
346
  }
346
347
  function CodeBlock({ lang, lines }) {
347
348
  const theme = useTheme();
@@ -378,6 +379,7 @@ function CodeBlock({ lang, lines }) {
378
379
  return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [lang && _jsx(Text, { color: theme.muted, children: lang }), _jsx(Box, { flexDirection: "column", children: highlighted.map((line, i) => (_jsx(Text, { children: line || " " }, i))) })] }));
379
380
  }
380
381
  function TableBlock({ headers, rows, maxWidth, }) {
382
+ const theme = useTheme();
381
383
  const { columns: termWidth } = useTerminalSize();
382
384
  const colCount = headers.length;
383
385
  // Reserve a buffer so the table fits even when wrapped inside an indented
@@ -425,7 +427,7 @@ function TableBlock({ headers, rows, maxWidth, }) {
425
427
  return lines;
426
428
  });
427
429
  const height = Math.max(1, ...wrapped.map((lines) => lines.length));
428
- return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: height }, (_, line) => (_jsxs(Text, { children: [`${g.v} `, wrapped.map((cellLines, i) => (_jsxs(React.Fragment, { children: [renderCellLine(cellLines[line] ?? [], widths[i] ?? 4, `${keyPrefix}-cell-${i}-${line}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
430
+ return (_jsx(Box, { flexDirection: "column", children: Array.from({ length: height }, (_, line) => (_jsxs(Text, { children: [`${g.v} `, wrapped.map((cellLines, i) => (_jsxs(React.Fragment, { children: [renderCellLine(cellLines[line] ?? [], widths[i] ?? 4, `${keyPrefix}-cell-${i}-${line}`, theme.inlineCode), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, `${keyPrefix}-line-${line}`))) }, keyPrefix));
429
431
  };
430
432
  return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
431
433
  }
@@ -491,10 +493,10 @@ export function allocateColumnWidths(natural, available) {
491
493
  }
492
494
  return widths;
493
495
  }
494
- function renderCellLine(segments, width, keyPrefix) {
496
+ function renderCellLine(segments, width, keyPrefix, codeColor) {
495
497
  const padding = " ".repeat(Math.max(0, width - inlineSegmentsWidth(segments)));
496
498
  return [
497
- ...segments.map((segment, index) => (_jsx(Text, { bold: segment.bold, italic: segment.italic, color: segment.code ? "#a78bfa" : undefined, children: segment.text }, `${keyPrefix}-${index}`))),
499
+ ...renderSegmentNodes(segments, keyPrefix, codeColor),
498
500
  _jsx(Text, { children: padding }, `${keyPrefix}-pad`),
499
501
  ];
500
502
  }
@@ -31,7 +31,11 @@ interface MessageListProps {
31
31
  * a freshly-cleared screen instead of appending duplicates.
32
32
  */
33
33
  staticGeneration?: number;
34
- /** Horizontal padding applied inside each committed/streaming row. */
34
+ /**
35
+ * Horizontal padding applied inside each committed/streaming row. Wrap
36
+ * budgets net this out via TRANSCRIPT_ROW_PADDING (= both sides of the
37
+ * default of 1) — keep the two in sync if this ever changes.
38
+ */
35
39
  paddingX?: number;
36
40
  /**
37
41
  * Maximum height (rows) for the live/dynamic region. The in-progress turn is
@@ -12,6 +12,16 @@ import { latestSubagentNote, sortSubagents, subagentDescriptor, subagentLabel, s
12
12
  import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
13
13
  import { splitImageDisplayContent } from "../tui/image-display.js";
14
14
  const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
15
+ /**
16
+ * Every transcript row — each committed <Static> item and the DynamicClamp
17
+ * live region — wraps its content in paddingX={1}: one column of inset on
18
+ * EACH side. Any wrap budget derived from terminalColumns inside a row must
19
+ * net out these two columns. Forgetting them hands the pre-wrapper a budget
20
+ * two columns wider than the real room, and a dense CJK line that packs flush
21
+ * against that budget renders past the terminal edge — the terminal then
22
+ * hard-wraps the last glyph onto a stray row at column 0.
23
+ */
24
+ const TRANSCRIPT_ROW_PADDING = 2;
15
25
  export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
16
26
  const theme = useTheme();
17
27
  const hasStreaming = !!(streamingContent ||
@@ -195,7 +205,9 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
195
205
  // cell right and would overflow. Reserve that extra cell up front so the
196
206
  // pre-wrap never packs a line the terminal then hard-wraps.
197
207
  const gutter = ambiguousIsWide() ? 6 : 5;
198
- const available = terminalColumns ? Math.max(20, terminalColumns - gutter) : undefined;
208
+ const available = terminalColumns
209
+ ? Math.max(20, terminalColumns - gutter - TRANSCRIPT_ROW_PADDING)
210
+ : undefined;
199
211
  const trimmed = visible.trim();
200
212
  return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
201
213
  }
@@ -347,7 +359,8 @@ function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromP
347
359
  }
348
360
  function PendingInputMessagesBlock({ messages, terminalColumns, title, hint, bulletColor, }) {
349
361
  const theme = useTheme();
350
- const contentWidth = Math.max(20, terminalColumns - 5);
362
+ // Inset per row: marginLeft (2) + "↳ " (2) + the transcript row padding.
363
+ const contentWidth = Math.max(20, terminalColumns - 4 - TRANSCRIPT_ROW_PADDING);
351
364
  return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: bulletColor, children: "\u2022 " }), _jsxs(Text, { bold: true, color: theme.inputText, children: [title, " "] }), _jsxs(Text, { color: theme.dim, children: ["(", hint, ")"] })] }), messages.flatMap((message, messageIndex) => {
352
365
  const { bodyLines, referenceLines } = splitImageDisplayContent(message.content || " ");
353
366
  const wrappedBody = bodyLines.flatMap((line) => wrapByVisualWidth(line || " ", contentWidth));
@@ -65,8 +65,10 @@ export interface ProviderPickerProps {
65
65
  onSelect: (providerId: string) => void;
66
66
  onCancel: () => void;
67
67
  title?: string;
68
+ /** Fires whenever the highlighted row changes (and once on mount) — lets callers live-preview the selection. */
69
+ onHighlight?: (providerId: string) => void;
68
70
  }
69
- export declare function ProviderPicker({ providers, current, onSelect, onCancel, title }: ProviderPickerProps): import("react/jsx-runtime").JSX.Element;
71
+ export declare function ProviderPicker({ providers, current, onSelect, onCancel, title, onHighlight }: ProviderPickerProps): import("react/jsx-runtime").JSX.Element;
70
72
  export interface KeyPickerProps {
71
73
  providerName: string;
72
74
  onSubmit: (key: string) => void;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useMemo } from "react";
3
3
  import { Box, Text, useInput, usePaste, useStdout } from "ink";
4
4
  import { isKeyReleaseEvent } from "./key-events.js";
@@ -7,7 +7,7 @@ import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from ".
7
7
  import { listBuiltinModels } from "../model-catalog.js";
8
8
  import { padVisual, truncateVisual } from "../text-display.js";
9
9
  import { hasTerminalMouseSequence } from "./terminal-mouse.js";
10
- import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
10
+ import { getAvailableThinkingLevels, isThinkingOnlyLevels, isThinkingToggleModel, normalizeThinkingLevel } from "../provider-transform.js";
11
11
  export { padVisual, truncateVisual } from "../text-display.js";
12
12
  export function resolvePickerKeyAction(input, key) {
13
13
  if (key.escape)
@@ -78,25 +78,24 @@ export function padPickerRows(rows, bodyRows, width) {
78
78
  }
79
79
  return padded;
80
80
  }
81
- // MiniMax models expose thinking as a binary on/off switch (the API's `thinking`
82
- // param is disabled|adaptive — there's no graded effort), so render the "on"
83
- // level as on/off instead of our internal "medium". Scoped to MiniMax only —
84
- // other 2-level models (e.g. GLM toggles) keep their effort labels.
85
- function isMiniMaxToggleModel(modelId) {
86
- return modelId.toLowerCase().includes("minimax");
81
+ function isThinkingToggleOption(option) {
82
+ const decoded = decodeModel(option.id);
83
+ return isThinkingToggleModel(decoded.providerId || option.providerBadge, decoded.modelId);
87
84
  }
88
85
  export function formatReasoningLevelsLabel(levels, asToggle = false) {
89
- const normalized = levels.length > 0 ? levels : ["off"];
86
+ if (isThinkingOnlyLevels(levels))
87
+ return "on";
90
88
  if (asToggle)
91
89
  return "thinking on/off";
92
- return `effort ${normalized.join("/")}`;
90
+ const normalized = levels.length > 0 ? levels : ["off"];
91
+ return normalized.join("/");
93
92
  }
94
93
  export function formatModelPickerRow(option, options) {
95
94
  const width = Math.max(24, options.width);
96
95
  const marker = options.selected ? "> " : " ";
97
96
  const label = option.label.replace(/\s+/g, " ").trim();
98
97
  const provider = option.providerBadge.replace(/\s+/g, " ").trim();
99
- const effort = formatReasoningLevelsLabel(option.reasoningLevels, isMiniMaxToggleModel(option.id));
98
+ const effort = formatReasoningLevelsLabel(option.reasoningLevels, isThinkingToggleOption(option));
100
99
  const current = options.current ? " ●" : "";
101
100
  const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
102
101
  const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
@@ -345,7 +344,14 @@ export function ModelPicker({ registry, current, currentThinkingLevel, recent, o
345
344
  row,
346
345
  selected: rawModelRows[index]?.selected ?? false,
347
346
  }));
348
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: phase.kind === "effort" ? "Select Reasoning Effort" : "Select Model" }), phase.kind === "effort" ? (_jsx(EffortPickerView, { model: phase.model, selectedIndex: phase.selectedIndex, bodyRows: bodyRows, rowWidth: rowWidth })) : (_jsxs(_Fragment, { children: [_jsx(SearchField, { query: query, placeholder: "Type to search models...", width: rowWidth }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter choose effort \u00B7 Esc cancel \u00B7 Backspace clear" })] })), phase.kind === "model" && _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", marginTop: 1, children: modelRows.map(({ key, row, selected }) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: selected ? theme.accent : (key === "no-results" ? theme.muted : undefined), bold: selected, children: row }) }, key))) })] }));
347
+ const highlightedOption = options[clampPickerIndex(selectedIndex, options.length)];
348
+ const enterAction = highlightedOption && shouldOpenEffortPicker(highlightedOption)
349
+ ? (isThinkingToggleOption(highlightedOption) ? "choose mode" : "choose effort")
350
+ : "select";
351
+ const effortTitle = phase.kind === "effort" && isThinkingToggleOption(phase.model)
352
+ ? "Select Thinking Mode"
353
+ : "Select Reasoning Effort";
354
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: phase.kind === "effort" ? effortTitle : "Select Model" }), phase.kind === "effort" ? (_jsx(EffortPickerView, { model: phase.model, selectedIndex: phase.selectedIndex, bodyRows: bodyRows, rowWidth: rowWidth })) : (_jsxs(_Fragment, { children: [_jsx(SearchField, { query: query, placeholder: "Type to search models...", width: rowWidth }), _jsxs(Text, { color: theme.muted, children: ["\u2191/\u2193 navigate \u00B7 Enter ", enterAction, " \u00B7 Esc cancel \u00B7 Backspace clear"] })] })), phase.kind === "model" && _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", marginTop: 1, children: modelRows.map(({ key, row, selected }) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: selected ? theme.accent : (key === "no-results" ? theme.muted : undefined), bold: selected, children: row }) }, key))) })] }));
349
355
  }
350
356
  function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
351
357
  const theme = useTheme();
@@ -355,7 +361,7 @@ function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
355
361
  row: formatEffortPickerRow(level, {
356
362
  selected: index === safeSelectedIndex,
357
363
  width: rowWidth,
358
- asToggle: isMiniMaxToggleModel(model.id),
364
+ asToggle: isThinkingToggleOption(model),
359
365
  }),
360
366
  selected: index === safeSelectedIndex,
361
367
  }));
@@ -448,7 +454,7 @@ function reasoningLevelsForModel(model) {
448
454
  const { providerId, modelId } = decodeModel(model);
449
455
  return getAvailableThinkingLevels(providerId || "openai", modelId);
450
456
  }
451
- export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
457
+ export function ProviderPicker({ providers, current, onSelect, onCancel, title, onHighlight }) {
452
458
  const theme = useTheme();
453
459
  const { stdout } = useStdout();
454
460
  const termHeight = stdout?.rows || 24;
@@ -457,6 +463,11 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
457
463
  const idx = providers.findIndex((p) => p.id === current);
458
464
  return idx >= 0 ? idx : 0;
459
465
  });
466
+ useEffect(() => {
467
+ const p = providers[selectedIndex];
468
+ if (p)
469
+ onHighlight?.(p.id);
470
+ }, [selectedIndex]);
460
471
  useInput((input, key) => {
461
472
  if (isKeyReleaseEvent(key))
462
473
  return;
@@ -110,6 +110,6 @@ export function PlanConfirm({ initialPlan, onApprove, onReject }) {
110
110
  return (_jsx(Text, { children: line || " " }, index));
111
111
  }
112
112
  const safe = line || " ";
113
- return (_jsxs(Box, { children: [_jsx(Text, { children: safe.slice(0, cursorCol) }), _jsx(Text, { backgroundColor: "white", color: "black", children: safe[cursorCol] || " " }), _jsx(Text, { children: safe.slice(cursorCol + 1) })] }, index));
113
+ return (_jsxs(Box, { children: [_jsx(Text, { children: safe.slice(0, cursorCol) }), _jsx(Text, { inverse: true, children: safe[cursorCol] || " " }), _jsx(Text, { children: safe.slice(cursorCol + 1) })] }, index));
114
114
  }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: [_jsx(Text, { color: theme.accent, bold: true, children: "\u2303S" }), " save & approve \u00A0\u00A0", _jsx(Text, { color: theme.accent, bold: true, children: "esc" }), " cancel edit"] }) })] }));
115
115
  }
@@ -26,7 +26,9 @@ export declare function subagentDescriptor(subagent: SubagentDisplay, includeThi
26
26
  export declare function subagentStatusColor(status: string | undefined, theme: Theme): string;
27
27
  export declare function subagentSummary(subagents: SubagentDisplay[]): string;
28
28
  export declare function sortSubagents(subagents: SubagentDisplay[]): SubagentDisplay[];
29
- /** A spawn_agent (one member), an agent_team/agent_batch, or a run_workflow (a group of members). */
29
+ /** A spawn_agent (one member) or a run_workflow (a group of members). The
30
+ * "team"/"batch" kinds survive only to render transcripts recorded before
31
+ * those tools were removed (2026-07-06). */
30
32
  export interface SubagentGroup {
31
33
  id: string;
32
34
  kind: "single" | "team" | "batch" | "workflow";
@@ -15,7 +15,13 @@ export interface Theme {
15
15
  error: string;
16
16
  warning: string;
17
17
  success: string;
18
- background: string;
18
+ /**
19
+ * Painted canvas color. `undefined` means "do not paint" — the terminal's
20
+ * own background shows through, so the app never fights the user's terminal
21
+ * palette (e.g. forcing pure black inside a soft-dark terminal). Users can
22
+ * still force a painted canvas via `theme.overrides.background` in config.
23
+ */
24
+ background: string | undefined;
19
25
  accent: string;
20
26
  /** Welcome banner border. */
21
27
  bannerBorder: string;
@@ -40,7 +46,8 @@ export interface Theme {
40
46
  toolResult: string;
41
47
  toolError: string;
42
48
  toolPending: string;
43
- code: string;
49
+ /** Markdown inline `code` spans. */
50
+ inlineCode: string;
44
51
  traceAction: string;
45
52
  traceCount: string;
46
53
  traceDetail: string;
@@ -50,8 +57,10 @@ export interface Theme {
50
57
  userMessageBg: string;
51
58
  userMessageText: string;
52
59
  userRail: string;
60
+ /** Painted band backgrounds for diff lines inside user-message cards. */
53
61
  diffAdd: string;
54
62
  diffRemove: string;
63
+ /** Foregrounds for +/- diff lines rendered without a painted background. */
55
64
  diffAddFg: string;
56
65
  diffRemoveFg: string;
57
66
  }
@@ -62,6 +71,14 @@ export declare const darkTheme: Theme;
62
71
  * terminal background.
63
72
  */
64
73
  export declare const lightTheme: Theme;
74
+ /**
75
+ * Decide whether the canvas needs painting. Auto mode always inherits the
76
+ * terminal's own background, and so does a forced theme that matches the
77
+ * detected terminal. A forced theme that mismatches (e.g. `/theme light`
78
+ * inside a dark terminal) paints its canvas, because its foregrounds are
79
+ * tuned for the opposite background and would otherwise be unreadable.
80
+ */
81
+ export declare function canvasBackgroundFor(mode: ThemeMode, resolved: ResolvedTheme, terminalTheme: ResolvedTheme): string | undefined;
65
82
  export declare const ThemeProvider: import("react").Provider<Theme>;
66
83
  export declare function useTheme(): Theme;
67
84
  /** Build the active palette given a resolved mode and optional overrides. */
@@ -14,7 +14,7 @@ export const darkTheme = {
14
14
  error: "red",
15
15
  warning: "yellow",
16
16
  success: "green",
17
- background: "#0A0A0A",
17
+ background: undefined,
18
18
  accent: "cyan",
19
19
  bannerBorder: "#38bdf8",
20
20
  bannerGradientFrom: "#67e8f9",
@@ -37,7 +37,7 @@ export const darkTheme = {
37
37
  toolResult: "gray",
38
38
  toolError: "red",
39
39
  toolPending: "yellow",
40
- code: "yellow",
40
+ inlineCode: "#a78bfa",
41
41
  traceAction: "#E89A6B",
42
42
  traceCount: "#c9c1bd",
43
43
  traceDetail: "gray",
@@ -49,8 +49,8 @@ export const darkTheme = {
49
49
  userRail: "#8A7FC6",
50
50
  diffAdd: "#1a3d1a",
51
51
  diffRemove: "#3d1a1a",
52
- diffAddFg: "#9CDCFE",
53
- diffRemoveFg: "#F48771",
52
+ diffAddFg: "green",
53
+ diffRemoveFg: "red",
54
54
  };
55
55
  /**
56
56
  * Light palette tuned for paper-neutral surfaces, blue focus/user rails, warm
@@ -63,7 +63,7 @@ export const lightTheme = {
63
63
  error: "#B62633",
64
64
  warning: "#8B4A00",
65
65
  success: "#2F7D4A",
66
- background: "#FCFCFA",
66
+ background: undefined,
67
67
  accent: "#8B4A00",
68
68
  bannerBorder: "#356FD2",
69
69
  bannerGradientFrom: "#0E7490",
@@ -86,7 +86,7 @@ export const lightTheme = {
86
86
  toolResult: "#171717",
87
87
  toolError: "#B62633",
88
88
  toolPending: "#8B4A00",
89
- code: "#2F7D4A",
89
+ inlineCode: "#6D28D9",
90
90
  traceAction: "#8B4A00",
91
91
  traceCount: "#6F7377",
92
92
  traceDetail: "#8B9094",
@@ -98,9 +98,26 @@ export const lightTheme = {
98
98
  userRail: "#356FD2",
99
99
  diffAdd: "#D7E8D8",
100
100
  diffRemove: "#F7DADC",
101
- diffAddFg: "#173D2D",
102
- diffRemoveFg: "#5D1922",
101
+ diffAddFg: "#2F7D4A",
102
+ diffRemoveFg: "#B62633",
103
103
  };
104
+ /** Canvas colors painted only when a forced theme mismatches the terminal. */
105
+ const paintedCanvas = {
106
+ dark: "#0A0A0A",
107
+ light: "#FCFCFA",
108
+ };
109
+ /**
110
+ * Decide whether the canvas needs painting. Auto mode always inherits the
111
+ * terminal's own background, and so does a forced theme that matches the
112
+ * detected terminal. A forced theme that mismatches (e.g. `/theme light`
113
+ * inside a dark terminal) paints its canvas, because its foregrounds are
114
+ * tuned for the opposite background and would otherwise be unreadable.
115
+ */
116
+ export function canvasBackgroundFor(mode, resolved, terminalTheme) {
117
+ if (mode === "auto" || resolved === terminalTheme)
118
+ return undefined;
119
+ return paintedCanvas[resolved];
120
+ }
104
121
  const ThemeContext = createContext(darkTheme);
105
122
  export const ThemeProvider = ThemeContext.Provider;
106
123
  export function useTheme() {
@@ -9,6 +9,7 @@ interface WelcomeBannerProps {
9
9
  /** Session identifier (session file basename). */
10
10
  sessionLabel?: string;
11
11
  providerId?: string;
12
+ modelId?: string;
12
13
  modelLabel?: string;
13
14
  /** Active thinking level, rendered as part of the model unit (e.g. "xhigh"). */
14
15
  thinkingLabel?: string;
@@ -19,6 +20,6 @@ interface WelcomeVisibilityInput {
19
20
  }
20
21
  export declare function shouldShowWelcomeBanner({ startedWithVisibleHistory, }: WelcomeVisibilityInput): boolean;
21
22
  export declare function lerpColor(from: string, to: string, t: number): string;
22
- export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
23
- export declare function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }: Pick<WelcomeBannerProps, "providerId" | "modelLabel" | "thinkingLabel" | "tips">): string;
23
+ export declare function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }: WelcomeBannerProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function formatModelLine({ providerId, modelId, modelLabel, thinkingLabel, tips, }: Pick<WelcomeBannerProps, "providerId" | "modelId" | "modelLabel" | "thinkingLabel" | "tips">): string;
24
25
  export {};
@@ -2,6 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { Box, Text } from "ink";
3
3
  import { createRequire } from "node:module";
4
4
  import { useTheme } from "./theme.js";
5
+ import { isThinkingOnlyModel, isThinkingToggleModel } from "../provider-transform.js";
5
6
  const require = createRequire(import.meta.url);
6
7
  const PACKAGE_VERSION = readPackageVersion();
7
8
  const COMPACT_LOGO = [
@@ -28,11 +29,12 @@ function GradientText({ text, from, to }) {
28
29
  const chars = [...text];
29
30
  return (_jsx(_Fragment, { children: chars.map((ch, i) => (_jsx(Text, { bold: true, color: lerpColor(from, to, chars.length <= 1 ? 0 : i / (chars.length - 1)), children: ch }, `ch-${i}`))) }));
30
31
  }
31
- export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelLabel, thinkingLabel, }) {
32
+ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessionLabel, providerId, modelId, modelLabel, thinkingLabel, }) {
32
33
  const theme = useTheme();
33
34
  const effectiveWidth = Math.max(24, Math.min(terminalColumns - 2, 96));
34
35
  const modelLine = formatModelLine({
35
36
  providerId,
37
+ modelId,
36
38
  modelLabel,
37
39
  thinkingLabel,
38
40
  tips,
@@ -48,14 +50,16 @@ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, sessio
48
50
  const labelWidth = Math.max(...infoRows.map((row) => row.label.length)) + 1;
49
51
  return (_jsxs(Box, { width: effectiveWidth, flexDirection: "column", marginBottom: 1, borderStyle: "round", borderColor: theme.bannerBorder, paddingX: 2, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", marginRight: 2, flexShrink: 0, children: COMPACT_LOGO.map((line, rowIndex) => (_jsx(Text, { bold: true, color: lerpColor(theme.bannerGradientFrom, theme.bannerGradientTo, COMPACT_LOGO.length <= 1 ? 0 : rowIndex / (COMPACT_LOGO.length - 1)), children: line }, `logo-row-${rowIndex}`))) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, flexShrink: 1, children: [_jsx(Box, { children: _jsx(GradientText, { text: "Welcome to Bubble!", from: theme.bannerGradientFrom, to: theme.bannerGradientTo }) }), _jsx(Text, { color: theme.muted, wrap: "wrap", children: "I am a cat and you can send /help for help information." })] })] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: infoRows.map((row) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: theme.dim, children: row.label.padEnd(labelWidth) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { color: row.color, wrap: "wrap", children: row.value }) })] }, row.label))) }), updateNotice && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.accent, children: updateNotice }) }))] }));
50
52
  }
51
- export function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }) {
53
+ export function formatModelLine({ providerId, modelId, modelLabel, thinkingLabel, tips, }) {
52
54
  const parts = [];
53
- // MiniMax thinking is a binary toggle (adaptive thinking), so label it
54
- // "thinking mode" rather than "<level> effort"; and its provider id
55
- // ("minimax-anthropic") is redundant with the model name, so omit it.
55
+ // Binary thinking toggles use an internal placeholder level, so label them
56
+ // "thinking mode" rather than "<level> effort". MiniMax provider ids are
57
+ // redundant with the model name, so omit them.
56
58
  const isMiniMax = (providerId || "").toLowerCase().includes("minimax");
59
+ const isThinkingToggle = isThinkingToggleModel(providerId, modelId);
60
+ const isThinkingOnly = isThinkingOnlyModel(providerId, modelId);
57
61
  if (modelLabel) {
58
- if (thinkingLabel && isMiniMax)
62
+ if (thinkingLabel && (isThinkingToggle || isThinkingOnly))
59
63
  parts.push(modelLabel, "thinking mode");
60
64
  else if (thinkingLabel)
61
65
  parts.push(`${modelLabel} with ${thinkingLabel} effort`);