@bubblebrain-ai/bubble 0.0.24 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +1 -1
  2. package/dist/config.d.ts +3 -0
  3. package/dist/config.js +22 -6
  4. package/dist/goal/format.js +34 -4
  5. package/dist/goal/store.d.ts +3 -0
  6. package/dist/goal/store.js +14 -1
  7. package/dist/goal/usage.d.ts +2 -0
  8. package/dist/goal/usage.js +3 -0
  9. package/dist/main.js +23 -42
  10. package/dist/provider.js +20 -5
  11. package/dist/tui/detect-theme.d.ts +1 -0
  12. package/dist/tui/detect-theme.js +23 -0
  13. package/dist/tui/image-display.d.ts +13 -0
  14. package/dist/tui/image-display.js +49 -0
  15. package/dist/tui/input-history.d.ts +37 -6
  16. package/dist/tui/input-history.js +194 -23
  17. package/dist/tui/model-switch.d.ts +42 -0
  18. package/dist/tui/model-switch.js +55 -0
  19. package/dist/tui-ink/app.d.ts +32 -2
  20. package/dist/tui-ink/app.js +1360 -522
  21. package/dist/tui-ink/approval/select.js +10 -0
  22. package/dist/tui-ink/detect-theme.d.ts +1 -2
  23. package/dist/tui-ink/detect-theme.js +1 -87
  24. package/dist/tui-ink/display-history.d.ts +1 -0
  25. package/dist/tui-ink/display-history.js +11 -0
  26. package/dist/tui-ink/feedback-dialog.js +10 -0
  27. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  28. package/dist/tui-ink/footer.d.ts +1 -0
  29. package/dist/tui-ink/footer.js +8 -2
  30. package/dist/tui-ink/input-box.d.ts +70 -9
  31. package/dist/tui-ink/input-box.js +354 -120
  32. package/dist/tui-ink/input-history.d.ts +1 -16
  33. package/dist/tui-ink/input-history.js +1 -79
  34. package/dist/tui-ink/input-queue.d.ts +12 -0
  35. package/dist/tui-ink/input-queue.js +17 -0
  36. package/dist/tui-ink/key-events.d.ts +9 -0
  37. package/dist/tui-ink/key-events.js +8 -0
  38. package/dist/tui-ink/markdown.js +1 -1
  39. package/dist/tui-ink/message-list.d.ts +3 -1
  40. package/dist/tui-ink/message-list.js +42 -24
  41. package/dist/tui-ink/model-picker.d.ts +24 -2
  42. package/dist/tui-ink/model-picker.js +224 -20
  43. package/dist/tui-ink/plan-confirm.js +10 -0
  44. package/dist/tui-ink/question-dialog.js +10 -0
  45. package/dist/tui-ink/run.d.ts +10 -1
  46. package/dist/tui-ink/run.js +21 -28
  47. package/dist/tui-ink/session-picker.js +3 -0
  48. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  49. package/dist/tui-ink/submit-dedupe.js +25 -0
  50. package/dist/tui-ink/terminal-mouse.d.ts +13 -1
  51. package/dist/tui-ink/terminal-mouse.js +63 -21
  52. package/dist/tui-ink/theme.d.ts +6 -3
  53. package/dist/tui-ink/theme.js +10 -4
  54. package/dist/tui-ink/transcript-input.d.ts +8 -0
  55. package/dist/tui-ink/transcript-input.js +9 -0
  56. package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
  57. package/dist/tui-ink/transcript-viewport-math.js +1 -2
  58. package/dist/tui-ink/welcome.d.ts +1 -0
  59. package/dist/tui-ink/welcome.js +25 -28
  60. package/package.json +1 -5
  61. package/dist/tui/clipboard.d.ts +0 -1
  62. package/dist/tui/clipboard.js +0 -53
  63. package/dist/tui/escape-confirmation.d.ts +0 -15
  64. package/dist/tui/escape-confirmation.js +0 -30
  65. package/dist/tui/global-key-router.d.ts +0 -3
  66. package/dist/tui/global-key-router.js +0 -87
  67. package/dist/tui/markdown-inline.d.ts +0 -22
  68. package/dist/tui/markdown-inline.js +0 -68
  69. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  70. package/dist/tui/markdown-theme-rules.js +0 -164
  71. package/dist/tui/markdown-theme.d.ts +0 -5
  72. package/dist/tui/markdown-theme.js +0 -27
  73. package/dist/tui/opencode-spinner.d.ts +0 -22
  74. package/dist/tui/opencode-spinner.js +0 -216
  75. package/dist/tui/prompt-keybindings.d.ts +0 -42
  76. package/dist/tui/prompt-keybindings.js +0 -35
  77. package/dist/tui/render-signature.d.ts +0 -1
  78. package/dist/tui/render-signature.js +0 -7
  79. package/dist/tui/run.d.ts +0 -67
  80. package/dist/tui/run.js +0 -10166
  81. package/dist/tui/sidebar-mcp.d.ts +0 -31
  82. package/dist/tui/sidebar-mcp.js +0 -62
  83. package/dist/tui/sidebar-state.d.ts +0 -12
  84. package/dist/tui/sidebar-state.js +0 -69
  85. package/dist/tui/streaming-tool-args.d.ts +0 -15
  86. package/dist/tui/streaming-tool-args.js +0 -30
  87. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  88. package/dist/tui/tool-renderers/fallback.js +0 -75
  89. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  90. package/dist/tui/tool-renderers/registry.js +0 -11
  91. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  92. package/dist/tui/tool-renderers/subagent.js +0 -135
  93. package/dist/tui/tool-renderers/types.d.ts +0 -36
  94. package/dist/tui/tool-renderers/types.js +0 -1
  95. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  96. package/dist/tui/tool-renderers/write-preview.js +0 -32
  97. package/dist/tui/tool-renderers/write.d.ts +0 -6
  98. package/dist/tui/tool-renderers/write.js +0 -88
  99. package/dist/tui-opentui/app.d.ts +0 -54
  100. package/dist/tui-opentui/app.js +0 -1371
  101. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  102. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  103. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  104. package/dist/tui-opentui/approval/diff-view.js +0 -43
  105. package/dist/tui-opentui/approval/select.d.ts +0 -37
  106. package/dist/tui-opentui/approval/select.js +0 -91
  107. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  108. package/dist/tui-opentui/detect-theme.js +0 -87
  109. package/dist/tui-opentui/display-history.d.ts +0 -56
  110. package/dist/tui-opentui/display-history.js +0 -130
  111. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  112. package/dist/tui-opentui/edit-diff.js +0 -57
  113. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  114. package/dist/tui-opentui/feedback-dialog.js +0 -164
  115. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  116. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  117. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  118. package/dist/tui-opentui/file-mentions.js +0 -174
  119. package/dist/tui-opentui/footer.d.ts +0 -26
  120. package/dist/tui-opentui/footer.js +0 -40
  121. package/dist/tui-opentui/image-paste.d.ts +0 -54
  122. package/dist/tui-opentui/image-paste.js +0 -288
  123. package/dist/tui-opentui/input-box.d.ts +0 -32
  124. package/dist/tui-opentui/input-box.js +0 -462
  125. package/dist/tui-opentui/input-history.d.ts +0 -16
  126. package/dist/tui-opentui/input-history.js +0 -79
  127. package/dist/tui-opentui/markdown.d.ts +0 -66
  128. package/dist/tui-opentui/markdown.js +0 -127
  129. package/dist/tui-opentui/message-list.d.ts +0 -31
  130. package/dist/tui-opentui/message-list.js +0 -131
  131. package/dist/tui-opentui/model-picker.d.ts +0 -63
  132. package/dist/tui-opentui/model-picker.js +0 -450
  133. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  134. package/dist/tui-opentui/plan-confirm.js +0 -124
  135. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  136. package/dist/tui-opentui/question-dialog.js +0 -110
  137. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  138. package/dist/tui-opentui/recent-activity.js +0 -71
  139. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  140. package/dist/tui-opentui/run-session-picker.js +0 -28
  141. package/dist/tui-opentui/run.d.ts +0 -38
  142. package/dist/tui-opentui/run.js +0 -48
  143. package/dist/tui-opentui/session-picker.d.ts +0 -12
  144. package/dist/tui-opentui/session-picker.js +0 -120
  145. package/dist/tui-opentui/theme.d.ts +0 -89
  146. package/dist/tui-opentui/theme.js +0 -157
  147. package/dist/tui-opentui/todos.d.ts +0 -9
  148. package/dist/tui-opentui/todos.js +0 -45
  149. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  150. package/dist/tui-opentui/trace-groups.js +0 -455
  151. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  152. package/dist/tui-opentui/use-terminal-size.js +0 -5
  153. package/dist/tui-opentui/welcome.d.ts +0 -25
  154. package/dist/tui-opentui/welcome.js +0 -77
