@bubblebrain-ai/bubble 0.0.23 → 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 (168) 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/command.d.ts +20 -0
  5. package/dist/goal/command.js +71 -0
  6. package/dist/goal/engine.d.ts +33 -0
  7. package/dist/goal/engine.js +65 -0
  8. package/dist/goal/format.d.ts +18 -0
  9. package/dist/goal/format.js +112 -0
  10. package/dist/goal/prompts.d.ts +13 -0
  11. package/dist/goal/prompts.js +84 -0
  12. package/dist/goal/store.d.ts +64 -0
  13. package/dist/goal/store.js +174 -0
  14. package/dist/goal/tools.d.ts +10 -0
  15. package/dist/goal/tools.js +70 -0
  16. package/dist/goal/usage.d.ts +2 -0
  17. package/dist/goal/usage.js +3 -0
  18. package/dist/main.js +29 -42
  19. package/dist/model-catalog.js +11 -0
  20. package/dist/provider-transform.js +17 -0
  21. package/dist/provider.js +20 -5
  22. package/dist/session-types.d.ts +3 -0
  23. package/dist/tools/index.d.ts +3 -0
  24. package/dist/tools/index.js +2 -0
  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 +1360 -522
  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 +70 -9
  45. package/dist/tui-ink/input-box.js +354 -120
  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 +3 -1
  54. package/dist/tui-ink/message-list.js +42 -24
  55. package/dist/tui-ink/model-picker.d.ts +24 -2
  56. package/dist/tui-ink/model-picker.js +224 -20
  57. package/dist/tui-ink/plan-confirm.js +10 -0
  58. package/dist/tui-ink/question-dialog.js +10 -0
  59. package/dist/tui-ink/run.d.ts +11 -0
  60. package/dist/tui-ink/run.js +21 -28
  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 +13 -1
  65. package/dist/tui-ink/terminal-mouse.js +63 -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/transcript-input.d.ts +8 -0
  69. package/dist/tui-ink/transcript-input.js +9 -0
  70. package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
  71. package/dist/tui-ink/transcript-viewport-math.js +1 -2
  72. package/dist/tui-ink/welcome.d.ts +1 -0
  73. package/dist/tui-ink/welcome.js +25 -28
  74. package/package.json +1 -5
  75. package/dist/tui/clipboard.d.ts +0 -1
  76. package/dist/tui/clipboard.js +0 -53
  77. package/dist/tui/escape-confirmation.d.ts +0 -15
  78. package/dist/tui/escape-confirmation.js +0 -30
  79. package/dist/tui/global-key-router.d.ts +0 -3
  80. package/dist/tui/global-key-router.js +0 -87
  81. package/dist/tui/markdown-inline.d.ts +0 -22
  82. package/dist/tui/markdown-inline.js +0 -68
  83. package/dist/tui/markdown-theme-rules.d.ts +0 -23
  84. package/dist/tui/markdown-theme-rules.js +0 -164
  85. package/dist/tui/markdown-theme.d.ts +0 -5
  86. package/dist/tui/markdown-theme.js +0 -27
  87. package/dist/tui/opencode-spinner.d.ts +0 -22
  88. package/dist/tui/opencode-spinner.js +0 -216
  89. package/dist/tui/prompt-keybindings.d.ts +0 -42
  90. package/dist/tui/prompt-keybindings.js +0 -35
  91. package/dist/tui/render-signature.d.ts +0 -1
  92. package/dist/tui/render-signature.js +0 -7
  93. package/dist/tui/run.d.ts +0 -65
  94. package/dist/tui/run.js +0 -9934
  95. package/dist/tui/sidebar-mcp.d.ts +0 -31
  96. package/dist/tui/sidebar-mcp.js +0 -62
  97. package/dist/tui/sidebar-state.d.ts +0 -12
  98. package/dist/tui/sidebar-state.js +0 -69
  99. package/dist/tui/streaming-tool-args.d.ts +0 -15
  100. package/dist/tui/streaming-tool-args.js +0 -30
  101. package/dist/tui/tool-renderers/fallback.d.ts +0 -2
  102. package/dist/tui/tool-renderers/fallback.js +0 -75
  103. package/dist/tui/tool-renderers/registry.d.ts +0 -3
  104. package/dist/tui/tool-renderers/registry.js +0 -11
  105. package/dist/tui/tool-renderers/subagent.d.ts +0 -2
  106. package/dist/tui/tool-renderers/subagent.js +0 -135
  107. package/dist/tui/tool-renderers/types.d.ts +0 -36
  108. package/dist/tui/tool-renderers/types.js +0 -1
  109. package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
  110. package/dist/tui/tool-renderers/write-preview.js +0 -32
  111. package/dist/tui/tool-renderers/write.d.ts +0 -6
  112. package/dist/tui/tool-renderers/write.js +0 -88
  113. package/dist/tui-opentui/app.d.ts +0 -54
  114. package/dist/tui-opentui/app.js +0 -1371
  115. package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
  116. package/dist/tui-opentui/approval/approval-dialog.js +0 -155
  117. package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
  118. package/dist/tui-opentui/approval/diff-view.js +0 -43
  119. package/dist/tui-opentui/approval/select.d.ts +0 -37
  120. package/dist/tui-opentui/approval/select.js +0 -91
  121. package/dist/tui-opentui/detect-theme.d.ts +0 -2
  122. package/dist/tui-opentui/detect-theme.js +0 -87
  123. package/dist/tui-opentui/display-history.d.ts +0 -56
  124. package/dist/tui-opentui/display-history.js +0 -130
  125. package/dist/tui-opentui/edit-diff.d.ts +0 -11
  126. package/dist/tui-opentui/edit-diff.js +0 -57
  127. package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
  128. package/dist/tui-opentui/feedback-dialog.js +0 -164
  129. package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
  130. package/dist/tui-opentui/feishu-setup-picker.js +0 -272
  131. package/dist/tui-opentui/file-mentions.d.ts +0 -29
  132. package/dist/tui-opentui/file-mentions.js +0 -174
  133. package/dist/tui-opentui/footer.d.ts +0 -26
  134. package/dist/tui-opentui/footer.js +0 -40
  135. package/dist/tui-opentui/image-paste.d.ts +0 -54
  136. package/dist/tui-opentui/image-paste.js +0 -288
  137. package/dist/tui-opentui/input-box.d.ts +0 -32
  138. package/dist/tui-opentui/input-box.js +0 -462
  139. package/dist/tui-opentui/input-history.d.ts +0 -16
  140. package/dist/tui-opentui/input-history.js +0 -79
  141. package/dist/tui-opentui/markdown.d.ts +0 -66
  142. package/dist/tui-opentui/markdown.js +0 -127
  143. package/dist/tui-opentui/message-list.d.ts +0 -31
  144. package/dist/tui-opentui/message-list.js +0 -131
  145. package/dist/tui-opentui/model-picker.d.ts +0 -63
  146. package/dist/tui-opentui/model-picker.js +0 -450
  147. package/dist/tui-opentui/plan-confirm.d.ts +0 -9
  148. package/dist/tui-opentui/plan-confirm.js +0 -124
  149. package/dist/tui-opentui/question-dialog.d.ts +0 -10
  150. package/dist/tui-opentui/question-dialog.js +0 -110
  151. package/dist/tui-opentui/recent-activity.d.ts +0 -8
  152. package/dist/tui-opentui/recent-activity.js +0 -71
  153. package/dist/tui-opentui/run-session-picker.d.ts +0 -10
  154. package/dist/tui-opentui/run-session-picker.js +0 -28
  155. package/dist/tui-opentui/run.d.ts +0 -38
  156. package/dist/tui-opentui/run.js +0 -48
  157. package/dist/tui-opentui/session-picker.d.ts +0 -12
  158. package/dist/tui-opentui/session-picker.js +0 -120
  159. package/dist/tui-opentui/theme.d.ts +0 -89
  160. package/dist/tui-opentui/theme.js +0 -157
  161. package/dist/tui-opentui/todos.d.ts +0 -9
  162. package/dist/tui-opentui/todos.js +0 -45
  163. package/dist/tui-opentui/trace-groups.d.ts +0 -27
  164. package/dist/tui-opentui/trace-groups.js +0 -455
  165. package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
  166. package/dist/tui-opentui/use-terminal-size.js +0 -5
  167. package/dist/tui-opentui/welcome.d.ts +0 -25
  168. package/dist/tui-opentui/welcome.js +0 -77
