@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,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,128 @@ 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
+ // MiniMax models expose thinking as a binary on/off switch (the API's `thinking`
82
+ // param is disabled|adaptive — there's no graded effort), so render the "on"
83
+ // level as on/off instead of our internal "medium". Scoped to MiniMax only —
84
+ // other 2-level models (e.g. GLM toggles) keep their effort labels.
85
+ function isMiniMaxToggleModel(modelId) {
86
+ return modelId.toLowerCase().includes("minimax");
87
+ }
88
+ export function formatReasoningLevelsLabel(levels, asToggle = false) {
89
+ const normalized = levels.length > 0 ? levels : ["off"];
90
+ if (asToggle)
91
+ return "thinking on/off";
92
+ return `effort ${normalized.join("/")}`;
93
+ }
94
+ export function formatModelPickerRow(option, options) {
95
+ const width = Math.max(24, options.width);
96
+ const marker = options.selected ? "> " : " ";
97
+ const label = option.label.replace(/\s+/g, " ").trim();
98
+ const provider = option.providerBadge.replace(/\s+/g, " ").trim();
99
+ const effort = formatReasoningLevelsLabel(option.reasoningLevels, isMiniMaxToggleModel(option.id));
100
+ const current = options.current ? " ●" : "";
101
+ const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
102
+ const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
103
+ const labelWidth = Math.max(6, width - marker.length - providerWidth - effortWidth - 4 - current.length);
104
+ const row = [
105
+ marker,
106
+ padVisual(truncateVisual(label, labelWidth), labelWidth),
107
+ " ",
108
+ padVisual(truncateVisual(provider, providerWidth), providerWidth),
109
+ " ",
110
+ truncateVisual(effort, effortWidth),
111
+ current,
112
+ ].join("");
113
+ return padVisual(truncateVisual(row, width), width);
114
+ }
115
+ export function formatEffortPickerRow(level, options) {
116
+ const width = Math.max(24, options.width);
117
+ const marker = options.selected ? "> " : " ";
118
+ const name = options.asToggle ? (level === "off" ? "off" : "on") : level;
119
+ const row = `${marker}${name} ${effortDescription(level, options.asToggle)}`;
120
+ return padVisual(truncateVisual(row, width), width);
121
+ }
122
+ export function formatNoModelResultsRow(query, width) {
123
+ const rowWidth = Math.max(24, width);
124
+ const normalizedQuery = query.replace(/\s+/g, " ").trim();
125
+ const row = normalizedQuery
126
+ ? ` No models match "${normalizedQuery}"`
127
+ : " No models available";
128
+ return padVisual(truncateVisual(row, rowWidth), rowWidth);
129
+ }
130
+ export function preferredEffortIndex(option, currentThinkingLevel) {
131
+ const preferred = normalizeThinkingLevel(currentThinkingLevel, option.reasoningLevels);
132
+ const index = option.reasoningLevels.indexOf(preferred);
133
+ return index >= 0 ? index : 0;
134
+ }
135
+ export function shouldOpenEffortPicker(option) {
136
+ return option.reasoningLevels.length > 1;
137
+ }
138
+ function effortDescription(level, asToggle) {
139
+ if (asToggle)
140
+ return level === "off" ? "thinking disabled" : "thinking enabled";
141
+ switch (level) {
142
+ case "off":
143
+ return "no reasoning effort";
144
+ case "minimal":
145
+ return "fastest reasoning";
146
+ case "low":
147
+ return "light reasoning";
148
+ case "medium":
149
+ return "balanced reasoning";
150
+ case "high":
151
+ return "deeper reasoning";
152
+ case "xhigh":
153
+ return "extra high reasoning";
154
+ case "max":
155
+ return "maximum provider effort";
156
+ default:
157
+ return "reasoning effort";
158
+ }
159
+ }
50
160
  function normalizeEscapeSequence(input) {
51
161
  return input.startsWith("\x1b") ? input.slice(1) : input;
52
162
  }