@@ -1,16 +1 @@
1
- export declare function defaultHistoryFilePath(): string;
2
- export declare function loadHistorySync(filePath?: string): string[];
3
- export declare function appendHistoryEntry(entry: string, filePath?: string): void;
4
- export interface HistoryNavState {
5
- history: string[];
6
- index: number | null;
7
- draft: string;
8
- }
9
- export interface HistoryNavResult {
10
- text: string;
11
- index: number | null;
12
- draft: string;
13
- changed: boolean;
14
- }
15
- export declare function stepHistory(state: HistoryNavState, direction: "up" | "down", currentText: string): HistoryNavResult;
16
- export declare function pushHistoryEntry(history: string[], entry: string): string[];
1
+ export * from "../tui/input-history.js";
@@ -1,79 +1 @@
1
- import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { getBubbleHome } from "../bubble-home.js";
4
- const MAX_HISTORY_ENTRIES = 1000;
5
- export function defaultHistoryFilePath() {
6
- return join(getBubbleHome(), "input-history.jsonl");
7
- }
8
- // JSONL on disk: each line is a JSON-encoded string. JSON encoding handles
9
- // embedded newlines and quotes so multi-line composer entries round-trip safely.
10
- export function loadHistorySync(filePath = defaultHistoryFilePath()) {
11
- try {
12
- if (!existsSync(filePath))
13
- return [];
14
- const raw = readFileSync(filePath, "utf8");
15
- const out = [];
16
- for (const line of raw.split("\n")) {
17
- if (!line)
18
- continue;
19
- try {
20
- const parsed = JSON.parse(line);
21
- if (typeof parsed === "string" && parsed.length > 0)
22
- out.push(parsed);
23
- }
24
- catch {
25
- // Malformed line - skip rather than fail the whole load.
26
- }
27
- }
28
- return out.length > MAX_HISTORY_ENTRIES ? out.slice(-MAX_HISTORY_ENTRIES) : out;
29
- }
30
- catch {
31
- return [];
32
- }
33
- }
34
- export function appendHistoryEntry(entry, filePath = defaultHistoryFilePath()) {
35
- if (!entry || entry.trim().length === 0)
36
- return;
37
- try {
38
- mkdirSync(dirname(filePath), { recursive: true });
39
- appendFileSync(filePath, JSON.stringify(entry) + "\n", "utf8");
40
- }
41
- catch {
42
- // Persistence is best-effort; never crash the composer over disk IO.
43
- }
44
- }
45
- // Pure transition for up/down navigation. `index === null` means the user is
46
- // editing a fresh draft; otherwise it points at history[index]. When stepping
47
- // from the draft into history we snapshot the current text so down past the
48
- // newest entry can restore it.
49
- export function stepHistory(state, direction, currentText) {
50
- const { history, index, draft } = state;
51
- const noChange = { text: currentText, index, draft, changed: false };
52
- if (direction === "up") {
53
- if (history.length === 0)
54
- return noChange;
55
- if (index === null) {
56
- const newIdx = history.length - 1;
57
- return { text: history[newIdx], index: newIdx, draft: currentText, changed: true };
58
- }
59
- if (index > 0) {
60
- return { text: history[index - 1], index: index - 1, draft, changed: true };
61
- }
62
- return noChange;
63
- }
64
- if (index === null)
65
- return noChange;
66
- if (index < history.length - 1) {
67
- return { text: history[index + 1], index: index + 1, draft, changed: true };
68
- }
69
- return { text: draft, index: null, draft: "", changed: true };
70
- }
71
- // Push to in-memory history with last-entry dedupe so repeated identical
72
- // submissions don't spam the stack.
73
- export function pushHistoryEntry(history, entry) {
74
- if (!entry || entry.trim().length === 0)
75
- return history;
76
- if (history.length > 0 && history[history.length - 1] === entry)
77
- return history;
78
- return [...history, entry];
79
- }
1
+ export * from "../tui/input-history.js";
@@ -0,0 +1,12 @@
1
+ import type { SubmitPayload } from "./input-box.js";
2
+ export interface QueuedInput {
3
+ payload: SubmitPayload;
4
+ displayKey?: string;
5
+ sessionFile?: string;
6
+ }
7
+ export interface PendingSteerMeta {
8
+ displayKey: string;
9
+ sessionFile?: string;
10
+ }
11
+ export declare function isQueuedInputForCurrentSession(input: QueuedInput, currentSessionFile?: string): boolean;
12
+ export declare function queuedAndPendingDisplayKeys(queuedInputs: QueuedInput[], pendingSteers: Iterable<PendingSteerMeta>): Set<string>;
@@ -0,0 +1,17 @@
1
+ export function isQueuedInputForCurrentSession(input, currentSessionFile) {
2
+ if (!input.sessionFile || !currentSessionFile)
3
+ return true;
4
+ return input.sessionFile === currentSessionFile;
5
+ }
6
+ export function queuedAndPendingDisplayKeys(queuedInputs, pendingSteers) {
7
+ const keys = new Set();
8
+ for (const input of queuedInputs) {
9
+ if (input.displayKey)
10
+ keys.add(input.displayKey);
11
+ }
12
+ for (const steer of pendingSteers) {
13
+ if (steer.displayKey)
14
+ keys.add(steer.displayKey);
15
+ }
16
+ return keys;
17
+ }
@@ -0,0 +1,9 @@
1
+ export interface InkKeyEvent {
2
+ eventType?: string;
3
+ }
4
+ /**
5
+ * Kitty keyboard protocol can report key press/repeat/release separately.
6
+ * Release events still carry the printable text, so handling them like normal
7
+ * input inserts every typed character twice.
8
+ */
9
+ export declare function isKeyReleaseEvent(key: InkKeyEvent): boolean;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Kitty keyboard protocol can report key press/repeat/release separately.
3
+ * Release events still carry the printable text, so handling them like normal
4
+ * input inserts every typed character twice.
5
+ */
6
+ export function isKeyReleaseEvent(key) {
7
+ return key.eventType === "release";
8
+ }
@@ -377,7 +377,7 @@ function TableBlock({ headers, rows, maxWidth, }) {
377
377
  const { columns: termWidth } = useTerminalSize();
378
378
  const colCount = headers.length;
379
379
  // Reserve a buffer so the table fits even when wrapped inside an indented
380
- // box (e.g. the timeline gutter contributes marginLeft + " " = 5 cells).
380
+ // box (e.g. the timeline gutter contributes marginLeft + " " = 5 cells).
381
381
  const budget = Math.max(20, (maxWidth ?? termWidth) - 8);
382
382
  const maxWidths = headers.map((h, i) => {
383
383
  let max = visualWidth(inlinePlainText(h));
@@ -17,6 +17,8 @@ interface MessageListProps {
17
17
  streamingTools: DisplayToolCall[];
18
18
  streamingParts: DisplayMessagePart[];
19
19
  terminalColumns: number;
20
+ showThinking?: boolean;
21
+ expandedToolOutput?: boolean;
20
22
  verboseTrace: boolean;
21
23
  pendingApproval?: PendingApprovalHint | null;
22
24
  /** Animation tick used to refresh in-progress elapsed counters. */
@@ -24,5 +26,5 @@ interface MessageListProps {
24
26
  /** Optional banner rendered as the first item in the app-controlled transcript. */
25
27
  welcomeBanner?: React.ReactNode;
26
28
  }
27
- export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
28
30
  export {};
@@ -9,42 +9,48 @@ import { buildTraceGroups, executeCommandBlock, formatTracePath, shouldInlineExe
9
9
  import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
10
10
  import { formatSubagentRoute } from "../agent/subagent-route-format.js";
11
11
  import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
12
+ import { splitImageDisplayContent } from "../tui/image-display.js";
12
13
  const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
13
- export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
14
+ export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, expandedToolOutput = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
15
+ const theme = useTheme();
14
16
  const hasStreaming = !!(streamingContent ||
15
17
  streamingReasoning ||
16
18
  streamingTools.length > 0 ||
17
19
  streamingParts.length > 0);
20
+ const regularMessages = messages.filter((message) => !message.inputStatus);
21
+ const pendingSteerMessages = messages.filter((message) => message.inputStatus === "pending_steer");
22
+ const queuedInputMessages = messages.filter((message) => message.inputStatus === "queued");
18
23
  const staticItems = [];
19
24
  if (welcomeBanner) {
20
25
  staticItems.push({ kind: "welcome", key: "welcome" });
21
26
  }
22
- const lastMessageIndex = messages.length - 1;
23
- for (let i = 0; i < messages.length; i++) {
24
- const msg = messages[i];
27
+ const lastMessageIndex = regularMessages.length - 1;
28
+ for (let i = 0; i < regularMessages.length; i++) {
29
+ const msg = regularMessages[i];
25
30
  staticItems.push({
26
31
  kind: "message",
27
32
  key: msg.key ?? `message-${i}`,
28
33
  message: msg,
29
34
  showExpandHint: !hasStreaming && i === lastMessageIndex,
35
+ separateFromPrevious: msg.role === "user" && regularMessages[i - 1]?.role === "user",
30
36
  });
31
37
  }
32
38
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [staticItems.map((item) => {
33
39
  if (item.kind === "welcome") {
34
40
  return _jsx(React.Fragment, { children: welcomeBanner }, item.key);
35
41
  }
36
- return (_jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, nowTick: item.showExpandHint ? nowTick : undefined }, item.key));
37
- }), hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick }))] }));
42
+ return (_jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious, nowTick: item.showExpandHint ? nowTick : undefined }, item.key));
43
+ }), hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }));
38
44
  }