@@ -1,10 +1,13 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useMemo } from "react";
3
3
  import { Box, Text, useInput, usePaste, useStdout } from "ink";
4
+ import { isKeyReleaseEvent } from "./key-events.js";
4
5
  import { useTheme } from "./theme.js";
5
6
  import { encodeModel, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
6
7
  import { listBuiltinModels } from "../model-catalog.js";
7
8
  import { padVisual, truncateVisual } from "../text-display.js";
9
+ import { hasTerminalMouseSequence } from "./terminal-mouse.js";
10
+ import { getAvailableThinkingLevels, normalizeThinkingLevel } from "../provider-transform.js";
8
11
  export { padVisual, truncateVisual } from "../text-display.js";
9
12
  export function resolvePickerKeyAction(input, key) {
10
13
  if (key.escape)
@@ -29,6 +32,8 @@ export function resolvePickerKeyAction(input, key) {
29
32
  export function isPrintablePickerInput(input) {
30
33
  if (!input)
31
34
  return false;
35
+ if (hasTerminalMouseSequence(input))
36
+ return false;
32
37
  if (input.startsWith("\x1b"))
33
38
  return false;
34
39
  if (isRawEscapeTail(input))
@@ -47,20 +52,116 @@ export function formatSkillPickerRow(skill, options) {
47
52
  : `${marker}${nameCell}`;
48
53
  return padVisual(truncateVisual(row, width), width);
49
54
  }
55
+ export const MODEL_PICKER_MAX_BODY_ROWS = 10;
56
+ export const MODEL_PICKER_CHROME_ROWS = 13;
57
+ export function modelPickerBodyRows(termHeight) {
58
+ const rows = Number.isFinite(termHeight) ? Math.floor(termHeight) : 24;
59
+ return Math.max(1, Math.min(MODEL_PICKER_MAX_BODY_ROWS, rows - MODEL_PICKER_CHROME_ROWS));
60
+ }
61
+ export function clampPickerIndex(index, length) {
62
+ if (length <= 0)
63
+ return 0;
64
+ return Math.max(0, Math.min(length - 1, index));
65
+ }
66
+ export function pickerWindowStart(selectedIndex, length, visibleRows) {
67
+ const rows = Math.max(1, Math.floor(visibleRows));
68
+ const safeIndex = clampPickerIndex(selectedIndex, length);
69
+ const maxStart = Math.max(0, length - rows);
70
+ return Math.max(0, Math.min(maxStart, safeIndex - Math.floor(rows / 2)));
71
+ }
72
+ export function padPickerRows(rows, bodyRows, width) {
73
+ const rowCount = Math.max(1, Math.floor(bodyRows));
74
+ const rowWidth = Math.max(1, Math.floor(width));
75
+ const padded = rows.slice(0, rowCount).map((row) => padVisual(truncateVisual(row, rowWidth), rowWidth));
76
+ while (padded.length < rowCount) {
77
+ padded.push(padVisual("", rowWidth));
78
+ }
79
+ return padded;
80
+ }
81
+ export function formatReasoningLevelsLabel(levels) {
82
+ const normalized = levels.length > 0 ? levels : ["off"];
83
+ return `effort ${normalized.join("/")}`;
84
+ }
85
+ export function formatModelPickerRow(option, options) {
86
+ const width = Math.max(24, options.width);
87
+ const marker = options.selected ? "> " : " ";
88
+ const label = option.label.replace(/\s+/g, " ").trim();
89
+ const provider = option.providerBadge.replace(/\s+/g, " ").trim();
90
+ const effort = formatReasoningLevelsLabel(option.reasoningLevels);
91
+ const current = options.current ? " ●" : "";
92
+ const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
93
+ const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
94
+ const labelWidth = Math.max(6, width - marker.length - providerWidth - effortWidth - 4 - current.length);
95
+ const row = [
96
+ marker,
97
+ padVisual(truncateVisual(label, labelWidth), labelWidth),
98
+ " ",
99
+ padVisual(truncateVisual(provider, providerWidth), providerWidth),
100
+ " ",
101
+ truncateVisual(effort, effortWidth),
102
+ current,
103
+ ].join("");
104
+ return padVisual(truncateVisual(row, width), width);
105
+ }
106
+ export function formatEffortPickerRow(level, options) {
107
+ const width = Math.max(24, options.width);
108
+ const marker = options.selected ? "> " : " ";
109
+ const row = `${marker}${level} ${effortDescription(level)}`;
110
+ return padVisual(truncateVisual(row, width), width);
111
+ }
112
+ export function formatNoModelResultsRow(query, width) {
113
+ const rowWidth = Math.max(24, width);
114
+ const normalizedQuery = query.replace(/\s+/g, " ").trim();
115
+ const row = normalizedQuery
116
+ ? ` No models match "${normalizedQuery}"`
117
+ : " No models available";
118
+ return padVisual(truncateVisual(row, rowWidth), rowWidth);
119
+ }
120
+ export function preferredEffortIndex(option, currentThinkingLevel) {
121
+ const preferred = normalizeThinkingLevel(currentThinkingLevel, option.reasoningLevels);
122
+ const index = option.reasoningLevels.indexOf(preferred);
123
+ return index >= 0 ? index : 0;
124
+ }
125
+ export function shouldOpenEffortPicker(option) {
126
+ return option.reasoningLevels.length > 1;
127
+ }
128
+ function effortDescription(level) {
129
+ switch (level) {
130
+ case "off":
131
+ return "no reasoning effort";
132
+ case "minimal":
133
+ return "fastest reasoning";
134
+ case "low":
135
+ return "light reasoning";
136
+ case "medium":
137
+ return "balanced reasoning";
138
+ case "high":
139
+ return "deeper reasoning";
140
+ case "xhigh":
141
+ return "extra high reasoning";
142
+ case "max":
143
+ return "maximum provider effort";
144
+ default:
145
+ return "reasoning effort";
146
+ }
147
+ }
50
148
  function normalizeEscapeSequence(input) {
51
149
  return input.startsWith("\x1b") ? input.slice(1) : input;
52
150
  }
53
151
  function isRawEscapeTail(input) {
54
152
  return /^(?:O[ABCDHF]|\[[\d;:]*[A-Za-z~])$/.test(input);
55
153
  }
56
- export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
154
+ export function ModelPicker({ registry, current, currentThinkingLevel, recent, onSelect, onCancel }) {
57
155
  const theme = useTheme();
58
156
  const { stdout } = useStdout();
59
157
  const termHeight = stdout?.rows || 24;
60
- const maxVisible = Math.max(5, termHeight - 10);
158
+ const terminalColumns = stdout?.columns || 80;
159
+ const bodyRows = modelPickerBodyRows(termHeight);
160
+ const rowWidth = Math.max(36, Math.min(110, terminalColumns - 6));
61
161
  const [rawOptions, setRawOptions] = useState(() => buildLocalModelOptions(registry, current, recent));
62
162
  const [query, setQuery] = useState("");
63
163
  const [selectedIndex, setSelectedIndex] = useState(() => preferredModelIndex(buildLocalModelOptions(registry, current, recent), current));
164
+ const [phase, setPhase] = useState({ kind: "model" });
64
165
  useEffect(() => {
65
166
  let cancelled = false;
66
167
  const localOptions = buildLocalModelOptions(registry, current, recent);
@@ -103,13 +204,21 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
103
204
  if (current && !seen.has(current)) {
104
205
  const { providerId } = decodeModel(current);
105
206
  const provider = enabled.find((p) => p.id === providerId);
106
- opts.unshift({ id: current, label: displayModel(current), group: "Current", providerBadge: provider?.name || providerId || "" });
207
+ opts.unshift({
208
+ id: current,
209
+ label: displayModel(current),
210
+ group: "Current",
211
+ providerBadge: provider?.name || providerId || "",
212
+ reasoningLevels: reasoningLevelsForModel(current),
213
+ });
107
214
  }
108
215
  if (!cancelled) {
109
216
  setRawOptions(opts);
110
217
  setSelectedIndex((index) => {
111
218
  const currentIndex = preferredModelIndex(opts, current);
112
- return index === preferredModelIndex(localOptions, current) ? currentIndex : Math.min(index, Math.max(0, opts.length - 1));
219
+ return index === preferredModelIndex(localOptions, current)
220
+ ? currentIndex
221
+ : clampPickerIndex(index, opts.length);
113
222
  });
114
223
  }
115
224
  }
@@ -125,23 +234,59 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
125
234
  return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
126
235
  }, [rawOptions, query]);
127
236
  useInput((input, key) => {
237
+ if (isKeyReleaseEvent(key))
238
+ return;
128
239
  const action = resolvePickerKeyAction(input, key);
240
+ if (phase.kind === "effort") {
241
+ const levels = phase.model.reasoningLevels;
242
+ if (action === "escape" || action === "backspace" || action === "delete") {
243
+ setPhase({ kind: "model" });
244
+ return;
245
+ }
246
+ if (action === "enter") {
247
+ onSelect(phase.model.id, levels[clampPickerIndex(phase.selectedIndex, levels.length)] ?? "off");
248
+ return;
249
+ }
250
+ if (action === "up") {
251
+ setPhase((currentPhase) => currentPhase.kind === "effort"
252
+ ? { ...currentPhase, selectedIndex: clampPickerIndex(currentPhase.selectedIndex - 1, levels.length) }
253
+ : currentPhase);
254
+ return;
255
+ }
256
+ if (action === "down") {
257
+ setPhase((currentPhase) => currentPhase.kind === "effort"
258
+ ? { ...currentPhase, selectedIndex: clampPickerIndex(currentPhase.selectedIndex + 1, levels.length) }
259
+ : currentPhase);
260
+ return;
261
+ }
262
+ return;
263
+ }
129
264
  if (action === "escape") {
130
265
  onCancel();
131
266
  return;
132
267
  }
133
268
  if (action === "enter") {
134
- const opt = options[selectedIndex];
135
- if (opt)
136
- onSelect(opt.id);
269
+ const opt = options[clampPickerIndex(selectedIndex, options.length)];
270
+ if (opt) {
271
+ if (shouldOpenEffortPicker(opt)) {
272
+ setPhase({
273
+ kind: "effort",
274
+ model: opt,
275
+ selectedIndex: preferredEffortIndex(opt, currentThinkingLevel),
276
+ });
277
+ }
278
+ else {
279
+ onSelect(opt.id, opt.reasoningLevels[0] ?? "off");
280
+ }
281
+ }
137
282
  return;
138
283
  }
139
284
  if (action === "up") {
140
- setSelectedIndex((i) => Math.max(0, i - 1));
285
+ setSelectedIndex((i) => clampPickerIndex(i - 1, options.length));
141
286
  return;
142
287
  }
143
288
  if (action === "down") {
144
- setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
289
+ setSelectedIndex((i) => clampPickerIndex(i + 1, options.length));
145
290
  return;
146
291
  }
147
292
  if (action === "backspace" || action === "delete") {
@@ -161,22 +306,67 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
161
306
  return;
162
307
  }
163
308
  });
164
- const start = Math.max(0, Math.min(selectedIndex, options.length - maxVisible));
165
- const visible = options.slice(start, start + maxVisible);
166
- return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: "Select Model" }), _jsx(SearchField, { query: query, placeholder: "Type to search models..." }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc cancel \u00B7 Backspace clear" }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [options.length === 0 && (_jsxs(Text, { color: theme.muted, children: ["No models match \"", query, "\""] })), visible.map((opt, i) => {
167
- const actualIndex = start + i;
168
- const isSelected = actualIndex === selectedIndex;
169
- return (_jsxs(Box, { children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", opt.label] }), _jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.muted, dimColor: true, children: opt.providerBadge }) }), opt.id === current && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: theme.accent, children: "\u25CF" }) }))] }, opt.id));
170
- })] })] }));
309
+ const safeSelectedIndex = clampPickerIndex(selectedIndex, options.length);
310
+ const start = pickerWindowStart(safeSelectedIndex, options.length, bodyRows);
311
+ const visible = options.slice(start, start + bodyRows);
312
+ const rawModelRows = options.length === 0
313
+ ? [{
314
+ key: "no-results",
315
+ row: formatNoModelResultsRow(query, rowWidth),
316
+ selected: false,
317
+ }]
318
+ : visible.map((opt, i) => {
319
+ const actualIndex = start + i;
320
+ const isSelected = actualIndex === safeSelectedIndex;
321
+ return {
322
+ key: opt.id,
323
+ row: formatModelPickerRow(opt, {
324
+ selected: isSelected,
325
+ current: opt.id === current,
326
+ width: rowWidth,
327
+ }),
328
+ selected: isSelected,
329
+ };
330
+ });
331
+ const modelRows = padPickerRows(rawModelRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
332
+ key: rawModelRows[index]?.key ?? `blank-${index}`,
333
+ row,
334
+ selected: rawModelRows[index]?.selected ?? false,
335
+ }));
336
+ 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))) })] }));
337
+ }
338
+ function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
339
+ const theme = useTheme();
340
+ const safeSelectedIndex = clampPickerIndex(selectedIndex, model.reasoningLevels.length);
341
+ const rawRows = model.reasoningLevels.map((level, index) => ({
342
+ key: level,
343
+ row: formatEffortPickerRow(level, {
344
+ selected: index === safeSelectedIndex,
345
+ width: rowWidth,
346
+ }),
347
+ selected: index === safeSelectedIndex,
348
+ }));
349
+ const effortRows = padPickerRows(rawRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
350
+ key: rawRows[index]?.key ?? `blank-${index}`,
351
+ row,
352
+ selected: rawRows[index]?.selected ?? false,
353
+ }));
354
+ const modelDetail = padVisual(truncateVisual(`${model.label} · ${model.providerBadge}`, rowWidth), rowWidth);
355
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: theme.muted, children: modelDetail }) }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter select \u00B7 Esc back" }), _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", marginTop: 1, children: effortRows.map(({ key, row, selected }) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: selected ? theme.accent : undefined, bold: selected, children: row }) }, key))) })] }));
171
356
  }
