@bubblebrain-ai/bubble 0.0.25 → 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 +4 -2
- package/dist/agent.js +1 -1
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- 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 +150 -22
- package/dist/slash-commands/commands.js +22 -0
- package/dist/tools/todo.js +22 -38
- package/dist/tui-ink/app.js +80 -58
- package/dist/tui-ink/input-box.d.ts +1 -0
- package/dist/tui-ink/input-box.js +20 -16
- package/dist/tui-ink/message-list.d.ts +17 -1
- package/dist/tui-ink/message-list.js +74 -13
- package/dist/tui-ink/model-picker.d.ts +3 -2
- package/dist/tui-ink/model-picker.js +17 -4
- package/dist/tui-ink/question-dialog.js +36 -10
- package/dist/tui-ink/run.js +14 -22
- package/dist/tui-ink/terminal-mouse.d.ts +11 -0
- package/dist/tui-ink/terminal-mouse.js +13 -0
- package/dist/tui-ink/welcome.js +13 -3
- package/dist/variant/variant-resolver.js +4 -1
- package/package.json +1 -1
- package/dist/tui/transcript-scroll.d.ts +0 -25
- package/dist/tui/transcript-scroll.js +0 -20
- package/dist/tui-ink/transcript-input.d.ts +0 -8
- package/dist/tui-ink/transcript-input.js +0 -9
- package/dist/tui-ink/transcript-viewport-math.d.ts +0 -10
- package/dist/tui-ink/transcript-viewport-math.js +0 -16
- package/dist/tui-ink/transcript-viewport.d.ts +0 -24
- package/dist/tui-ink/transcript-viewport.js +0 -83
|
@@ -16,7 +16,6 @@ import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePa
|
|
|
16
16
|
import { imageDisplayLabel } from "../tui/image-display.js";
|
|
17
17
|
const MIN_VISIBLE_LINES = 3;
|
|
18
18
|
const MAX_VISIBLE_LINES = 6;
|
|
19
|
-
const CURSOR_BLINK_INTERVAL_MS = 530;
|
|
20
19
|
const PADDING_X = 1;
|
|
21
20
|
const PROMPT = " > ";
|
|
22
21
|
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
@@ -44,6 +43,9 @@ export function isCtrlCInput(input, key) {
|
|
|
44
43
|
export function shouldUseLineComposerFrame(_background) {
|
|
45
44
|
return true;
|
|
46
45
|
}
|
|
46
|
+
export function composerSurfaceBackground(lineFrame, background, inputBg) {
|
|
47
|
+
return lineFrame ? background : inputBg;
|
|
48
|
+
}
|
|
47
49
|
export function shouldUseHardwareComposerCursor(env = process.env) {
|
|
48
50
|
return env.BUBBLE_HARDWARE_CURSOR === "1";
|
|
49
51
|
}
|
|
@@ -903,23 +905,24 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorRes
|
|
|
903
905
|
const displayText = imageInlinePrefix + text;
|
|
904
906
|
const displayCursor = cursor + imageInlinePrefix.length;
|
|
905
907
|
const displayCursorToSourceCursor = (value) => Math.max(0, Math.min(text.length, value - imageInlinePrefix.length));
|
|
908
|
+
// Steady (non-blinking) cursor on purpose. The composer lives in the live
|
|
909
|
+
// (repainting) region; a blink timer would rewrite these rows ~twice a second
|
|
910
|
+
// even at idle, and the terminal drops any in-progress text selection every
|
|
911
|
+
// time the underlying cells are rewritten — which is why composer text could
|
|
912
|
+
// not be highlighted/copied while agent answers (committed to <Static>, never
|
|
913
|
+
// repainted) could. Keeping the cursor steady leaves the idle composer frame
|
|
914
|
+
// static, so native selection works. We still hide it while disabled.
|
|
906
915
|
useEffect(() => {
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
setSoftwareCursorVisible(true);
|
|
912
|
-
const timer = setInterval(() => {
|
|
913
|
-
setSoftwareCursorVisible((visible) => !visible);
|
|
914
|
-
}, CURSOR_BLINK_INTERVAL_MS);
|
|
915
|
-
return () => clearInterval(timer);
|
|
916
|
-
}, [disabled, displayCursor, displayText]);
|
|
916
|
+
setSoftwareCursorVisible(!disabled);
|
|
917
|
+
}, [disabled]);
|
|
917
918
|
const visualLines = useMemo(() => computeVisualLines(displayText, lineWidth), [displayText, lineWidth]);
|
|
918
919
|
const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, displayCursor);
|
|
919
|
-
// ----
|
|
920
|
+
// ---- Up/Down arrow handling in the composer ----
|
|
920
921
|
//
|
|
921
|
-
//
|
|
922
|
-
// multiline input first, then
|
|
922
|
+
// Scrolling is the terminal's job now (native scrollback), so the composer
|
|
923
|
+
// owns Up/Down unconditionally: move within multiline input first, then
|
|
924
|
+
// browse prompt history at the top edge (Up → previous sent message) or the
|
|
925
|
+
// bottom edge (Down → next message, then back to the in-progress draft).
|
|
923
926
|
const performVerticalArrowRef = useRef(() => { });
|
|
924
927
|
performVerticalArrowRef.current = (direction) => {
|
|
925
928
|
if (direction === "up") {
|
|
@@ -1086,6 +1089,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorRes
|
|
|
1086
1089
|
// Reference cursorTick so the effect re-runs on the forced render pass.
|
|
1087
1090
|
void cursorTick;
|
|
1088
1091
|
const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
|
|
1092
|
+
const composerBg = composerSurfaceBackground(lineFrame, theme.background, inputBg);
|
|
1089
1093
|
const rowBg = lineFrame ? undefined : inputBg;
|
|
1090
1094
|
const cursorFg = lineFrame ? theme.background : inputBg;
|
|
1091
1095
|
const cursorCellStyle = resolveSoftwareCursorCellStyle({
|
|
@@ -1100,7 +1104,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorRes
|
|
|
1100
1104
|
const visibleWidth = stringWidth(value);
|
|
1101
1105
|
return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
|
|
1102
1106
|
};
|
|
1103
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor:
|
|
1107
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: composerBg, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
|
|
1104
1108
|
if (row.kind === "pad") {
|
|
1105
1109
|
return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
|
|
1106
1110
|
}
|
|
@@ -1121,7 +1125,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorRes
|
|
|
1121
1125
|
});
|
|
1122
1126
|
const renderedLine = renderedSegments.map((segment) => segment.text).join("");
|
|
1123
1127
|
const fill = " ".repeat(Math.max(0, lineWidth - stringWidth(renderedLine)));
|
|
1124
|
-
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor:
|
|
1128
|
+
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: composerBg, ref: isCursorLine
|
|
1125
1129
|
? (el) => {
|
|
1126
1130
|
cursorLineRef.current = el;
|
|
1127
1131
|
}
|
|
@@ -25,6 +25,22 @@ interface MessageListProps {
|
|
|
25
25
|
nowTick?: number;
|
|
26
26
|
/** Optional banner rendered as the first item in the app-controlled transcript. */
|
|
27
27
|
welcomeBanner?: React.ReactNode;
|
|
28
|
+
/**
|
|
29
|
+
* Bumped whenever the settled transcript is rebuilt non-monotonically
|
|
30
|
+
* (/clear, /compact, /rewind, session switch). Used as the <Static> key so
|
|
31
|
+
* Ink discards its already-printed rows and re-prints the rebuilt list onto
|
|
32
|
+
* a freshly-cleared screen instead of appending duplicates.
|
|
33
|
+
*/
|
|
34
|
+
staticGeneration?: number;
|
|
35
|
+
/** Horizontal padding applied inside each committed/streaming row. */
|
|
36
|
+
paddingX?: number;
|
|
37
|
+
/**
|
|
38
|
+
* Maximum height (rows) for the live/dynamic region. The in-progress turn is
|
|
39
|
+
* clipped to this and pinned to the bottom (tail view) so the live frame
|
|
40
|
+
* never exceeds the viewport — a taller-than-pane frame breaks Ink's redraw
|
|
41
|
+
* under tmux. The full turn still lands in <Static> scrollback on commit.
|
|
42
|
+
*/
|
|
43
|
+
maxStreamRows?: number;
|
|
28
44
|
}
|
|
29
|
-
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, welcomeBanner, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
45
|
+
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration, paddingX, maxStreamRows, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
30
46
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { Box, Text } from "ink";
|
|
3
|
+
import { Box, Static, Text, measureElement } from "ink";
|
|
4
4
|
import { useTheme } from "./theme.js";
|
|
5
5
|
import { highlightCode, inferLang } from "./code-highlight.js";
|
|
6
6
|
import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
|
|
@@ -11,7 +11,7 @@ import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
|
11
11
|
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
12
12
|
import { splitImageDisplayContent } from "../tui/image-display.js";
|
|
13
13
|
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
14
|
-
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, expandedToolOutput = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
|
|
14
|
+
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, expandedToolOutput = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
|
|
15
15
|
const theme = useTheme();
|
|
16
16
|
const hasStreaming = !!(streamingContent ||
|
|
17
17
|
streamingReasoning ||
|
|
@@ -35,12 +35,63 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
35
35
|
separateFromPrevious: msg.role === "user" && regularMessages[i - 1]?.role === "user",
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const hasDynamic = hasStreaming || pendingSteerMessages.length > 0 || queuedInputMessages.length > 0;
|
|
39
|
+
// The live region must never grow taller than the viewport: a frame taller
|
|
40
|
+
// than the pane breaks Ink's in-place redraw under tmux (the cursor-up clear
|
|
41
|
+
// can't reach scrolled-off rows), leaving large blank gaps + stray glyphs.
|
|
42
|
+
// Clip it to maxStreamRows and pin to the bottom so the user sees the latest
|
|
43
|
+
// output (tail view); the full turn lands in <Static> scrollback on commit.
|
|
44
|
+
const clampDynamic = typeof maxStreamRows === "number" && maxStreamRows > 0;
|
|
45
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Static, { items: staticItems, children: (item) => {
|
|
46
|
+
if (item.kind === "welcome") {
|
|
47
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
|
|
48
|
+
}
|
|
49
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
|
|
50
|
+
} }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
|
|
54
|
+
* to the bottom so the user always sees the latest output (tail view). A live
|
|
55
|
+
* frame taller than the terminal pane breaks Ink's in-place redraw under tmux
|
|
56
|
+
* (the cursor-up clear can't reach rows that scrolled off), leaving large blank
|
|
57
|
+
* gaps and stray glyphs. We measure the natural content height and only clip
|
|
58
|
+
* when it actually exceeds `maxRows`, so short turns keep their natural height
|
|
59
|
+
* (no reserved-space gap). The full turn still lands in <Static> scrollback the
|
|
60
|
+
* moment it commits, so nothing is lost — only the live preview is windowed.
|
|
61
|
+
*/
|
|
62
|
+
function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
63
|
+
const innerRef = React.useRef(null);
|
|
64
|
+
const [offset, setOffset] = React.useState(0);
|
|
65
|
+
const [clipHeight, setClipHeight] = React.useState(undefined);
|
|
66
|
+
// Re-measure after every commit (streaming changes height with each token).
|
|
67
|
+
// useLayoutEffect runs before Ink flushes the frame to the terminal, so the
|
|
68
|
+
// clamp is applied in the same paint — avoiding a one-frame overflow flash
|
|
69
|
+
// under tmux when a turn first grows past the pane. setState bails out via
|
|
70
|
+
// Object.is when nothing moved, so the steady state does not loop.
|
|
71
|
+
React.useLayoutEffect(() => {
|
|
72
|
+
if (!maxRows || !innerRef.current) {
|
|
73
|
+
if (offset !== 0)
|
|
74
|
+
setOffset(0);
|
|
75
|
+
if (clipHeight !== undefined)
|
|
76
|
+
setClipHeight(undefined);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const height = measureElement(innerRef.current).height;
|
|
80
|
+
if (height > maxRows) {
|
|
81
|
+
const nextOffset = -(height - maxRows);
|
|
82
|
+
if (nextOffset !== offset)
|
|
83
|
+
setOffset(nextOffset);
|
|
84
|
+
if (clipHeight !== maxRows)
|
|
85
|
+
setClipHeight(maxRows);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (offset !== 0)
|
|
89
|
+
setOffset(0);
|
|
90
|
+
if (clipHeight !== undefined)
|
|
91
|
+
setClipHeight(undefined);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingX: paddingX, ...(clipHeight !== undefined ? { height: clipHeight, overflowY: "hidden" } : {}), children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: offset, children: children }) }));
|
|
44
95
|
}
|
|
45
96
|
// Memoized: with no <Static> region, every transcript row re-renders on each
|
|
46
97
|
// state change unless its props are referentially stable. Message objects are
|
|
@@ -62,13 +113,16 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
62
113
|
return (_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: theme.error, children: "\u23F9 " }), _jsx(Text, { color: theme.muted, dimColor: true, children: message.content || "Interrupted by user" })] }));
|
|
63
114
|
}
|
|
64
115
|
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
65
|
-
|
|
116
|
+
// Same defense as reasoning: strip any internal reminder markup the model
|
|
117
|
+
// echoed back into its visible answer so it never reaches the transcript.
|
|
118
|
+
const visibleContent = sanitizeInternalReminderBlocks(message.content ?? "");
|
|
119
|
+
const hasVisibleAssistantContent = !!visibleContent.trim() ||
|
|
66
120
|
(message.toolCalls?.length ?? 0) > 0 ||
|
|
67
121
|
(message.parts?.length ?? 0) > 0 ||
|
|
68
122
|
(!!visibleReasoning && (showThinking || verboseTrace));
|
|
69
123
|
if (!hasVisibleAssistantContent)
|
|
70
124
|
return null;
|
|
71
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })),
|
|
125
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
72
126
|
});
|
|
73
127
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, }) {
|
|
74
128
|
const deferredContent = React.useDeferredValue(content);
|
|
@@ -105,13 +159,19 @@ function findLastTextPartIndex(parts) {
|
|
|
105
159
|
}
|
|
106
160
|
function TimelineText({ content, compactTop, terminalColumns, streaming = false, }) {
|
|
107
161
|
const theme = useTheme();
|
|
108
|
-
|
|
162
|
+
// Strip any internal reminder/context markup the model echoed back into its
|
|
163
|
+
// visible text — the reasoning path already does this. Without it, a model
|
|
164
|
+
// that parrots a <bubble_internal_*> block (e.g. an injected system reminder)
|
|
165
|
+
// leaks it straight into the transcript. The streaming sanitizer also holds a
|
|
166
|
+
// half-typed block until it closes, so partial markup never flashes.
|
|
167
|
+
const visible = sanitizeInternalReminderBlocks(content);
|
|
168
|
+
if (!visible.trim())
|
|
109
169
|
return null;
|
|
110
170
|
// marginLeft (2) + "● " marker (3 visual cells) = 5 cells consumed by the
|
|
111
171
|
// timeline gutter; pass the remaining width so wide blocks like tables size
|
|
112
172
|
// themselves against the actual content area instead of the raw terminal.
|
|
113
173
|
const available = terminalColumns ? Math.max(20, terminalColumns - 5) : undefined;
|
|
114
|
-
const trimmed =
|
|
174
|
+
const trimmed = visible.trim();
|
|
115
175
|
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
|
|
116
176
|
}
|
|
117
177
|
function ToolsPart({ toolCalls, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
@@ -254,7 +314,8 @@ function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromP
|
|
|
254
314
|
const { bodyLines, referenceLines } = splitImageDisplayContent(content);
|
|
255
315
|
const wrappedLines = bodyLines
|
|
256
316
|
.flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
|
|
257
|
-
|
|
317
|
+
const attachmentReferenceIndent = " ".repeat(railWidth + 1);
|
|
318
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: separateFromPrevious ? 1 : 0, children: [badge && (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: inputStatus === "pending_steer" ? theme.warning : theme.muted, children: ` ${badge} ` }), _jsx(Text, { color: theme.dim, children: inputStatus === "pending_steer" ? "applies at the next model call" : "runs after this turn" })] })), wrappedLines.map((line, index) => (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userRail, children: "▌ " }), _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: ` ${padVisual(line || " ", bubbleTextWidth)} ` })] }, index))), referenceLines.map((line, index) => (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: `${attachmentReferenceIndent}${line}` }) }, `attachment-${index}`)))] }));
|
|
258
319
|
}
|
|
259
320
|
function PendingInputMessagesBlock({ messages, terminalColumns, title, hint, bulletColor, }) {
|
|
260
321
|
const theme = useTheme();
|
|
@@ -31,8 +31,8 @@ export declare function modelPickerBodyRows(termHeight: number): number;
|
|
|
31
31
|
export declare function clampPickerIndex(index: number, length: number): number;
|
|
32
32
|
export declare function pickerWindowStart(selectedIndex: number, length: number, visibleRows: number): number;
|
|
33
33
|
export declare function padPickerRows(rows: string[], bodyRows: number, width: number): string[];
|
|
34
|
-
export declare function formatReasoningLevelsLabel(levels: readonly ThinkingLevel[]): string;
|
|
35
|
-
export declare function formatModelPickerRow(option: Pick<ModelPickerOption, "label" | "providerBadge" | "reasoningLevels">, options: {
|
|
34
|
+
export declare function formatReasoningLevelsLabel(levels: readonly ThinkingLevel[], asToggle?: boolean): string;
|
|
35
|
+
export declare function formatModelPickerRow(option: Pick<ModelPickerOption, "id" | "label" | "providerBadge" | "reasoningLevels">, options: {
|
|
36
36
|
selected: boolean;
|
|
37
37
|
current: boolean;
|
|
38
38
|
width: number;
|
|
@@ -40,6 +40,7 @@ export declare function formatModelPickerRow(option: Pick<ModelPickerOption, "la
|
|
|
40
40
|
export declare function formatEffortPickerRow(level: ThinkingLevel, options: {
|
|
41
41
|
selected: boolean;
|
|
42
42
|
width: number;
|
|
43
|
+
asToggle?: boolean;
|
|
43
44
|
}): string;
|
|
44
45
|
export declare function formatNoModelResultsRow(query: string, width: number): string;
|
|
45
46
|
export declare function preferredEffortIndex(option: Pick<ModelPickerOption, "reasoningLevels">, currentThinkingLevel: ThinkingLevel): number;
|
|
@@ -78,8 +78,17 @@ export function padPickerRows(rows, bodyRows, width) {
|
|
|
78
78
|
}
|
|
79
79
|
return padded;
|
|
80
80
|
}
|
|
81
|
-
|
|
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) {
|
|
82
89
|
const normalized = levels.length > 0 ? levels : ["off"];
|
|
90
|
+
if (asToggle)
|
|
91
|
+
return "thinking on/off";
|
|
83
92
|
return `effort ${normalized.join("/")}`;
|
|
84
93
|
}
|
|
85
94
|
export function formatModelPickerRow(option, options) {
|
|
@@ -87,7 +96,7 @@ export function formatModelPickerRow(option, options) {
|
|
|
87
96
|
const marker = options.selected ? "> " : " ";
|
|
88
97
|
const label = option.label.replace(/\s+/g, " ").trim();
|
|
89
98
|
const provider = option.providerBadge.replace(/\s+/g, " ").trim();
|
|
90
|
-
const effort = formatReasoningLevelsLabel(option.reasoningLevels);
|
|
99
|
+
const effort = formatReasoningLevelsLabel(option.reasoningLevels, isMiniMaxToggleModel(option.id));
|
|
91
100
|
const current = options.current ? " ●" : "";
|
|
92
101
|
const providerWidth = Math.max(6, Math.min(16, Math.floor(width * 0.18)));
|
|
93
102
|
const effortWidth = Math.max(12, Math.min(30, Math.floor(width * 0.32)));
|
|
@@ -106,7 +115,8 @@ export function formatModelPickerRow(option, options) {
|
|
|
106
115
|
export function formatEffortPickerRow(level, options) {
|
|
107
116
|
const width = Math.max(24, options.width);
|
|
108
117
|
const marker = options.selected ? "> " : " ";
|
|
109
|
-
const
|
|
118
|
+
const name = options.asToggle ? (level === "off" ? "off" : "on") : level;
|
|
119
|
+
const row = `${marker}${name} ${effortDescription(level, options.asToggle)}`;
|
|
110
120
|
return padVisual(truncateVisual(row, width), width);
|
|
111
121
|
}
|
|
112
122
|
export function formatNoModelResultsRow(query, width) {
|
|
@@ -125,7 +135,9 @@ export function preferredEffortIndex(option, currentThinkingLevel) {
|
|
|
125
135
|
export function shouldOpenEffortPicker(option) {
|
|
126
136
|
return option.reasoningLevels.length > 1;
|
|
127
137
|
}
|
|
128
|
-
function effortDescription(level) {
|
|
138
|
+
function effortDescription(level, asToggle) {
|
|
139
|
+
if (asToggle)
|
|
140
|
+
return level === "off" ? "thinking disabled" : "thinking enabled";
|
|
129
141
|
switch (level) {
|
|
130
142
|
case "off":
|
|
131
143
|
return "no reasoning effort";
|
|
@@ -343,6 +355,7 @@ function EffortPickerView({ model, selectedIndex, bodyRows, rowWidth, }) {
|
|
|
343
355
|
row: formatEffortPickerRow(level, {
|
|
344
356
|
selected: index === safeSelectedIndex,
|
|
345
357
|
width: rowWidth,
|
|
358
|
+
asToggle: isMiniMaxToggleModel(model.id),
|
|
346
359
|
}),
|
|
347
360
|
selected: index === safeSelectedIndex,
|
|
348
361
|
}));
|
|
@@ -15,17 +15,27 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
|
15
15
|
const canUseCustom = question?.custom !== false;
|
|
16
16
|
const isMultiple = question?.multiple === true;
|
|
17
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;
|
|
18
23
|
const currentAnswer = useMemo(() => answers[index] ?? [], [answers, index]);
|
|
19
24
|
const commitQuestion = () => {
|
|
20
|
-
const option = options[selected]?.label;
|
|
21
25
|
const customAnswer = custom.trim();
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
: []
|
|
24
32
|
: isMultiple
|
|
25
33
|
? currentAnswer
|
|
26
|
-
:
|
|
27
|
-
? [
|
|
28
|
-
:
|
|
34
|
+
: options[selected]?.label
|
|
35
|
+
? [options[selected].label]
|
|
36
|
+
: customAnswer
|
|
37
|
+
? [customAnswer]
|
|
38
|
+
: [];
|
|
29
39
|
const nextAnswers = answers.map((answer, i) => i === index ? nextAnswer : answer);
|
|
30
40
|
if (index < request.questions.length - 1) {
|
|
31
41
|
setAnswers(nextAnswers);
|
|
@@ -82,11 +92,24 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
|
82
92
|
return;
|
|
83
93
|
}
|
|
84
94
|
if (key.downArrow) {
|
|
85
|
-
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();
|
|
86
102
|
return;
|
|
87
103
|
}
|
|
88
|
-
if (
|
|
89
|
-
|
|
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
|
+
}
|
|
90
113
|
return;
|
|
91
114
|
}
|
|
92
115
|
if (key.return) {
|
|
@@ -97,7 +120,10 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
|
97
120
|
setCustom((value) => value.slice(0, -1));
|
|
98
121
|
return;
|
|
99
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.
|
|
100
125
|
if (canUseCustom && input && !key.ctrl && !key.meta) {
|
|
126
|
+
setSelected(customIndex);
|
|
101
127
|
setCustom((value) => value + input);
|
|
102
128
|
}
|
|
103
129
|
});
|
|
@@ -105,5 +131,5 @@ export function QuestionDialog({ request, onSubmit, onCancel }) {
|
|
|
105
131
|
const isSelected = optionIndex === selected;
|
|
106
132
|
const isChecked = currentAnswer.includes(option.label);
|
|
107
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}`));
|
|
108
|
-
}) }), 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"] }) })] }));
|
|
109
135
|
}
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -1,23 +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 { ALTERNATE_SCROLL_DISABLE, MOUSE_REPORTING_DISABLE, MOUSE_REPORTING_ENABLE, } from "./terminal-mouse.js";
|
|
5
4
|
import { warmHighlighter } from "./code-highlight.js";
|
|
6
5
|
export function createInkAppElement(agent, args, options, onExit) {
|
|
7
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 }));
|
|
8
7
|
}
|
|
9
8
|
/**
|
|
10
|
-
* Best-effort terminal restore for abnormal exits.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 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).
|
|
15
15
|
*/
|
|
16
16
|
function restoreTerminal() {
|
|
17
17
|
if (!process.stdout.isTTY)
|
|
18
18
|
return;
|
|
19
19
|
try {
|
|
20
|
-
process.stdout.write(
|
|
20
|
+
process.stdout.write("\x1b[?25h");
|
|
21
21
|
}
|
|
22
22
|
catch {
|
|
23
23
|
// stdout may already be destroyed during shutdown
|
|
@@ -65,26 +65,18 @@ export async function runTui(agent, args, options = {}) {
|
|
|
65
65
|
// reportEventTypes keeps release events out of text input.
|
|
66
66
|
flags: ["disambiguateEscapeCodes", "reportEventTypes"],
|
|
67
67
|
},
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
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,
|
|
72
75
|
});
|
|
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.
|
|
76
|
-
if (process.stdout.isTTY) {
|
|
77
|
-
process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_ENABLE);
|
|
78
|
-
}
|
|
79
76
|
try {
|
|
80
77
|
await instance.waitUntilExit();
|
|
81
78
|
}
|
|
82
79
|
finally {
|
|
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.
|
|
85
|
-
if (process.stdout.isTTY) {
|
|
86
|
-
process.stdout.write(ALTERNATE_SCROLL_DISABLE + MOUSE_REPORTING_DISABLE);
|
|
87
|
-
}
|
|
88
80
|
process.off("uncaughtException", onFatalError);
|
|
89
81
|
process.off("SIGTERM", onSigterm);
|
|
90
82
|
}
|
|
@@ -2,6 +2,16 @@ export declare const ALTERNATE_SCROLL_ENABLE = "\u001B[?1007h";
|
|
|
2
2
|
export declare const ALTERNATE_SCROLL_DISABLE = "\u001B[?1007l";
|
|
3
3
|
export declare const MOUSE_REPORTING_ENABLE = "\u001B[?1000h\u001B[?1006h";
|
|
4
4
|
export declare const MOUSE_REPORTING_DISABLE = "\u001B[?1003l\u001B[?1002l\u001B[?1000l\u001B[?1005l\u001B[?1006l\u001B[?1015l";
|
|
5
|
+
interface TerminalMouseEnv {
|
|
6
|
+
BUBBLE_ENABLE_MOUSE?: string;
|
|
7
|
+
BUBBLE_TUI_MOUSE?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Terminal mouse reporting captures drag events before the host terminal can
|
|
11
|
+
* create a native text selection. Keep it opt-in until Bubble owns a full
|
|
12
|
+
* renderer-level selection/copy pipeline.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isTerminalMouseReportingEnabled(env?: TerminalMouseEnv): boolean;
|
|
5
15
|
export type MouseWheelDirection = "up" | "down";
|
|
6
16
|
export interface TerminalMouseInput {
|
|
7
17
|
strippedInput: string;
|
|
@@ -15,3 +25,4 @@ export declare function transcriptScrollLinesFromMouseInput(mouseInput: Terminal
|
|
|
15
25
|
export declare function stripTerminalMouseSequences(input: string): string;
|
|
16
26
|
export declare function hasTerminalMouseSequence(input: string): boolean;
|
|
17
27
|
export declare function parseTerminalMouseWheel(input: string): MouseWheelDirection[];
|
|
28
|
+
export {};
|
|
@@ -10,6 +10,19 @@ export const MOUSE_REPORTING_ENABLE = "\x1b[?1000h\x1b[?1006h";
|
|
|
10
10
|
// Disable every common tracking mode defensively in case a crash or another
|
|
11
11
|
// renderer left the terminal in a reporting state.
|
|
12
12
|
export const MOUSE_REPORTING_DISABLE = "\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1005l\x1b[?1006l\x1b[?1015l";
|
|
13
|
+
function envValueEnabled(value) {
|
|
14
|
+
if (!value)
|
|
15
|
+
return false;
|
|
16
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Terminal mouse reporting captures drag events before the host terminal can
|
|
20
|
+
* create a native text selection. Keep it opt-in until Bubble owns a full
|
|
21
|
+
* renderer-level selection/copy pipeline.
|
|
22
|
+
*/
|
|
23
|
+
export function isTerminalMouseReportingEnabled(env = process.env) {
|
|
24
|
+
return envValueEnabled(env.BUBBLE_ENABLE_MOUSE) || envValueEnabled(env.BUBBLE_TUI_MOUSE);
|
|
25
|
+
}
|
|
13
26
|
const ESCAPED_MOUSE_SEQUENCE_RE = /\x1b(?:\[<(\d+);\d+;\d+([mM])|\[M([\s\S])[\s\S]{2})/g;
|
|
14
27
|
const RAW_SGR_MOUSE_SEQUENCE_RE = /\[?<(\d+);\d+;\d+([mM])/g;
|
|
15
28
|
const RAW_SGR_MOUSE_INPUT_RE = /^(?:\[?<\d+;\d+;\d+[mM])+$/;
|
package/dist/tui-ink/welcome.js
CHANGED
|
@@ -31,12 +31,22 @@ export function WelcomeBanner({ terminalColumns, tips, updateNotice, cwd, provid
|
|
|
31
31
|
}
|
|
32
32
|
export function formatModelLine({ providerId, modelLabel, thinkingLabel, tips, }) {
|
|
33
33
|
const parts = [];
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// MiniMax thinking is a binary toggle (adaptive thinking), so label it
|
|
35
|
+
// "thinking mode" rather than "<level> effort"; and its provider id
|
|
36
|
+
// ("minimax-anthropic") is redundant with the model name, so omit it.
|
|
37
|
+
const isMiniMax = (providerId || "").toLowerCase().includes("minimax");
|
|
38
|
+
if (modelLabel) {
|
|
39
|
+
if (thinkingLabel && isMiniMax)
|
|
40
|
+
parts.push(modelLabel, "thinking mode");
|
|
41
|
+
else if (thinkingLabel)
|
|
42
|
+
parts.push(`${modelLabel} with ${thinkingLabel} effort`);
|
|
43
|
+
else
|
|
44
|
+
parts.push(modelLabel);
|
|
45
|
+
}
|
|
36
46
|
const readyTip = tips.find((item) => item.startsWith("Ready with"));
|
|
37
47
|
if (!modelLabel && readyTip)
|
|
38
48
|
parts.push(readyTip.replace(/^Ready with\s+/, ""));
|
|
39
|
-
if (providerId)
|
|
49
|
+
if (providerId && !isMiniMax)
|
|
40
50
|
parts.push(providerId);
|
|
41
51
|
return parts.join(" · ");
|
|
42
52
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { getBuiltinModel } from "../model-catalog.js";
|
|
1
|
+
import { getBuiltinModel, getModelDefaultReasoningLevel } from "../model-catalog.js";
|
|
2
2
|
import { clampThinkingLevel } from "./thinking-level.js";
|
|
3
3
|
export function getAvailableThinkingLevels(providerId, modelId) {
|
|
4
4
|
return getBuiltinModel(providerId, modelId)?.reasoningLevels ?? ["off"];
|
|
5
5
|
}
|
|
6
6
|
export function getDefaultThinkingLevel(providerId, modelId) {
|
|
7
7
|
const levels = getAvailableThinkingLevels(providerId, modelId);
|
|
8
|
+
const explicitDefault = getModelDefaultReasoningLevel(providerId, modelId);
|
|
9
|
+
if (explicitDefault && levels.includes(explicitDefault))
|
|
10
|
+
return explicitDefault;
|
|
8
11
|
return levels.includes("medium") ? "medium" : levels[0] || "off";
|
|
9
12
|
}
|
|
10
13
|
export function normalizeThinkingLevel(level, supportedLevels) {
|
package/package.json
CHANGED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcript scroll-follow policy.
|
|
3
|
-
*
|
|
4
|
-
* The transcript snaps to the bottom ("follows") while the user is at the
|
|
5
|
-
* bottom, and stays put while they read older history. Two events override a
|
|
6
|
-
* scrolled-up position and re-engage following:
|
|
7
|
-
* - the user sends a message (explicit intent to watch the newest turn)
|
|
8
|
-
* - an approval prompt appears (requires the user's attention)
|
|
9
|
-
*
|
|
10
|
-
* Those renders set `forcePending`, which must survive until the deferred
|
|
11
|
-
* scroll actually runs: streaming redraws in the interim recompute the follow
|
|
12
|
-
* flag from the (still unscrolled) position and would otherwise cancel the
|
|
13
|
-
* snap. A user mouse scroll clears the pending force — their latest gesture
|
|
14
|
-
* always wins.
|
|
15
|
-
*/
|
|
16
|
-
export interface TranscriptScrollState {
|
|
17
|
-
/** A forceFollow render is waiting for its deferred scroll to execute. */
|
|
18
|
-
forcePending: boolean;
|
|
19
|
-
/** The viewport was at the bottom when the update was scheduled. */
|
|
20
|
-
shouldFollow: boolean;
|
|
21
|
-
/** Live follow flag, recomputed from the viewport position on each render. */
|
|
22
|
-
following: boolean;
|
|
23
|
-
}
|
|
24
|
-
export type TranscriptScrollAction = "scroll-bottom" | "sync-position";
|
|
25
|
-
export declare function resolveTranscriptScroll(state: TranscriptScrollState): TranscriptScrollAction;
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transcript scroll-follow policy.
|
|
3
|
-
*
|
|
4
|
-
* The transcript snaps to the bottom ("follows") while the user is at the
|
|
5
|
-
* bottom, and stays put while they read older history. Two events override a
|
|
6
|
-
* scrolled-up position and re-engage following:
|
|
7
|
-
* - the user sends a message (explicit intent to watch the newest turn)
|
|
8
|
-
* - an approval prompt appears (requires the user's attention)
|
|
9
|
-
*
|
|
10
|
-
* Those renders set `forcePending`, which must survive until the deferred
|
|
11
|
-
* scroll actually runs: streaming redraws in the interim recompute the follow
|
|
12
|
-
* flag from the (still unscrolled) position and would otherwise cancel the
|
|
13
|
-
* snap. A user mouse scroll clears the pending force — their latest gesture
|
|
14
|
-
* always wins.
|
|
15
|
-
*/
|
|
16
|
-
export function resolveTranscriptScroll(state) {
|
|
17
|
-
if (state.forcePending)
|
|
18
|
-
return "scroll-bottom";
|
|
19
|
-
return state.shouldFollow && state.following ? "scroll-bottom" : "sync-position";
|
|
20
|
-
}
|