39
45
  // Memoized: with no <Static> region, every transcript row re-renders on each
40
46
  // state change unless its props are referentially stable. Message objects are
41
47
  // append-only (compaction reuses already-compacted instances), keys are
42
48
  // stable, and nowTick is only threaded to the last row, so memo hits for all
43
49
  // settled history rows.
44
- const MessageItem = React.memo(function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, nowTick, }) {
50
+ const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, expandedToolOutput, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
45
51
  const theme = useTheme();
46
52
  if (message.role === "user") {
47
- return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus }));
53
+ return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus, separateFromPrevious: separateFromPrevious }));
48
54
  }
49
55
  if (message.role === "error") {
50
56
  return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { color: theme.error, children: ["Error: ", message.content] }) }));
@@ -59,12 +65,12 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
59
65
  const hasVisibleAssistantContent = !!message.content ||
60
66
  (message.toolCalls?.length ?? 0) > 0 ||
61
67
  (message.parts?.length ?? 0) > 0 ||
62
- (!!visibleReasoning && verboseTrace);
68
+ (!!visibleReasoning && (showThinking || verboseTrace));
63
69
  if (!hasVisibleAssistantContent)
64
70
  return null;
65
- return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
71
+ return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
66
72
  });
67
- function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
73
+ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, }) {
68
74
  const deferredContent = React.useDeferredValue(content);
69
75
  const deferredReasoning = React.useDeferredValue(reasoning);
70
76
  const deferredParts = React.useDeferredValue(parts);
@@ -72,22 +78,22 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
72
78
  const visibleParts = deferredParts.length > 0
73
79
  ? deferredParts
74
80
  : fallbackStreamingParts(deferredContent, tools);
75
- return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }) })), visibleParts.length > 0 && (
81
+ return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }) })), visibleParts.length > 0 && (
76
82
  // marginTop=1 matches the committed MessageItem layout exactly, so the
77
83
  // gap under the user message is identical while streaming and after the
78
84
  // turn commits — no spacing jump at finalize time. (The old marginTop=0
79
85
  // was a flicker mitigation for the main-screen <Static> renderer; the
80
86
  // alt-screen viewport repaints frames atomically, so it's obsolete.)
81
- _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
87
+ _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
82
88
  }
