@bubblebrain-ai/bubble 0.0.24 → 0.0.26

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 (171) hide show
  1. package/README.md +5 -3
  2. package/dist/agent.js +1 -1
  3. package/dist/clipboard.d.ts +14 -0
  4. package/dist/clipboard.js +132 -0
  5. package/dist/config.d.ts +3 -0
  6. package/dist/config.js +22 -6
  7. package/dist/goal/format.js +34 -4
  8. package/dist/goal/store.d.ts +3 -0
  9. package/dist/goal/store.js +14 -1
  10. package/dist/goal/usage.d.ts +2 -0
  11. package/dist/goal/usage.js +3 -0
  12. package/dist/main.js +23 -42
  13. package/dist/model-catalog.d.ts +3 -1
  14. package/dist/model-catalog.js +17 -28
  15. package/dist/prompt/compose.js +1 -1
  16. package/dist/provider-anthropic.d.ts +4 -0
  17. package/dist/provider-anthropic.js +31 -0
  18. package/dist/provider-ark-responses.d.ts +17 -0
  19. package/dist/provider-ark-responses.js +462 -0
  20. package/dist/provider-transform.js +7 -0
  21. package/dist/provider.d.ts +7 -0
  22. package/dist/provider.js +170 -27
  23. package/dist/slash-commands/commands.js +22 -0
  24. package/dist/tools/todo.js +22 -38
  25. package/dist/tui/detect-theme.d.ts +1 -0
  26. package/dist/tui/detect-theme.js +23 -0
  27. package/dist/tui/image-display.d.ts +13 -0
  28. package/dist/tui/image-display.js +49 -0
  29. package/dist/tui/input-history.d.ts +37 -6
  30. package/dist/tui/input-history.js +194 -23
  31. package/dist/tui/model-switch.d.ts +42 -0
  32. package/dist/tui/model-switch.js +55 -0
  33. package/dist/tui-ink/app.d.ts +32 -2
  34. package/dist/tui-ink/app.js +1409 -549
  35. package/dist/tui-ink/approval/select.js +10 -0
  36. package/dist/tui-ink/detect-theme.d.ts +1 -2
  37. package/dist/tui-ink/detect-theme.js +1 -87
  38. package/dist/tui-ink/display-history.d.ts +1 -0
  39. package/dist/tui-ink/display-history.js +11 -0
  40. package/dist/tui-ink/feedback-dialog.js +10 -0
  41. package/dist/tui-ink/feishu-setup-picker.js +10 -0
  42. package/dist/tui-ink/footer.d.ts +1 -0
  43. package/dist/tui-ink/footer.js +8 -2
  44. package/dist/tui-ink/input-box.d.ts +71 -9
  45. package/dist/tui-ink/input-box.js +359 -121
  46. package/dist/tui-ink/input-history.d.ts +1 -16
  47. package/dist/tui-ink/input-history.js +1 -79
  48. package/dist/tui-ink/input-queue.d.ts +12 -0
  49. package/dist/tui-ink/input-queue.js +17 -0
  50. package/dist/tui-ink/key-events.d.ts +9 -0
  51. package/dist/tui-ink/key-events.js +8 -0
  52. package/dist/tui-ink/markdown.js +1 -1
  53. package/dist/tui-ink/message-list.d.ts +19 -1
  54. package/dist/tui-ink/message-list.js +111 -32
  55. package/dist/tui-ink/model-picker.d.ts +25 -2
  56. package/dist/tui-ink/model-picker.js +237 -20
  57. package/dist/tui-ink/plan-confirm.js +10 -0
  58. package/dist/tui-ink/question-dialog.js +46 -10
  59. package/dist/tui-ink/run.d.ts +10 -1
  60. package/dist/tui-ink/run.js +27 -42
  61. package/dist/tui-ink/session-picker.js +3 -0
  62. package/dist/tui-ink/submit-dedupe.d.ts +5 -0
  63. package/dist/tui-ink/submit-dedupe.js +25 -0
  64. package/dist/tui-ink/terminal-mouse.d.ts +24 -1
  65. package/dist/tui-ink/terminal-mouse.js +76 -21
  66. package/dist/tui-ink/theme.d.ts +6 -3
  67. package/dist/tui-ink/theme.js +10 -4
  68. package/dist/tui-ink/welcome.d.ts +1 -0
  69. package/dist/tui-ink/welcome.js +34 -27
  70. package/dist/variant/variant-resolver.js +4 -1
  71. package/package.json +1 -5
  72. package/dist/tui/clipboard.d.ts +0 -1
  73. package/dist/tui/clipboard.js +0 -53
  74. package/dist/tui/escape-confirmation.d.ts +0 -15
  75. package/dist/tui/escape-confirmation.js +0 -30
  76. package/dist/tui/global-key-router.d.ts +0 -3
  77. package/dist/tui/global-key-router.js +0 -87
  78. package/dist/tui/markdown-inline.d.ts +0 -22
  79. package/dist/tui/markdown-inline.js +0 -68
  80. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  81. package/dist/tui/markdown-theme-rules.js +0 -164
  82. package/dist/tui/markdown-theme.d.ts +0 -5
  83. package/dist/tui/markdown-theme.js +0 -27
  84. package/dist/tui/opencode-spinner.d.ts +0 -22
  85. package/dist/tui/opencode-spinner.js +0 -216
  86. package/dist/tui/prompt-keybindings.d.ts +0 -42
  87. package/dist/tui/prompt-keybindings.js +0 -35
  88. package/dist/tui/render-signature.d.ts +0 -1
  89. package/dist/tui/render-signature.js +0 -7
  90. package/dist/tui/run.d.ts +0 -67
  91. package/dist/tui/run.js +0 -10166
  92. package/dist/tui/sidebar-mcp.d.ts +0 -31
  93. package/dist/tui/sidebar-mcp.js +0 -62
  94. package/dist/tui/sidebar-state.d.ts +0 -12
  95. package/dist/tui/sidebar-state.js +0 -69
  96. package/dist/tui/streaming-tool-args.d.ts +0 -15
  97. package/dist/tui/streaming-tool-args.js +0 -30
  98. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  99. package/dist/tui/tool-renderers/fallback.js +0 -75
  100. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  101. package/dist/tui/tool-renderers/registry.js +0 -11
  102. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  103. package/dist/tui/tool-renderers/subagent.js +0 -135
  104. package/dist/tui/tool-renderers/types.d.ts +0 -36
  105. package/dist/tui/tool-renderers/types.js +0 -1
  106. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  107. package/dist/tui/tool-renderers/write-preview.js +0 -32
  108. package/dist/tui/tool-renderers/write.d.ts +0 -6
  109. package/dist/tui/tool-renderers/write.js +0 -88
  110. package/dist/tui/transcript-scroll.d.ts +0 -25
  111. package/dist/tui/transcript-scroll.js +0 -20
  112. package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
  113. package/dist/tui-ink/transcript-viewport-math.js +0 -17
  114. package/dist/tui-ink/transcript-viewport.d.ts +0 -24
  115. package/dist/tui-ink/transcript-viewport.js +0 -83
  116. package/dist/tui-opentui/app.d.ts +0 -54
  117. package/dist/tui-opentui/app.js +0 -1371
  118. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  119. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  120. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  121. package/dist/tui-opentui/approval/diff-view.js +0 -43
  122. package/dist/tui-opentui/approval/select.d.ts +0 -37
  123. package/dist/tui-opentui/approval/select.js +0 -91
  124. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  125. package/dist/tui-opentui/detect-theme.js +0 -87
  126. package/dist/tui-opentui/display-history.d.ts +0 -56
  127. package/dist/tui-opentui/display-history.js +0 -130
  128. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  129. package/dist/tui-opentui/edit-diff.js +0 -57
  130. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  131. package/dist/tui-opentui/feedback-dialog.js +0 -164
  132. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  133. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  134. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  135. package/dist/tui-opentui/file-mentions.js +0 -174
  136. package/dist/tui-opentui/footer.d.ts +0 -26
  137. package/dist/tui-opentui/footer.js +0 -40
  138. package/dist/tui-opentui/image-paste.d.ts +0 -54
  139. package/dist/tui-opentui/image-paste.js +0 -288
  140. package/dist/tui-opentui/input-box.d.ts +0 -32
  141. package/dist/tui-opentui/input-box.js +0 -462
  142. package/dist/tui-opentui/input-history.d.ts +0 -16
  143. package/dist/tui-opentui/input-history.js +0 -79
  144. package/dist/tui-opentui/markdown.d.ts +0 -66
  145. package/dist/tui-opentui/markdown.js +0 -127
  146. package/dist/tui-opentui/message-list.d.ts +0 -31
  147. package/dist/tui-opentui/message-list.js +0 -131
  148. package/dist/tui-opentui/model-picker.d.ts +0 -63
  149. package/dist/tui-opentui/model-picker.js +0 -450
  150. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  151. package/dist/tui-opentui/plan-confirm.js +0 -124
  152. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  153. package/dist/tui-opentui/question-dialog.js +0 -110
  154. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  155. package/dist/tui-opentui/recent-activity.js +0 -71
  156. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  157. package/dist/tui-opentui/run-session-picker.js +0 -28
  158. package/dist/tui-opentui/run.d.ts +0 -38
  159. package/dist/tui-opentui/run.js +0 -48
  160. package/dist/tui-opentui/session-picker.d.ts +0 -12
  161. package/dist/tui-opentui/session-picker.js +0 -120
  162. package/dist/tui-opentui/theme.d.ts +0 -89
  163. package/dist/tui-opentui/theme.js +0 -157
  164. package/dist/tui-opentui/todos.d.ts +0 -9
  165. package/dist/tui-opentui/todos.js +0 -45
  166. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  167. package/dist/tui-opentui/trace-groups.js +0 -455
  168. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  169. package/dist/tui-opentui/use-terminal-size.js +0 -5
  170. package/dist/tui-opentui/welcome.d.ts +0 -25
  171. 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,12 +17,30 @@ 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. */