53
163
  function isRawEscapeTail(input) {
54
164
  return /^(?:O[ABCDHF]|\[[\d;:]*[A-Za-z~])$/.test(input);
55
165
  }
56
- export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
166
+ export function ModelPicker({ registry, current, currentThinkingLevel, recent, onSelect, onCancel }) {
57
167
  const theme = useTheme();
58
168
  const { stdout } = useStdout();
59
169
  const termHeight = stdout?.rows || 24;
60
- const maxVisible = Math.max(5, termHeight - 10);
170
+ const terminalColumns = stdout?.columns || 80;
171
+ const bodyRows = modelPickerBodyRows(termHeight);
172
+ const rowWidth = Math.max(36, Math.min(110, terminalColumns - 6));
61
173
  const [rawOptions, setRawOptions] = useState(() => buildLocalModelOptions(registry, current, recent));
62
174
  const [query, setQuery] = useState("");
63
175
  const [selectedIndex, setSelectedIndex] = useState(() => preferredModelIndex(buildLocalModelOptions(registry, current, recent), current));
176
+ const [phase, setPhase] = useState({ kind: "model" });
64
177
  useEffect(() => {
65
178
  let cancelled = false;
66
179
  const localOptions = buildLocalModelOptions(registry, current, recent);
@@ -103,13 +216,21 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
103
216
  if (current && !seen.has(current)) {
104
217
  const { providerId } = decodeModel(current);
105
218
  const provider = enabled.find((p) => p.id === providerId);
106
- opts.unshift({ id: current, label: displayModel(current), group: "Current", providerBadge: provider?.name || providerId || "" });
219
+ opts.unshift({
220
+ id: current,
221
+ label: displayModel(current),
222
+ group: "Current",
223
+ providerBadge: provider?.name || providerId || "",
224
+ reasoningLevels: reasoningLevelsForModel(current),
225
+ });
107
226
  }
108
227
  if (!cancelled) {
109
228
  setRawOptions(opts);
110
229
  setSelectedIndex((index) => {
111
230
  const currentIndex = preferredModelIndex(opts, current);
112
- return index === preferredModelIndex(localOptions, current) ? currentIndex : Math.min(index, Math.max(0, opts.length - 1));
231
+ return index === preferredModelIndex(localOptions, current)
232
+ ? currentIndex
233
+ : clampPickerIndex(index, opts.length);
113
234
  });
114
235
  }
115
236
  }
@@ -125,23 +246,59 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
125
246
  return rawOptions.filter((opt) => opt.label.toLowerCase().includes(q) || opt.providerBadge.toLowerCase().includes(q));
126
247
  }, [rawOptions, query]);
127
248
  useInput((input, key) => {
249
+ if (isKeyReleaseEvent(key))
250
+ return;
128
251
  const action = resolvePickerKeyAction(input, key);
252
+ if (phase.kind === "effort") {
253
+ const levels = phase.model.reasoningLevels;
254
+ if (action === "escape" || action === "backspace" || action === "delete") {
255
+ setPhase({ kind: "model" });
256
+ return;
257
+ }
258
+ if (action === "enter") {
259
+ onSelect(phase.model.id, levels[clampPickerIndex(phase.selectedIndex, levels.length)] ?? "off");
260
+ return;
261
+ }
262
+ if (action === "up") {
263
+ setPhase((currentPhase) => currentPhase.kind === "effort"
264
+ ? { ...currentPhase, selectedIndex: clampPickerIndex(currentPhase.selectedIndex - 1, levels.length) }
265
+ : currentPhase);
266
+ return;
267
+ }
268
+ if (action === "down") {
269
+ setPhase((currentPhase) => currentPhase.kind === "effort"
270
+ ? { ...currentPhase, selectedIndex: clampPickerIndex(currentPhase.selectedIndex + 1, levels.length) }
271
+ : currentPhase);
272
+ return;
273
+ }
274
+ return;
275
+ }
129
276
  if (action === "escape") {
130
277
  onCancel();
131
278
  return;
132
279
  }
133
280
  if (action === "enter") {
134
- const opt = options[selectedIndex];
135
- if (opt)
136
- onSelect(opt.id);
281
+ const opt = options[clampPickerIndex(selectedIndex, options.length)];
282
+ if (opt) {
283
+ if (shouldOpenEffortPicker(opt)) {
284
+ setPhase({
285
+ kind: "effort",
286
+ model: opt,
287
+ selectedIndex: preferredEffortIndex(opt, currentThinkingLevel),
288
+ });
289
+ }
290
+ else {
291
+ onSelect(opt.id, opt.reasoningLevels[0] ?? "off");
292
+ }
293
+ }
137
294
  return;
138
295
  }
139
296
  if (action === "up") {
140
- setSelectedIndex((i) => Math.max(0, i - 1));
297
+ setSelectedIndex((i) => clampPickerIndex(i - 1, options.length));
141
298
  return;
142
299
  }
143
300
  if (action === "down") {
144
- setSelectedIndex((i) => Math.min(options.length - 1, i + 1));
301
+ setSelectedIndex((i) => clampPickerIndex(i + 1, options.length));
145
302
  return;
146
303
  }
147
304
  if (action === "backspace" || action === "delete") {
@@ -161,22 +318,68 @@ export function ModelPicker({ registry, current, recent, onSelect, onCancel }) {
161
318
  return;
162
319
  }
163
320
  });
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
- })] })] }));
321
+ const safeSelectedIndex = clampPickerIndex(selectedIndex, options.length);
322
+ const start = pickerWindowStart(safeSelectedIndex, options.length, bodyRows);
323
+ const visible = options.slice(start, start + bodyRows);
324
+ const rawModelRows = options.length === 0
325
+ ? [{
326
+ key: "no-results",
327
+ row: formatNoModelResultsRow(query, rowWidth),
328
+ selected: false,
329
+ }]
330
+ : visible.map((opt, i) => {
331
+ const actualIndex = start + i;
332
+ const isSelected = actualIndex === safeSelectedIndex;
333
+ return {
334
+ key: opt.id,
335
+ row: formatModelPickerRow(opt, {
336
+ selected: isSelected,
337
+ current: opt.id === current,
338
+ width: rowWidth,
339
+ }),
340
+ selected: isSelected,
341
+ };
342
+ });
343
+ const modelRows = padPickerRows(rawModelRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
344
+ key: rawModelRows[index]?.key ?? `blank-${index}`,
345
+ row,
346
+ selected: rawModelRows[index]?.selected ?? false,
347
+ }));
348
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, paddingX: 1, borderStyle: "round", borderColor: theme.borderActive, children: [_jsx(Text, { bold: true, color: theme.accent, children: phase.kind === "effort" ? "Select Reasoning Effort" : "Select Model" }), phase.kind === "effort" ? (_jsx(EffortPickerView, { model: phase.model, selectedIndex: phase.selectedIndex, bodyRows: bodyRows, rowWidth: rowWidth })) : (_jsxs(_Fragment, { children: [_jsx(SearchField, { query: query, placeholder: "Type to search models...", width: rowWidth }), _jsx(Text, { color: theme.muted, children: "\u2191/\u2193 navigate \u00B7 Enter choose effort \u00B7 Esc cancel \u00B7 Backspace clear" })] })), phase.kind === "model" && _jsx(Box, { flexDirection: "column", height: bodyRows, overflow: "hidden", marginTop: 1, children: modelRows.map(({ key, row, selected }) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(Text, { color: selected ? theme.accent : (key === "no-results" ? theme.muted : undefined), bold: selected, children: row }) }, key))) })] }));
349
+ }
350
+ function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
351
+ const theme = useTheme();
352
+ const safeSelectedIndex = clampPickerIndex(selectedIndex, model.reasoningLevels.length);
353
+ const rawRows = model.reasoningLevels.map((level, index) => ({
354
+ key: level,
355
+ row: formatEffortPickerRow(level, {
356
+ selected: index === safeSelectedIndex,
357
+ width: rowWidth,
358
+ asToggle: isMiniMaxToggleModel(model.id),
359
+ }),
360
+ selected: index === safeSelectedIndex,
361
+ }));
362
+ const effortRows = padPickerRows(rawRows.map((row) => row.row), bodyRows, rowWidth).map((row, index) => ({
363
+ key: rawRows[index]?.key ?? `blank-${index}`,
364
+ row,
365
+ selected: rawRows[index]?.selected ?? false,
366
+ }));
367
+ const modelDetail = padVisual(truncateVisual(`${model.label} · ${model.providerBadge}`, rowWidth), rowWidth);
368
+ 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
369
  }