83
- function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
89
+ function MessageParts({ parts, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
84
90
  const lastToolsPartIndex = findLastToolsPartIndex(parts);
85
91
  const lastTextPartIndex = findLastTextPartIndex(parts);
86
92
  return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
87
93
  if (part.type === "text") {
88
94
  return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
89
95
  }
90
- return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
96
+ return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
91
97
  }) }));
92
98
  }
93
99
  function findLastTextPartIndex(parts) {
@@ -101,23 +107,24 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
101
107
  const theme = useTheme();
102
108
  if (!content.trim())
103
109
  return null;
104
- // marginLeft (2) + " " glyph (3 visual cells) = 5 cells consumed by the
110
+ // marginLeft (2) + " " marker (3 visual cells) = 5 cells consumed by the
105
111
  // timeline gutter; pass the remaining width so wide blocks like tables size
106
112
  // themselves against the actual content area instead of the raw terminal.
107
113
  const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
108
114
  const trimmed = content.trim();
109
- return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u26EC " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
115
+ 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 })) })] }));
110
116
  }
111
- function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
117
+ function ToolsPart({ toolCalls, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
112
118
  if (toolCalls.length === 0)
113
119
  return null;
114
- if (!verboseTrace) {
120
+ const expandTools = verboseTrace || expandedToolOutput;
121
+ if (!expandTools) {
115
122
  return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
116
123
  }
117
124
  const lastIdx = toolCalls.length - 1;
118
125
  return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
119
126
  const isWaitingApproval = isToolPending(tc) && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
120
- return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
127
+ return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: expandTools, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
121
128
  }) }));