23
25
  nowTick?: number;
24
26
  /** Optional banner rendered as the first item in the app-controlled transcript. */
25
27
  welcomeBanner?: React.ReactNode;
28
+ /**
29
+ * Bumped whenever the settled transcript is rebuilt non-monotonically
30
+ * (/clear, /compact, /rewind, session switch). Used as the <Static> key so
31
+ * Ink discards its already-printed rows and re-prints the rebuilt list onto
32
+ * a freshly-cleared screen instead of appending duplicates.
33
+ */
34
+ staticGeneration?: number;
35
+ /** Horizontal padding applied inside each committed/streaming row. */
36
+ paddingX?: number;
37
+ /**
38
+ * Maximum height (rows) for the live/dynamic region. The in-progress turn is
39
+ * clipped to this and pinned to the bottom (tail view) so the live frame
40
+ * never exceeds the viewport — a taller-than-pane frame breaks Ink's redraw
41
+ * under tmux. The full turn still lands in <Static> scrollback on commit.
42
+ */
43
+ maxStreamRows?: number;
26
44
  }
27
- export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
45
+ export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration, paddingX, maxStreamRows, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
28
46
  export {};
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
- import { Box, Text } from "ink";
3
+ import { Box, Static, Text, measureElement } from "ink";
4
4
  import { useTheme } from "./theme.js";