172
- function SearchField({ query, placeholder }) {
370
+ function SearchField({ query, placeholder, width }) {
173
371
  const theme = useTheme();
174
372
  const [cursorVisible, setCursorVisible] = useState(true);
175
373
  useEffect(() => {
176
374
  const t = setInterval(() => setCursorVisible((v) => !v), 500);
177
375
  return () => clearInterval(t);
178
376
  }, []);
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] })] }));
377
+ const contentBudget = width ? Math.max(1, width - 3) : undefined;
378
+ const visibleQuery = contentBudget ? truncateVisual(query, contentBudget) : query;
379
+ const visiblePlaceholder = !query
380
+ ? (contentBudget ? truncateVisual(` ${placeholder}`, contentBudget) : ` ${placeholder}`)
381
+ : "";
382
+ 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
383
  }
181
384
  export function buildLocalModelOptions(registry, current, recent) {
182
385
  const enabled = registry.getEnabled();
@@ -210,6 +413,7 @@ export function buildLocalModelOptions(registry, current, recent) {
210
413
  label: displayModel(current),
211
414
  group: "Current",
212
415
  providerBadge: provider?.name || providerId || "",
416
+ reasoningLevels: reasoningLevelsForModel(current),
213
417
  });
214
418
  }
215
419
  return opts;