172
- function SearchField({ query, placeholder }) {
357
+ function SearchField({ query, placeholder, width }) {
173
358
  const theme = useTheme();
174
359
  const [cursorVisible, setCursorVisible] = useState(true);
175
360
  useEffect(() => {
176
361
  const t = setInterval(() => setCursorVisible((v) => !v), 500);
177
362
  return () => clearInterval(t);
178
363
  }, []);
179
- return (_jsxs(Box, { marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.accent, children: "❯ " }), _jsx(Text, { children: query }), _jsx(Text, { color: theme.accent, inverse: cursorVisible, children: " " }), !query && _jsxs(Text, { color: theme.muted, dimColor: true, children: [" ", placeholder] })] }));
364
+ const contentBudget = width ? Math.max(1, width - 3) : undefined;
365
+ const visibleQuery = contentBudget ? truncateVisual(query, contentBudget) : query;
366
+ const visiblePlaceholder = !query
367
+ ? (contentBudget ? truncateVisual(` ${placeholder}`, contentBudget) : ` ${placeholder}`)
368
+ : "";
369
+ return (_jsxs(Box, { height: 1, overflow: "hidden", marginTop: 1, children: [_jsx(Text, { color: theme.accent, children: "❯ " }), _jsx(Text, { children: visibleQuery }), _jsx(Text, { color: theme.accent, inverse: cursorVisible, children: " " }), visiblePlaceholder && _jsx(Text, { color: theme.muted, dimColor: true, children: visiblePlaceholder })] }));
180
370
  }