5
5
  import { highlightCode, inferLang } from "./code-highlight.js";
6
6
  import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
@@ -9,42 +9,99 @@ 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, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
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
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [staticItems.map((item) => {
33
- if (item.kind === "welcome") {
34
- return _jsx(React.Fragment, { children: welcomeBanner }, item.key);
35
- }
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 }))] }));
38
+ const hasDynamic = hasStreaming || pendingSteerMessages.length > 0 || queuedInputMessages.length > 0;
39
+ // The live region must never grow taller than the viewport: a frame taller
40
+ // than the pane breaks Ink's in-place redraw under tmux (the cursor-up clear
41
+ // can't reach scrolled-off rows), leaving large blank gaps + stray glyphs.
42
+ // Clip it to maxStreamRows and pin to the bottom so the user sees the latest
43
+ // output (tail view); the full turn lands in <Static> scrollback on commit.
44
+ const clampDynamic = typeof maxStreamRows === "number" && maxStreamRows > 0;
45
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Static, { items: staticItems, children: (item) => {
46
+ if (item.kind === "welcome") {
47
+ return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
48
+ }
49
+ return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
50
+ } }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [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 }))] }))] }));
51
+ }
52
+ /**
53
+ * Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
54
+ * to the bottom so the user always sees the latest output (tail view). A live
55
+ * frame taller than the terminal pane breaks Ink's in-place redraw under tmux
56
+ * (the cursor-up clear can't reach rows that scrolled off), leaving large blank
57
+ * gaps and stray glyphs. We measure the natural content height and only clip
58
+ * when it actually exceeds `maxRows`, so short turns keep their natural height
59
+ * (no reserved-space gap). The full turn still lands in <Static> scrollback the
60
+ * moment it commits, so nothing is lost — only the live preview is windowed.
61
+ */
62
+ function DynamicClamp({ maxRows, paddingX, children, }) {
63
+ const innerRef = React.useRef(null);
64
+ const [offset, setOffset] = React.useState(0);
65
+ const [clipHeight, setClipHeight] = React.useState(undefined);
66
+ // Re-measure after every commit (streaming changes height with each token).
67
+ // useLayoutEffect runs before Ink flushes the frame to the terminal, so the
68
+ // clamp is applied in the same paint — avoiding a one-frame overflow flash
69
+ // under tmux when a turn first grows past the pane. setState bails out via
70
+ // Object.is when nothing moved, so the steady state does not loop.
71
+ React.useLayoutEffect(() => {
72
+ if (!maxRows || !innerRef.current) {
73
+ if (offset !== 0)
74
+ setOffset(0);
75
+ if (clipHeight !== undefined)
76
+ setClipHeight(undefined);
77
+ return;
78
+ }
79
+ const height = measureElement(innerRef.current).height;
80
+ if (height > maxRows) {
81
+ const nextOffset = -(height - maxRows);
82
+ if (nextOffset !== offset)
83
+ setOffset(nextOffset);
84
+ if (clipHeight !== maxRows)
85
+ setClipHeight(maxRows);
86
+ }
87
+ else {
88
+ if (offset !== 0)
89
+ setOffset(0);
90
+ if (clipHeight !== undefined)
91
+ setClipHeight(undefined);
92
+ }
93
+ });
94
+ return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: paddingX, ...(clipHeight !== undefined ? { height: clipHeight, overflowY: "hidden" } : {}), children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: offset, children: children }) }));
38
95
  }
