@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.
- package/README.md +5 -3
- package/dist/agent.js +1 -1
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +22 -6
- package/dist/goal/format.js +34 -4
- package/dist/goal/store.d.ts +3 -0
- package/dist/goal/store.js +14 -1
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/main.js +23 -42
- package/dist/model-catalog.d.ts +3 -1
- package/dist/model-catalog.js +17 -28
- package/dist/prompt/compose.js +1 -1
- package/dist/provider-anthropic.d.ts +4 -0
- package/dist/provider-anthropic.js +31 -0
- package/dist/provider-ark-responses.d.ts +17 -0
- package/dist/provider-ark-responses.js +462 -0
- package/dist/provider-transform.js +7 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +170 -27
- package/dist/slash-commands/commands.js +22 -0
- package/dist/tools/todo.js +22 -38
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +23 -0
- package/dist/tui/image-display.d.ts +13 -0
- package/dist/tui/image-display.js +49 -0
- package/dist/tui/input-history.d.ts +37 -6
- package/dist/tui/input-history.js +194 -23
- package/dist/tui/model-switch.d.ts +42 -0
- package/dist/tui/model-switch.js +55 -0
- package/dist/tui-ink/app.d.ts +32 -2
- package/dist/tui-ink/app.js +1409 -549
- package/dist/tui-ink/approval/select.js +10 -0
- package/dist/tui-ink/detect-theme.d.ts +1 -2
- package/dist/tui-ink/detect-theme.js +1 -87
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +11 -0
- package/dist/tui-ink/feedback-dialog.js +10 -0
- package/dist/tui-ink/feishu-setup-picker.js +10 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +8 -2
- package/dist/tui-ink/input-box.d.ts +71 -9
- package/dist/tui-ink/input-box.js +359 -121
- package/dist/tui-ink/input-history.d.ts +1 -16
- package/dist/tui-ink/input-history.js +1 -79
- package/dist/tui-ink/input-queue.d.ts +12 -0
- package/dist/tui-ink/input-queue.js +17 -0
- package/dist/tui-ink/key-events.d.ts +9 -0
- package/dist/tui-ink/key-events.js +8 -0
- package/dist/tui-ink/markdown.js +1 -1
- package/dist/tui-ink/message-list.d.ts +19 -1
- package/dist/tui-ink/message-list.js +111 -32
- package/dist/tui-ink/model-picker.d.ts +25 -2
- package/dist/tui-ink/model-picker.js +237 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +46 -10
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +27 -42
- package/dist/tui-ink/session-picker.js +3 -0
- package/dist/tui-ink/submit-dedupe.d.ts +5 -0
- package/dist/tui-ink/submit-dedupe.js +25 -0
- package/dist/tui-ink/terminal-mouse.d.ts +24 -1
- package/dist/tui-ink/terminal-mouse.js +76 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +34 -27
- package/dist/variant/variant-resolver.js +4 -1
- package/package.json +1 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -22
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -67
- package/dist/tui/run.js +0 -10166
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -135
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/types.js +0 -1
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -32
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- package/dist/tui/transcript-scroll.d.ts +0 -25
- package/dist/tui/transcript-scroll.js +0 -20
- package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
- package/dist/tui-ink/transcript-viewport-math.js +0 -17
- package/dist/tui-ink/transcript-viewport.d.ts +0 -24
- package/dist/tui-ink/transcript-viewport.js +0 -83
- package/dist/tui-opentui/app.d.ts +0 -54
- package/dist/tui-opentui/app.js +0 -1371
- package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
- package/dist/tui-opentui/approval/approval-dialog.js +0 -155
- package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
- package/dist/tui-opentui/approval/diff-view.js +0 -43
- package/dist/tui-opentui/approval/select.d.ts +0 -37
- package/dist/tui-opentui/approval/select.js +0 -91
- package/dist/tui-opentui/detect-theme.d.ts +0 -2
- package/dist/tui-opentui/detect-theme.js +0 -87
- package/dist/tui-opentui/display-history.d.ts +0 -56
- package/dist/tui-opentui/display-history.js +0 -130
- package/dist/tui-opentui/edit-diff.d.ts +0 -11
- package/dist/tui-opentui/edit-diff.js +0 -57
- package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
- package/dist/tui-opentui/feedback-dialog.js +0 -164
- package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
- package/dist/tui-opentui/feishu-setup-picker.js +0 -272
- package/dist/tui-opentui/file-mentions.d.ts +0 -29
- package/dist/tui-opentui/file-mentions.js +0 -174
- package/dist/tui-opentui/footer.d.ts +0 -26
- package/dist/tui-opentui/footer.js +0 -40
- package/dist/tui-opentui/image-paste.d.ts +0 -54
- package/dist/tui-opentui/image-paste.js +0 -288
- package/dist/tui-opentui/input-box.d.ts +0 -32
- package/dist/tui-opentui/input-box.js +0 -462
- package/dist/tui-opentui/input-history.d.ts +0 -16
- package/dist/tui-opentui/input-history.js +0 -79
- package/dist/tui-opentui/markdown.d.ts +0 -66
- package/dist/tui-opentui/markdown.js +0 -127
- package/dist/tui-opentui/message-list.d.ts +0 -31
- package/dist/tui-opentui/message-list.js +0 -131
- package/dist/tui-opentui/model-picker.d.ts +0 -63
- package/dist/tui-opentui/model-picker.js +0 -450
- package/dist/tui-opentui/plan-confirm.d.ts +0 -9
- package/dist/tui-opentui/plan-confirm.js +0 -124
- package/dist/tui-opentui/question-dialog.d.ts +0 -10
- package/dist/tui-opentui/question-dialog.js +0 -110
- package/dist/tui-opentui/recent-activity.d.ts +0 -8
- package/dist/tui-opentui/recent-activity.js +0 -71
- package/dist/tui-opentui/run-session-picker.d.ts +0 -10
- package/dist/tui-opentui/run-session-picker.js +0 -28
- package/dist/tui-opentui/run.d.ts +0 -38
- package/dist/tui-opentui/run.js +0 -48
- package/dist/tui-opentui/session-picker.d.ts +0 -12
- package/dist/tui-opentui/session-picker.js +0 -120
- package/dist/tui-opentui/theme.d.ts +0 -89
- package/dist/tui-opentui/theme.js +0 -157
- package/dist/tui-opentui/todos.d.ts +0 -9
- package/dist/tui-opentui/todos.js +0 -45
- package/dist/tui-opentui/trace-groups.d.ts +0 -27
- package/dist/tui-opentui/trace-groups.js +0 -455
- package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
- package/dist/tui-opentui/use-terminal-size.js +0 -5
- package/dist/tui-opentui/welcome.d.ts +0 -25
- 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
|
|
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({
|
|
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)
|
|
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
|
-
|
|
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) =>
|
|
297
|
+
setSelectedIndex((i) => clampPickerIndex(i - 1, options.length));
|
|
141
298
|
return;
|
|
142
299
|
}
|
|
143
300
|
if (action === "down") {
|
|
144
|
-
setSelectedIndex((i) =>
|
|
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
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
:
|
|
25
|
-
? [
|
|
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,
|
|
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 (
|
|
79
|
-
|
|
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
|
|
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
|
}
|
package/dist/tui-ink/run.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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>;
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -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.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* when Ink already ran; load-bearing when it
|
|
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(
|
|
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(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
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
|
}
|