181
371
  export function buildLocalModelOptions(registry, current, recent) {
182
372
  const enabled = registry.getEnabled();
@@ -210,6 +400,7 @@ export function buildLocalModelOptions(registry, current, recent) {
210
400
  label: displayModel(current),
211
401
  group: "Current",
212
402
  providerBadge: provider?.name || providerId || "",
403
+ reasoningLevels: reasoningLevelsForModel(current),
213
404
  });
214
405
  }
215
406
  return opts;
@@ -231,12 +422,19 @@ function appendModelOption(options, seen, option) {
231
422
  if (seen.has(option.id))
232
423
  return;
233
424
  seen.add(option.id);
234
- options.push(option);
425
+ options.push({
426
+ ...option,
427
+ reasoningLevels: option.reasoningLevels ?? reasoningLevelsForModel(option.id),
428
+ });
235
429
  }
236
430
  function preferredModelIndex(options, current) {
237
431
  const idx = options.findIndex((option) => option.id === current);
238
432
  return idx >= 0 ? idx : 0;
239
433
  }
434
+ function reasoningLevelsForModel(model) {
435
+ const { providerId, modelId } = decodeModel(model);
436
+ return getAvailableThinkingLevels(providerId || "openai", modelId);
437
+ }
240
438
  export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
241
439
  const theme = useTheme();