@@ -231,12 +435,19 @@ function appendModelOption(options, seen, option) {
231
435
  if (seen.has(option.id))
232
436
  return;
233
437
  seen.add(option.id);
234
- options.push(option);
438
+ options.push({
439
+ ...option,
440
+ reasoningLevels: option.reasoningLevels ?? reasoningLevelsForModel(option.id),
441
+ });
235
442
  }
236
443
  function preferredModelIndex(options, current) {
237
444
  const idx = options.findIndex((option) => option.id === current);
238
445
  return idx >= 0 ? idx : 0;
239
446
  }
447
+ function reasoningLevelsForModel(model) {
448
+ const { providerId, modelId } = decodeModel(model);
449
+ return getAvailableThinkingLevels(providerId || "openai", modelId);
450
+ }
240
451
  export function ProviderPicker({ providers, current, onSelect, onCancel, title }) {
241
452
  const theme = useTheme();
242
453
  const { stdout } = useStdout();
@@ -247,6 +458,8 @@ export function ProviderPicker({ providers, current, onSelect, onCancel, title }
247
458
  return idx >= 0 ? idx : 0;
248
459
  });
249
460
  useInput((input, key) => {
461
+ if (isKeyReleaseEvent(key))
462
+ return;
250
463
  const action = resolvePickerKeyAction(input, key);
251
464
  if (action === "escape") {
252
465
  onCancel();
@@ -294,6 +507,8 @@ export function KeyPicker({ providerName, onSubmit, onCancel }) {
294
507
  const theme = useTheme();
295
508
  const [value, setValue] = useState("");
296
509
  useInput((input, key) => {
510
+ if (isKeyReleaseEvent(key))
511
+ return;
297
512
  const action = resolvePickerKeyAction(input, key);
298
513
  if (action === "escape") {
299
514
  onCancel();
@@ -338,6 +553,8 @@ export function SkillPicker({ skills, onSelect, onCancel }) {
338
553
  return skills.filter((skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q));
339
554
  }, [query, skills]);
340
555
  useInput((input, key) => {
556
+ if (isKeyReleaseEvent(key))
557
+ return;
341
558
  const action = resolvePickerKeyAction(input, key);
342
559
  if (action === "escape") {
343
560
  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);
@@ -13,17 +15,27 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
13
15
  const canUseCustom = question?.custom !== false;
14
16
  const isMultiple = question?.multiple === true;
15
17
  const totalTabs = request.questions.length;
18
+ // The "Custom: type to answer" row is the last navigable item (when custom is
19
+ // allowed), so Up/Down can reach and highlight it just like an option.
20
+ const customIndex = canUseCustom ? options.length : -1;
21
+ const navCount = options.length + (canUseCustom ? 1 : 0);
22
+ const isCustomSelected = canUseCustom && selected === customIndex;
16
23
  const currentAnswer = useMemo(() => answers[index] ?? [], [answers, index]);
17
24
  const commitQuestion = () => {
18
- const option = options[selected]?.label;
19
25
  const customAnswer = custom.trim();
20
- const nextAnswer = customAnswer
21
- ? [customAnswer]
26
+ // Submit what is actually selected: the Custom row submits the typed text;
27
+ // an option row submits that option (a stale custom buffer no longer wins).
28
+ const nextAnswer = isCustomSelected
29
+ ? customAnswer
30
+ ? [customAnswer]
31
+ : []
22
32
  : isMultiple
23
33
  ? currentAnswer
24
- : option
25
- ? [option]
26
- : [];
34
+ : options[selected]?.label
35
+ ? [options[selected].label]
36
+ : customAnswer
37
+ ? [customAnswer]
38
+ : [];
27
39
  const nextAnswers = answers.map((answer, i) => i === index ? nextAnswer : answer);
28
40
  if (index < request.questions.length - 1) {
29
41
  setAnswers(nextAnswers);
@@ -51,6 +63,14 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
51
63
  }));
52
64
  };
53
65
  useInput((input, key) => {
66
+ if (isKeyReleaseEvent(key))
67
+ return;
68
+ const strippedMouseInput = stripTerminalMouseSequences(input);
69
+ if (strippedMouseInput !== input) {
70
+ if (!strippedMouseInput)
71
+ return;
72
+ input = strippedMouseInput;
73
+ }
54
74
  if (key.escape) {
55
75
  onCancel();
56
76
  return;
@@ -72,11 +92,24 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
72
92
  return;
73
93
  }
74
94
  if (key.downArrow) {
75
- setSelected((i) => Math.min(Math.max(0, options.length - 1), i + 1));
95
+ setSelected((i) => Math.min(Math.max(0, navCount - 1), i + 1));
96
+ return;
97
+ }
98
+ // Tab toggles a checkbox; only meaningful while an option row is selected.
99
+ if (key.tab) {
100
+ if (!isCustomSelected)
101
+ toggleCurrentOption();
76
102
  return;
77
103
  }
78
- if (key.tab || input === " ") {
79
- toggleCurrentOption();
104
+ if (input === " ") {
105
+ // Space toggles the highlighted option, but on the Custom row it types a
106
+ // literal space into the answer instead of swallowing the keystroke.
107
+ if (isCustomSelected) {
108
+ setCustom((value) => value + " ");
109
+ }
110
+ else {
111
+ toggleCurrentOption();
112
+ }
80
113
  return;
81
114
  }
82
115
  if (key.return) {
@@ -87,7 +120,10 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
87
120
  setCustom((value) => value.slice(0, -1));
88
121
  return;
89
122
  }
123
+ // Any printable key starts/continues the custom answer and moves the
124
+ // highlight onto the Custom row, so typing and arrow navigation agree.
90
125
  if (canUseCustom && input && !key.ctrl && !key.meta) {
126
+ setSelected(customIndex);
91
127
  setCustom((value) => value + input);
92
128
  }
93
129
  });
@@ -95,5 +131,5 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
95
131
  const isSelected = optionIndex === selected;
96
132
  const isChecked = currentAnswer.includes(option.label);
97
133
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: isSelected ? theme.accent : undefined, children: [isSelected ? "> " : " ", isMultiple ? `[${isChecked ? "x" : " "}] ` : "", option.label] }), option.description && (_jsx(Box, { marginLeft: 4, children: _jsx(Text, { color: theme.muted, dimColor: true, children: option.description }) }))] }, `${option.label}-${optionIndex}`));
98
- }) }), canUseCustom && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: custom ? undefined : theme.muted, children: ["Custom: ", custom || "type to answer..."] }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: "\u2191\u2193 choose \u00B7 Tab/Space toggle \u00B7 Enter submit \u00B7 Esc dismiss" }) })] }));
134
+ }) }), canUseCustom && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: isCustomSelected ? theme.accent : custom ? undefined : theme.muted, children: [isCustomSelected ? "> " : " ", "Custom: ", custom || "type to answer"] }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: theme.muted, children: ["\u2191\u2193 choose \u00B7 ", isMultiple ? "Space toggle · " : "", "type for Custom \u00B7 Enter submit \u00B7 Esc dismiss"] }) })] }));
99
135
  }