39
96
  // Memoized: with no <Static> region, every transcript row re-renders on each
40
97
  // state change unless its props are referentially stable. Message objects are
41
98
  // append-only (compaction reuses already-compacted instances), keys are
42
99
  // stable, and nowTick is only threaded to the last row, so memo hits for all
43
100
  // settled history rows.
44
- const MessageItem = React.memo(function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, nowTick, }) {
101
+ const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, expandedToolOutput, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
45
102
  const theme = useTheme();
46
103
  if (message.role === "user") {
47
- return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus }));
104
+ return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus, separateFromPrevious: separateFromPrevious }));
48
105
  }
49
106
  if (message.role === "error") {
50
107
  return (_jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsxs(Text, { color: theme.error, children: ["Error: ", message.content] }) }));
@@ -56,15 +113,18 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
56
113
  return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.error, children: "\u23F9 " }), _jsx(Text, { color: theme.muted, dimColor: true, children: message.content || "Interrupted by user" })] }));
57
114
  }
58
115
  const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
59
- const hasVisibleAssistantContent = !!message.content ||
116
+ // Same defense as reasoning: strip any internal reminder markup the model
117
+ // echoed back into its visible answer so it never reaches the transcript.
118
+ const visibleContent = sanitizeInternalReminderBlocks(message.content ?? "");
119
+ const hasVisibleAssistantContent = !!visibleContent.trim() ||
60
120
  (message.toolCalls?.length ?? 0) > 0 ||