242
440
  const { stdout } = useStdout();
@@ -247,6 +445,8 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
247
445
  return idx >= 0 ? idx : 0;
248
446
  });
249
447
  useInput((input, key) => {
448
+ if (isKeyReleaseEvent(key))
449
+ return;
250
450
  const action = resolvePickerKeyAction(input, key);
251
451
  if (action === "escape") {
252
452
  onCancel();
@@ -294,6 +494,8 @@ export function KeyPicker({ providerName, onSubmit, onCancel }) {
294
494
  const theme = useTheme();
295
495
  const [value, setValue] = useState("");
296
496
  useInput((input, key) => {
497
+ if (isKeyReleaseEvent(key))
498
+ return;
297
499
  const action = resolvePickerKeyAction(input, key);
298
500
  if (action === "escape") {
299
501
  onCancel();
@@ -338,6 +540,8 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
338
540
  return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
339
541
  }, [query, skills]);
340
542
  useInput((input, key) => {
543
+ if (isKeyReleaseEvent(key))
544
+ return;
341
545
  const action = resolvePickerKeyAction(input, key);
342
546
  if (action === "escape") {
343
547
  onCancel();
@@ -1,14 +1,24 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
+ import { isKeyReleaseEvent } from "./key-events.js";
4
5
  import { useTheme } from "./theme.js";
5
6
  import { MarkdownContent } from "./markdown.js";
7
+ import { stripTerminalMouseSequences } from "./terminal-mouse.js";
6
8
  export function PlanConfirm({ initialPlan, onApprove, onReject }) {
7
9
  const theme = useTheme();
8
10
  const [stage, setStage] = useState("view");
9
11
  const [draft, setDraft] = useState(initialPlan);
10
12
  const [cursor, setCursor] = useState(initialPlan.length);
11
13
  useInput((input, key) => {
14
+ if (isKeyReleaseEvent(key))
15
+ return;
16
+ const strippedMouseInput = stripTerminalMouseSequences(input);
17
+ if (strippedMouseInput !== input) {
18
+ if (!strippedMouseInput)
19
+ return;
20
+ input = strippedMouseInput;
21
+ }
12
22
  if (stage === "view") {
13
23
  if (key.escape || input === "n" || input === "N") {
14
24
  onReject();
@@ -1,7 +1,9 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
+ import { isKeyReleaseEvent } from "./key-events.js";
4
5
  import { useTheme } from "./theme.js";
6
+ import { stripTerminalMouseSequences } from "./terminal-mouse.js";
5
7
  export function QuestionDialog({ request, onSubmit, onCancel }) {
6
8
  const theme = useTheme();
7
9
  const [index, setIndex] = useState(0);
@@ -51,6 +53,14 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
51
53
  }));
52
54
  };
53
55
  useInput((input, key) => {
56
+ if (isKeyReleaseEvent(key))
57
+ return;
58
+ const strippedMouseInput = stripTerminalMouseSequences(input);
59
+ if (strippedMouseInput !== input) {
60
+ if (!strippedMouseInput)
61
+ return;
62
+ input = strippedMouseInput;
63
+ }
54
64
  if (key.escape) {
55
65
  onCancel();
56
66
  return;
@@ -1,3 +1,4 @@
1
+ import React from "react";
1
2
  import type { Agent } from "../agent.js";
2
3
  import type { CliArgs } from "../cli.js";
3
4
  import type { SessionManager } from "../session.js";
@@ -23,8 +24,15 @@ export interface RunTuiOptions {
23
24
  questionController?: QuestionController;
24
25
  bashAllowlist?: BashAllowlist;
25
26
  settingsManager?: SettingsManager;
27
+ switchSession?: (sessionFile: string) => {
28
+ manager: SessionManager;
29
+ } | {
30
+ error: string;
31
+ };
26
32
  lspService?: LspService;
27
33
  mcpManager?: McpManager;
34
+ /** Shared with the model-facing goal tools and the Ink auto-continuation loop. */
35
+ goalStore?: import("../goal/store.js").GoalStore;
28
36
  themeMode?: ThemeMode;
29
37
  themeOverrides?: Record<string, string>;
30
38
  detectedTheme?: ResolvedTheme;
@@ -36,7 +44,10 @@ export interface RunTuiOptions {
36
44
  bypassEnabled?: boolean;
37
45
  /** One-line "update available" notice rendered under the welcome banner version. */
38
46
  updateNotice?: string;
47
+ /** Late update notice refresh surfaced after startup without restarting Ink. */
48
+ updateNoticeRefresh?: Promise<string | null>;
39
49
  /** External lifecycle hooks, threaded into slash-command execution. */
40
50
  hookController?: ExternalHookController;
41
51
  }
52
+ export declare function createInkAppElement(agent: Agent, args: CliArgs, options: RunTuiOptions, onExit: (summary: ExitSummary) => void): React.ReactElement;
42
53
  export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<ExitSummary | undefined>;
@@ -1,14 +1,11 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { render } from "ink";
3
3
  import { App } from "./app.js";
4
- import { MOUSE_REPORTING_DISABLE } from "./terminal-mouse.js";
5
- // DECSET 1007: terminals translate the mouse wheel into Up/Down arrow keys
6
- // while the alternate screen is active. Mouse reporting stays OFF on purpose
7
- // so plain drag-select and copy keep their native terminal behavior; the
8
- // composer classifies wheel-synthesized arrows vs real key presses.
9
- const ALTERNATE_SCROLL_ENABLE = "\x1b[?1007h";
10
- const ALTERNATE_SCROLL_DISABLE = "\x1b[?1007l";
4
+ import { ALTERNATE_SCROLL_DISABLE, MOUSE_REPORTING_DISABLE, MOUSE_REPORTING_ENABLE, } from "./terminal-mouse.js";
11
5
  import { warmHighlighter } from "./code-highlight.js";
6
+ export function createInkAppElement(agent, args, options, onExit) {
7
+ return (_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, switchSession: options.switchSession, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, goalStore: options.goalStore, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, updateNoticeRefresh: options.updateNoticeRefresh, hookController: options.hookController, onExit: onExit }));
8
+ }
12
9
  /**
13
10
  * Best-effort terminal restore for abnormal exits. DECSET mouse modes are
14
11
  * global terminal state — if the process dies without disabling them, the
@@ -49,15 +46,15 @@ export async function runTui(agent, args, options = {}) {
49
46
  };
50
47
  process.on("uncaughtException", onFatalError);
51
48
  process.on("SIGTERM", onSigterm);
52
- const instance = render(_jsx(App, { agent: agent, args: args, sessionManager: options.sessionManager, createProvider: options.createProvider, registry: options.registry, skillRegistry: options.skillRegistry, planHandlerRef: options.planHandlerRef, approvalHandlerRef: options.approvalHandlerRef, questionController: options.questionController, bashAllowlist: options.bashAllowlist, settingsManager: options.settingsManager, lspService: options.lspService, mcpManager: options.mcpManager, themeMode: options.themeMode, themeOverrides: options.themeOverrides, detectedTheme: options.detectedTheme, onThemeModeChange: options.onThemeModeChange, flushMemory: options.flushMemory, runMemoryCompaction: options.runMemoryCompaction, runMemorySummary: options.runMemorySummary, runMemoryRefresh: options.runMemoryRefresh, bypassEnabled: options.bypassEnabled, updateNotice: options.updateNotice, hookController: options.hookController, onExit: (summary) => {
53
- // The app already called useApp().exit() inside requestExit, which
54
- // triggers Ink's own unmount + TTY restore. waitUntilExit() below is
55
- // the canonical signal that we're done — we deliberately do *not*
56
- // call instance.unmount() again here to avoid double-teardown
57
- // warnings on React 19. We capture the summary and render it after
58
- // teardown so it lands in the real shell scrollback (Claude-Code style).
59
- exitSummary = summary;
60
- } }), {
49
+ const instance = render(createInkAppElement(agent, args, options, (summary) => {
50
+ // The app already called useApp().exit() inside requestExit, which
51
+ // triggers Ink's own unmount + TTY restore. waitUntilExit() below is
52
+ // the canonical signal that we're done — we deliberately do *not*
53
+ // call instance.unmount() again here to avoid double-teardown
54
+ // warnings on React 19. We capture the summary and render it after
55
+ // teardown so it lands in the real shell scrollback (Claude-Code style).
56
+ exitSummary = summary;
57
+ }), {
61
58
  // Bubble owns Ctrl+C so it can route both raw ETX and kitty keyboard
62
59
  // Ctrl+C through App.requestExit(). Ink's default only exits reliably
63
60
  // for raw "\x03"; with kitty keyboard it can swallow the parsed
@@ -65,10 +62,7 @@ export async function runTui(agent, args, options = {}) {
65
62
  exitOnCtrlC: false,
66
63
  kittyKeyboard: {
67
64
  mode: "enabled",
68
- // reportEventTypes lets the composer tell real arrow-key presses
69
- // (kitty-enhanced, carry eventType) apart from the bare arrow
70
- // sequences terminals synthesize for wheel scrolling in alternate
71
- // screen — see the classifier in input-box.tsx.
65
+ // reportEventTypes keeps release events out of text input.
72
66
  flags: ["disambiguateEscapeCodes", "reportEventTypes"],
73
67
  },
74
68
  // The whole point of the Ink migration: render into the 1049 alternate
@@ -76,21 +70,20 @@ export async function runTui(agent, args, options = {}) {
76
70
  // Ink degrades this to false automatically when stdout is not a TTY.
77
71
  alternateScreen: true,
78
72
  });
79
- // Enable alternate-scroll after render() so it follows alt-screen entry:
80
- // the wheel arrives as Up/Down arrows, while plain drag-select and copy
81
- // keep their native terminal behavior (no mouse reporting).
73
+ // Keep alternate-scroll disabled so wheel events do not alias keyboard
74
+ // arrows. Enable SGR mouse reporting after alt-screen entry so wheel events
75
+ // scroll the transcript through a distinct input channel.
82
76
  if (process.stdout.isTTY) {
83
- process.stdout.write(ALTERNATE_SCROLL_ENABLE);
77
+ process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_ENABLE);
84
78
  }
85
79
  try {
86
80
  await instance.waitUntilExit();
87
81
  }
88
82
  finally {
89
- // Reset scroll translation before anything is printed to the primary
90
- // screen; Ink has already left the alt screen by the time
91
- // waitUntilExit() resolves.
83
+ // Reset mouse reporting before anything is printed to the primary screen;
84
+ // Ink has already left the alt screen by the time waitUntilExit() resolves.
92
85
  if (process.stdout.isTTY) {
93
- process.stdout.write(ALTERNATE_SCROLL_DISABLE);
86
+ process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_DISABLE);
94
87
  }
95
88
  process.off("uncaughtException", onFatalError);
96
89
  process.off("SIGTERM", onSigterm);
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
3
  import { Box, Text, useInput, useStdout } from "ink";
4
+ import { isKeyReleaseEvent } from "./key-events.js";
4
5
  import { useTheme } from "./theme.js";
5
6
  import { formatRelativeTime } from "./recent-activity.js";
6
7
  import { padVisual, truncateVisual } from "../text-display.js";
@@ -19,6 +20,8 @@ export function SessionPicker({ currentCwd, currentSessions, allSessions, onSele
19
20
  : Math.min(selectedSessionIdx, sessionRowIndices.length - 1);
20
21
  const selectedRowIndex = sessionRowIndices[clampedIdx] ?? -1;
21
22
  useInput((input, key) => {
23
+ if (isKeyReleaseEvent(key))
24
+ return;
22
25
  if (key.escape) {
23
26
  onCancel();
24
27
  return;
@@ -0,0 +1,5 @@
1
+ import type { SubmitPayload } from "./input-box.js";
2
+ export type StartingSubmitDecision = "accept" | "ignore" | "queue";
3
+ export declare function submitPayloadFingerprint(payload: SubmitPayload): string;
4
+ export declare function decideStartingSubmit(activeFingerprint: string | null, payload: SubmitPayload): StartingSubmitDecision;
5
+ export declare function decideStartingSubmitFingerprint(activeFingerprint: string | null, submitFingerprint: string): StartingSubmitDecision;
@@ -0,0 +1,25 @@
1
+ import { createHash } from "node:crypto";
2
+ function hashValue(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
5
+ export function submitPayloadFingerprint(payload) {
6
+ return JSON.stringify({
7
+ text: payload.text,
8
+ displayText: payload.displayText ?? "",
9
+ images: payload.images.map((image) => ({
10
+ mediaType: image.mediaType,
11
+ bytes: image.bytes,
12
+ filename: image.filename ?? "",
13
+ sourcePath: image.sourcePath ?? "",
14
+ dataUrlHash: hashValue(image.dataUrl),
15
+ })),
16
+ });
17
+ }
18
+ export function decideStartingSubmit(activeFingerprint, payload) {
19
+ return decideStartingSubmitFingerprint(activeFingerprint, submitPayloadFingerprint(payload));
20
+ }
21
+ export function decideStartingSubmitFingerprint(activeFingerprint, submitFingerprint) {
22
+ if (!activeFingerprint)
23
+ return "accept";
24
+ return activeFingerprint === submitFingerprint ? "ignore" : "queue";
25
+ }
@@ -1,5 +1,17 @@
1
- export declare const MOUSE_REPORTING_DISABLE = "\u001B[?1006l\u001B[?1000l";
1
+ export declare const ALTERNATE_SCROLL_ENABLE = "\u001B[?1007h";
2
+ export declare const ALTERNATE_SCROLL_DISABLE = "\u001B[?1007l";
3
+ export declare const MOUSE_REPORTING_ENABLE = "\u001B[?1000h\u001B[?1006h";
4
+ export declare const MOUSE_REPORTING_DISABLE = "\u001B[?1003l\u001B[?1002l\u001B[?1000l\u001B[?1005l\u001B[?1006l\u001B[?1015l";
2
5
  export type MouseWheelDirection = "up" | "down";
6
+ export interface TerminalMouseInput {
7
+ strippedInput: string;
8
+ wheelDirections: MouseWheelDirection[];
9
+ hasMouse: boolean;
10
+ }
11
+ export declare function sanitizeTerminalMouseInput(input: string): TerminalMouseInput;
12
+ export declare function transcriptScrollLinesFromMouseInput(mouseInput: TerminalMouseInput, options: {
13
+ overlayActive: boolean;
14
+ }): number[];
3
15
  export declare function stripTerminalMouseSequences(input: string): string;
4
16
  export declare function hasTerminalMouseSequence(input: string): boolean;
5
17
  export declare function parseTerminalMouseWheel(input: string): MouseWheelDirection[];