@@ -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,9 +24,14 @@ 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;
28
- /** Accepted for compatibility with the shared options bag; the goal feature is OpenTUI-only. */
34
+ /** Shared with the model-facing goal tools and the Ink auto-continuation loop. */
29
35
  goalStore?: import("../goal/store.js").GoalStore;
30
36
  themeMode?: ThemeMode;
31
37
  themeOverrides?: Record<string, string>;
@@ -38,7 +44,10 @@ export interface RunTuiOptions {
38
44
  bypassEnabled?: boolean;
39
45
  /** One-line "update available" notice rendered under the welcome banner version. */
40
46
  updateNotice?: string;
47
+ /** Late update notice refresh surfaced after startup without restarting Ink. */
48
+ updateNoticeRefresh?: Promise<string | null>;
41
49
  /** External lifecycle hooks, threaded into slash-command execution. */
42
50
  hookController?: ExternalHookController;
43
51
  }
52
+ export declare function createInkAppElement(agent: Agent, args: CliArgs, options: RunTuiOptions, onExit: (summary: ExitSummary) => void): React.ReactElement;
44
53
  export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<ExitSummary | undefined>;
@@ -1,26 +1,23 @@
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";
11
4
  import { warmHighlighter } from "./code-highlight.js";
5
+ export function createInkAppElement(agent, args, options, onExit) {
6
+ 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 }));
7
+ }
12
8
  /**
13
- * Best-effort terminal restore for abnormal exits. DECSET mouse modes are
14
- * global terminal state if the process dies without disabling them, the
15
- * user's shell receives \x1b[<35;… garbage on every mouse move. The alt-screen
16
- * and cursor writes are defensive duplicates of Ink's own teardown (idempotent
17
- * when Ink already ran; load-bearing when it didn't).
9
+ * Best-effort terminal restore for abnormal exits. Bubble renders into the
10
+ * primary screen (no alt-screen, no mouse reporting) so the transcript flows
11
+ * into the terminal's native scrollback there is no global mouse/alt-screen
12
+ * state to undo. We only make sure the cursor is visible again, mirroring
13
+ * Ink's own teardown (idempotent when Ink already ran; load-bearing when it
14
+ * didn't).
18
15
  */