61
121
  (message.parts?.length ?? 0) > 0 ||
62
- (!!visibleReasoning && verboseTrace);
122
+ (!!visibleReasoning && (showThinking || verboseTrace));
63
123
  if (!hasVisibleAssistantContent)
64
124
  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 }))] }));
125
+ 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 })), visibleContent.trim() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
66
126
  });
67
- function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
127
+ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, }) {
68
128
  const deferredContent = React.useDeferredValue(content);
69
129
  const deferredReasoning = React.useDeferredValue(reasoning);
70
130
  const deferredParts = React.useDeferredValue(parts);
@@ -72,22 +132,22 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, v
72
132
  const visibleParts = deferredParts.length > 0
73
133
  ? deferredParts
74
134
  : 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 && (
135
+ return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }) })), visibleParts.length > 0 && (
76
136
  // marginTop=1 matches the committed MessageItem layout exactly, so the
77
137
  // gap under the user message is identical while streaming and after the
78
138
  // turn commits — no spacing jump at finalize time. (The old marginTop=0
79
139
  // was a flicker mitigation for the main-screen <Static> renderer; the
80
140
  // 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 }) }))] }));
141
+ _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
142
  }
83
- function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
143
+ function MessageParts({ parts, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
84
144
  const lastToolsPartIndex = findLastToolsPartIndex(parts);
85
145
  const lastTextPartIndex = findLastTextPartIndex(parts);
86
146
  return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
87
147
  if (part.type === "text") {
88
148
  return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
89
149
  }
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}`));
150
+ 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
151
  }) }));
92
152
  }
93
153
  function findLastTextPartIndex(parts) {
@@ -99,25 +159,32 @@ function findLastTextPartIndex(parts) {
99
159
  }
100
160
  function TimelineText({ content, compactTop, terminalColumns, streaming = false, }) {
101
161
  const theme = useTheme();
102
- if (!content.trim())
162
+ // Strip any internal reminder/context markup the model echoed back into its
163
+ // visible text — the reasoning path already does this. Without it, a model
164
+ // that parrots a <bubble_internal_*> block (e.g. an injected system reminder)
165
+ // leaks it straight into the transcript. The streaming sanitizer also holds a
166
+ // half-typed block until it closes, so partial markup never flashes.
167
+ const visible = sanitizeInternalReminderBlocks(content);
168
+ if (!visible.trim())
103
169
  return null;
104
- // marginLeft (2) + " " glyph (3 visual cells) = 5 cells consumed by the
170
+ // marginLeft (2) + " " marker (3 visual cells) = 5 cells consumed by the
105
171
  // timeline gutter; pass the remaining width so wide blocks like tables size
106
172
  // themselves against the actual content area instead of the raw terminal.
107
173
  const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
108
- 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 })) })] }));
174
+ const trimmed = visible.trim();
175
+ 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
176
  }
111
- function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
177
+ function ToolsPart({ toolCalls, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
112
178
  if (toolCalls.length === 0)
113
179
  return null;
114
- if (!verboseTrace) {
180
+ const expandTools = verboseTrace || expandedToolOutput;
181
+ if (!expandTools) {
115
182
  return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
116
183
  }
117
184
  const lastIdx = toolCalls.length - 1;
118
185
  return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
119
186
  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));
187
+ 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
188
  }) }));
122
189
  }
123
190
  function fallbackStreamingParts(content, tools) {
@@ -236,7 +303,7 @@ function CompactionSummaryBlock({ message }) {
236
303
  const summary = message.compactionSummary?.trim();
237
304
  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
305
  }
239
- function UserMessageBlock({ content, terminalColumns, inputStatus, }) {
306
+ function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromPrevious = false, }) {
240
307
  const theme = useTheme();
241
308
  const badge = userInputStatusBadgeLabel(inputStatus);
242
309
  // Rail and its right gutter must share the bubble background; otherwise the
@@ -244,10 +311,22 @@ function UserMessageBlock({ content, terminalColumns, inputStatus, }) {
244
311
  const railWidth = 2;
245
312
  const horizontalRoom = Math.max(20, terminalColumns - 2);
246
313
  const bubbleTextWidth = Math.max(1, horizontalRoom - railWidth - 2);
247
- const wrappedLines = content
248
- .split("\n")
314
+ const { bodyLines, referenceLines } = splitImageDisplayContent(content);
315
+ const wrappedLines = bodyLines
249
316
  .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)))] }));
317
+ const attachmentReferenceIndent = " ".repeat(railWidth + 1);
318
+ 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: "▌ " }), _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: `${attachmentReferenceIndent}${line}` }) }, `attachment-${index}`)))] }));
319
+ }
320
+ function PendingInputMessagesBlock({ messages, terminalColumns, title, hint, bulletColor, }) {
321
+ const theme = useTheme();
322
+ const contentWidth = Math.max(20, terminalColumns - 5);
323
+ 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) => {
324
+ const { bodyLines, referenceLines } = splitImageDisplayContent(message.content || " ");
325
+ const wrappedBody = bodyLines.flatMap((line) => wrapByVisualWidth(line || " ", contentWidth));
326
+ 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}`)));
327
+ const attachmentRows = referenceLines.map((line, lineIndex) => (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.dim, children: [" ", line] }) }, `attachment-${message.key ?? messageIndex}-${lineIndex}`)));
328
+ return [...bodyRows, ...attachmentRows];
329
+ })] }));
251
330
  }