122
129
  }
123
130
  function fallbackStreamingParts(content, tools) {
@@ -236,7 +243,7 @@ function CompactionSummaryBlock({ message }) {
236
243
  const summary = message.compactionSummary?.trim();
237
244
  return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
238
245
  }
239
- function UserMessageBlock({ content, terminalColumns, inputStatus, }) {
246
+ function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromPrevious = false, }) {
240
247
  const theme = useTheme();
241
248
  const badge = userInputStatusBadgeLabel(inputStatus);
242
249
  // Rail and its right gutter must share the bubble background; otherwise the
@@ -244,10 +251,21 @@ function UserMessageBlock({ content, terminalColumns, inputStatus, }) {
244
251
  const railWidth = 2;
245
252
  const horizontalRoom = Math.max(20, terminalColumns - 2);
246
253
  const bubbleTextWidth = Math.max(1, horizontalRoom - railWidth - 2);
247
- const wrappedLines = content
248
- .split("\n")
254
+ const { bodyLines, referenceLines } = splitImageDisplayContent(content);
255
+ const wrappedLines = bodyLines
249
256
  .flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
250
- return (_jsxs(Box, { flexDirection: "column", children: [badge && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: inputStatus === "pending_steer" ? theme.warning : theme.muted, children: ` ${badge} ` }), _jsx(Text, { color: theme.dim, children: inputStatus === "pending_steer" ? "applies at the next model call" : "runs after this turn" })] })), wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userRail, children: index === 0 ? "▌ " : " " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index)))] }));
257
+ return (_jsxs(Box, { flexDirection: "column", marginTop: separateFromPrevious ? 1 : 0, children: [badge && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: inputStatus === "pending_steer" ? theme.warning : theme.muted, children: ` ${badge} ` }), _jsx(Text, { color: theme.dim, children: inputStatus === "pending_steer" ? "applies at the next model call" : "runs after this turn" })] })), wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userRail, children: index === 0 ? "▌ " : " " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index))), referenceLines.map((line, index) => (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: ` ${line}` }) }, `attachment-${index}`)))] }));
258
+ }
259
+ function PendingInputMessagesBlock({ messages, terminalColumns, title, hint, bulletColor, }) {
260
+ const theme = useTheme();
261
+ const contentWidth = Math.max(20, terminalColumns - 5);
262
+ 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) => {
263
+ const { bodyLines, referenceLines } = splitImageDisplayContent(message.content || " ");
264
+ const wrappedBody = bodyLines.flatMap((line) => wrapByVisualWidth(line || " ", contentWidth));
265
+ const bodyRows = wrappedBody.map((line, lineIndex) => (_jsxs(Box, { marginLeft: 2, children: [_jsx(Text, { color: theme.dim, children: lineIndex === 0 ? "↳ " : " " }), _jsx(Text, { color: theme.inputText, children: line })] }, `body-${message.key ?? messageIndex}-${lineIndex}`)));
266
+ const attachmentRows = referenceLines.map((line, lineIndex) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dim, children: [" ", line] }) }, `attachment-${message.key ?? messageIndex}-${lineIndex}`)));
267
+ return [...bodyRows, ...attachmentRows];
268
+ })] }));
251
269
  }