19
16
  function restoreTerminal() {
20
17
  if (!process.stdout.isTTY)
21
18
  return;
22
19
  try {
23
- process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_DISABLE + "\x1b[?1049l\x1b[?25h");
20
+ process.stdout.write("\x1b[?25h");
24
21
  }
25
22
  catch {
26
23
  // stdout may already be destroyed during shutdown
@@ -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,33 +62,21 @@ 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
- // The whole point of the Ink migration: render into the 1049 alternate
75
- // screen so streaming repaints never touch the user's shell scrollback.
76
- // Ink degrades this to false automatically when stdout is not a TTY.
77
- alternateScreen: true,
68
+ // Render into the primary screen (NOT the 1049 alternate screen): settled
69
+ // transcript rows are committed once via Ink's <Static> region so they
70
+ // flow into the terminal's native scrollback. That gives flicker-free
71
+ // native scroll + text selection + copy (and tmux copy-mode) for free,
72
+ // and frees the arrow keys for composer history. Only the streaming tail
73
+ // and the composer live in the repainting region at the bottom.
74
+ alternateScreen: false,
78
75
  });
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).
82
- if (process.stdout.isTTY) {
83
- process.stdout.write(ALTERNATE_SCROLL_ENABLE);
84
- }
85
76
  try {
86
77
  await instance.waitUntilExit();
87
78
  }
88
79
  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.
92
- if (process.stdout.isTTY) {
93
- process.stdout.write(ALTERNATE_SCROLL_DISABLE);
94
- }
95
80
  process.off("uncaughtException", onFatalError);
96
81
  process.off("SIGTERM", onSigterm);
97
82
  }