252
331
  const TOOL_DISPLAY_NAMES = {
253
332
  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,35 @@ 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[], asToggle?: boolean): string;
35
+ export declare function formatModelPickerRow(option: Pick<ModelPickerOption, "id" | "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
+ asToggle?: boolean;
44
+ }): string;
45
+ export declare function formatNoModelResultsRow(query: string, width: number): string;
46
+ export declare function preferredEffortIndex(option: Pick<ModelPickerOption, "reasoningLevels">, currentThinkingLevel: ThinkingLevel): number;
47
+ export declare function shouldOpenEffortPicker(option: Pick<ModelPickerOption, "reasoningLevels">): boolean;
26
48
  export interface ModelPickerProps {
27
49
  registry: ProviderRegistry;
28
50
  current: string;
51
+ currentThinkingLevel: ThinkingLevel;
29
52
  recent: string[];
30
- onSelect: (model: string) => void;
53
+ onSelect: (model: string, thinkingLevel: ThinkingLevel) => void;
31
54
  onCancel: () => void;
32
55
  }
33
- export declare function ModelPicker({ registry, current, recent, onSelect, onCancel }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
56
+ export declare function ModelPicker({ registry, current, currentThinkingLevel, recent, onSelect, onCancel }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
34
57
  export declare function buildLocalModelOptions(registry: ProviderRegistry, current: string, recent: string[]): ModelPickerOption[];
35
58
  export interface ProviderPickerProps {
36
59
  providers: Array<{