252
270
  const TOOL_DISPLAY_NAMES = {
253
271
  read: "Read",
@@ -1,10 +1,12 @@
1
1
  import { ProviderRegistry } from "../provider-registry.js";
2
+ import type { ThinkingLevel } from "../types.js";
2
3
  export { padVisual, truncateVisual } from "../text-display.js";
3
4
  export interface ModelPickerOption {
4
5
  id: string;
5
6
  label: string;
6
7
  group: string;
7
8
  providerBadge: string;
9
+ reasoningLevels: ThinkingLevel[];
8
10
  }
9
11
  export type PickerKeyAction = "up" | "down" | "enter" | "escape" | "backspace" | "delete";
10
12
  export declare function resolvePickerKeyAction(input: string, key: {
@@ -23,14 +25,34 @@ export declare function formatSkillPickerRow(skill: {
23
25
  selected: boolean;
24
26
  width: number;
25
27
  }): string;
28
+ export declare const MODEL_PICKER_MAX_BODY_ROWS = 10;
29
+ export declare const MODEL_PICKER_CHROME_ROWS = 13;
30
+ export declare function modelPickerBodyRows(termHeight: number): number;
31
+ export declare function clampPickerIndex(index: number, length: number): number;
32
+ export declare function pickerWindowStart(selectedIndex: number, length: number, visibleRows: number): number;
33
+ export declare function padPickerRows(rows: string[], bodyRows: number, width: number): string[];
34
+ export declare function formatReasoningLevelsLabel(levels: readonly ThinkingLevel[]): string;
35
+ export declare function formatModelPickerRow(option: Pick<ModelPickerOption, "label" | "providerBadge" | "reasoningLevels">, options: {
36
+ selected: boolean;
37
+ current: boolean;
38
+ width: number;
39
+ }): string;
40
+ export declare function formatEffortPickerRow(level: ThinkingLevel, options: {
41
+ selected: boolean;
42
+ width: number;
43
+ }): string;
44
+ export declare function formatNoModelResultsRow(query: string, width: number): string;
45
+ export declare function preferredEffortIndex(option: Pick<ModelPickerOption, "reasoningLevels">, currentThinkingLevel: ThinkingLevel): number;
46
+ export declare function shouldOpenEffortPicker(option: Pick<ModelPickerOption, "reasoningLevels">): boolean;
26
47
  export interface ModelPickerProps {
27
48
  registry: ProviderRegistry;
28
49
  current: string;
50
+ currentThinkingLevel: ThinkingLevel;
29
51
  recent: string[];
30
- onSelect: (model: string) => void;
52
+ onSelect: (model: string, thinkingLevel: ThinkingLevel) => void;
31
53
  onCancel: () => void;
32
54
  }
33
- export declare function ModelPicker({ registry, current, recent, onSelect, onCancel }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
55
+ export declare function ModelPicker({ registry, current, currentThinkingLevel, recent, onSelect, onCancel }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
34
56
  export declare function buildLocalModelOptions(registry: ProviderRegistry, current: string, recent: string[]): ModelPickerOption[];
35
57
  export interface ProviderPickerProps {
36
58
  providers: Array<{