@bubblebrain-ai/bubble 0.0.46 → 0.0.48
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/dist/agent/child-runner.js +11 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +13 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +198 -0
- package/dist/agent/subagent/runtime.d.ts +203 -0
- package/dist/agent/subagent/runtime.js +789 -0
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-store.d.ts +18 -2
- package/dist/agent/subagent-store.js +32 -1
- package/dist/agent/tool-intent.d.ts +10 -0
- package/dist/agent/tool-intent.js +18 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/workflow/runs.d.ts +72 -0
- package/dist/agent/workflow/runs.js +157 -0
- package/dist/agent.d.ts +78 -96
- package/dist/agent.js +266 -1072
- package/dist/context/compact-llm.js +14 -5
- package/dist/context/compact.d.ts +9 -0
- package/dist/context/compact.js +36 -10
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.js +24 -5
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +13 -9
- package/dist/goal/tools.js +12 -3
- package/dist/main.js +24 -19
- package/dist/model-catalog.js +36 -2
- package/dist/model-config.d.ts +2 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +11 -7
- package/dist/model-pricing.js +0 -0
- package/dist/oauth/grok.d.ts +1 -1
- package/dist/oauth/grok.js +5 -2
- package/dist/oauth/storage.d.ts +9 -2
- package/dist/oauth/storage.js +16 -9
- package/dist/orchestrator/default-hooks.d.ts +20 -0
- package/dist/orchestrator/default-hooks.js +47 -295
- package/dist/orchestrator/hooks.d.ts +6 -39
- package/dist/permission/mode.js +1 -1
- package/dist/prompt/reminders.d.ts +9 -35
- package/dist/prompt/reminders.js +30 -136
- package/dist/prompt/task-reminders.d.ts +0 -31
- package/dist/prompt/task-reminders.js +0 -87
- package/dist/provider-ai-sdk.d.ts +2 -0
- package/dist/provider-ai-sdk.js +1 -0
- package/dist/provider-anthropic.d.ts +2 -0
- package/dist/provider-anthropic.js +3 -1
- package/dist/provider-ark-responses.d.ts +2 -0
- package/dist/provider-ark-responses.js +1 -0
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +39 -0
- package/dist/provider-registry.d.ts +14 -1
- package/dist/provider-registry.js +131 -3
- package/dist/provider-transform.js +37 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +13 -1
- package/dist/sdk/index.d.ts +152 -0
- package/dist/sdk/index.js +400 -0
- package/dist/session.js +9 -1
- package/dist/slash-commands/commands.js +33 -19
- package/dist/tools/exit-plan-mode.js +8 -0
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +11 -5
- package/dist/tui-ink/app.d.ts +2 -6
- package/dist/tui-ink/app.js +11 -18
- package/dist/tui-ink/display-history.d.ts +0 -2
- package/dist/tui-ink/display-history.js +0 -52
- package/dist/tui-ink/footer.js +2 -2
- package/dist/tui-ink/input-box.js +18 -6
- package/dist/tui-ink/message-list.js +62 -23
- package/dist/tui-ink/run.js +10 -5
- package/dist/tui-ink/sync-output.d.ts +17 -0
- package/dist/tui-ink/sync-output.js +35 -0
- package/dist/tui-ink/theme.d.ts +0 -1
- package/dist/tui-ink/theme.js +0 -2
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/types.d.ts +21 -1
- package/package.json +16 -2
- package/dist/agent/discovery-barrier.d.ts +0 -21
- package/dist/agent/discovery-barrier.js +0 -173
- package/dist/agent/evidence-tracker.d.ts +0 -15
- package/dist/agent/evidence-tracker.js +0 -93
- package/dist/agent/execution-governor.d.ts +0 -36
- package/dist/agent/execution-governor.js +0 -218
- package/dist/agent/task-classifier.d.ts +0 -3
- package/dist/agent/task-classifier.js +0 -114
- package/dist/agent/task-size.d.ts +0 -9
- package/dist/agent/task-size.js +0 -33
- package/dist/agent/tool-arbiter.d.ts +0 -7
- package/dist/agent/tool-arbiter.js +0 -33
- package/dist/orchestrator/workflow.d.ts +0 -10
- package/dist/orchestrator/workflow.js +0 -22
package/dist/tui-ink/app.js
CHANGED
|
@@ -2,14 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useApp, useInput } from "ink";
|
|
4
4
|
import { AgentAbortError, INTERRUPTED_ASSISTANT_CONTENT } from "../agent.js";
|
|
5
|
-
import { isHiddenToolMetadata } from "../agent/
|
|
5
|
+
import { isHiddenToolMetadata } from "../agent/tool-visibility.js";
|
|
6
6
|
import { SessionManager } from "../session.js";
|
|
7
7
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
8
8
|
import { UserConfig, maskKey } from "../config.js";
|
|
9
9
|
import { InputBox, isCtrlCInput, isCtrlLetterInput, } from "./input-box.js";
|
|
10
10
|
import { MessageList } from "./message-list.js";
|
|
11
11
|
import { isMultiplexedTerminal } from "./terminal-env.js";
|
|
12
|
-
import { appendTextPart, appendToolPart,
|
|
12
|
+
import { appendTextPart, appendToolPart, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
|
|
13
13
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
14
14
|
import { canvasBackgroundFor, paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
15
15
|
import { isPrintablePickerInput, ModelPicker, ProviderPicker, KeyPicker, SkillPicker } from "./model-picker.js";
|
|
@@ -45,7 +45,7 @@ import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
|
|
|
45
45
|
import { shouldContinueGoal, stopReasonNotice } from "../goal/engine.js";
|
|
46
46
|
import { goalCompleteNotice, goalIndicatorLine, goalSummaryText } from "../goal/format.js";
|
|
47
47
|
import { tokenUsageTotal } from "../goal/usage.js";
|
|
48
|
-
import { formatInternalContextBlock } from "../agent/internal-reminder-sanitizer.js";
|
|
48
|
+
import { formatInternalContextBlock, isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
|
|
49
49
|
import { MAX_ACTIVE_LOOPS, decideLoopFiring, formatInterval, formatLoopList, parseLoopCommand, } from "../loop/engine.js";
|
|
50
50
|
import { TaskWakeCoalescer, findDanglingTaskStarts, formatTaskWakeSummary, isPidAlive, shouldFireTaskWake, taskEligibleForWake, } from "../tasks/wake.js";
|
|
51
51
|
const TASK_WAKE_DEBOUNCE_MS = 2000;
|
|
@@ -87,17 +87,9 @@ function buildTips(agent, registry, grokActive = false) {
|
|
|
87
87
|
function slashResultNoticeKind(result) {
|
|
88
88
|
return /^(Grok m|M)odel switched to /.test(result) ? "ui_notice" : undefined;
|
|
89
89
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*/
|
|
94
|
-
export function isInternalBlockOnlyContent(content) {
|
|
95
|
-
if (typeof content !== "string")
|
|
96
|
-
return false;
|
|
97
|
-
const trimmed = content.trim();
|
|
98
|
-
return /^<bubble_internal_(?:context|reminder)\b/.test(trimmed)
|
|
99
|
-
&& /<\/bubble_internal_(?:context|reminder)>$/.test(trimmed);
|
|
100
|
-
}
|
|
90
|
+
// Canonical implementation lives in the sanitizer module so non-TUI surfaces
|
|
91
|
+
// (session pickers, desktop host) share it; re-exported here for existing users.
|
|
92
|
+
export { isInternalBlockOnlyContent };
|
|
101
93
|
/** Status-row summary for running background tasks (design §2.5). */
|
|
102
94
|
function taskRowSummary(tasks, nowTick) {
|
|
103
95
|
const first = tasks[0];
|
|
@@ -298,7 +290,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
298
290
|
const themePickerRevertRef = useRef("auto");
|
|
299
291
|
const themeResolved = themeMode === "auto" ? autoResolved : themeMode;
|
|
300
292
|
const { exit } = useApp();
|
|
301
|
-
const [messages, setMessages] = useState(() =>
|
|
293
|
+
const [messages, setMessages] = useState(() => reconstructDisplayMessages(agent.messages));
|
|
302
294
|
const nextImageDisplayLabelStartRef = useRef(nextImageDisplayLabelStart(messages));
|
|
303
295
|
const [isRunning, setIsRunning] = useState(false);
|
|
304
296
|
const [streamingContent, setStreamingContent] = useState("");
|
|
@@ -733,9 +725,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
733
725
|
// <system-reminder>, so we do not rebuild the cache-friendly system prompt here.
|
|
734
726
|
if (key.tab && key.shift && !pickerMode) {
|
|
735
727
|
const nextMode = getNextPermissionMode(agent.mode);
|
|
728
|
+
// setMode fires onModeUpdate, which is the single writer of the
|
|
729
|
+
// mode_switch marker — appending here too double-logs every switch.
|
|
736
730
|
agent.setMode(nextMode);
|
|
737
731
|
setPermissionMode(nextMode);
|
|
738
|
-
sessionManager?.appendMarker("mode_switch", nextMode);
|
|
739
732
|
return;
|
|
740
733
|
}
|
|
741
734
|
if (key.escape && !pickerMode) {
|
|
@@ -746,7 +739,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
746
739
|
}
|
|
747
740
|
});
|
|
748
741
|
const updateDisplayMessages = useCallback((updater) => {
|
|
749
|
-
setMessages((prev) =>
|
|
742
|
+
setMessages((prev) => updater(prev).map(withMessageKey));
|
|
750
743
|
}, []);
|
|
751
744
|
// Non-append transcript rebuilds (/clear, /compact, /rewind, session switch)
|
|
752
745
|
// replace the settled list rather than extending it. The rows already
|
|
@@ -1662,8 +1655,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1662
1655
|
break;
|
|
1663
1656
|
}
|
|
1664
1657
|
case "mode_changed": {
|
|
1658
|
+
// Marker already persisted by onModeUpdate at setMode time.
|
|
1665
1659
|
setPermissionMode(event.mode);
|
|
1666
|
-
sessionManager?.appendMarker("mode_switch", event.mode);
|
|
1667
1660
|
break;
|
|
1668
1661
|
}
|
|
1669
1662
|
case "input_applied": {
|
|
@@ -49,7 +49,6 @@ export interface DisplayToolCall {
|
|
|
49
49
|
*/
|
|
50
50
|
rawArguments?: string;
|
|
51
51
|
result?: string;
|
|
52
|
-
resultCollapsed?: boolean;
|
|
53
52
|
isError?: boolean;
|
|
54
53
|
metadata?: ToolResultMetadata;
|
|
55
54
|
/** Set when the tool_start event was received. Used to render elapsed time. */
|
|
@@ -61,7 +60,6 @@ export declare function appendToolPart(parts: DisplayMessagePart[], toolCall: Di
|
|
|
61
60
|
export declare function snapshotDisplayParts(parts: DisplayMessagePart[]): DisplayMessagePart[];
|
|
62
61
|
export declare function contentFromParts(parts: DisplayMessagePart[]): string;
|
|
63
62
|
export declare function toolCallsFromParts(parts: DisplayMessagePart[]): DisplayToolCall[];
|
|
64
|
-
export declare function compactDisplayMessages(messages: DisplayMessage[]): DisplayMessage[];
|
|
65
63
|
/**
|
|
66
64
|
* Find the most recent compaction summary embedded in the agent's system
|
|
67
65
|
* messages. Bubble's compaction step rewrites the system transcript so that
|
|
@@ -87,64 +87,12 @@ export function contentFromParts(parts) {
|
|
|
87
87
|
export function toolCallsFromParts(parts) {
|
|
88
88
|
return parts.flatMap((part) => part.type === "tools" ? part.toolCalls : []);
|
|
89
89
|
}
|
|
90
|
-
const FULL_DETAIL_WINDOW = 24;
|
|
91
|
-
// Folding policy: message text (content, reasoning) is NEVER rewritten or
|
|
92
|
-
// truncated — what the user or the assistant said renders verbatim. All
|
|
93
|
-
// messages stay in the list (the alt-screen viewport scrolls them); older
|
|
94
|
-
// messages only collapse bulky tool-result bodies, which the UI re-expands
|
|
95
|
-
// on demand.
|
|
96
|
-
export function compactDisplayMessages(messages) {
|
|
97
|
-
if (messages.length === 0) {
|
|
98
|
-
return messages;
|
|
99
|
-
}
|
|
100
|
-
const visible = messages.filter((message) => message.syntheticKind !== "ui_summary");
|
|
101
|
-
const detailStart = Math.max(0, visible.length - FULL_DETAIL_WINDOW);
|
|
102
|
-
return visible.map((message, index) => (index < detailStart ? compactDisplayMessage(message) : message));
|
|
103
|
-
}
|
|
104
|
-
// Messages that already went through compaction. Re-compacting them would
|
|
105
|
-
// produce equal-but-new objects on every transcript update, which defeats the
|
|
106
|
-
// React.memo row cache in message-list.tsx — settled rows must keep identity.
|
|
107
|
-
const compactedMessages = new WeakSet();
|
|
108
|
-
function compactDisplayMessage(message) {
|
|
109
|
-
if (message.syntheticKind) {
|
|
110
|
-
return message;
|
|
111
|
-
}
|
|
112
|
-
if (compactedMessages.has(message)) {
|
|
113
|
-
return message;
|
|
114
|
-
}
|
|
115
|
-
const compacted = {
|
|
116
|
-
...message,
|
|
117
|
-
toolCalls: message.toolCalls?.map(compactToolCall),
|
|
118
|
-
parts: message.parts?.map(compactDisplayPart),
|
|
119
|
-
};
|
|
120
|
-
compactedMessages.add(compacted);
|
|
121
|
-
return compacted;
|
|
122
|
-
}
|
|
123
90
|
function cloneToolCall(toolCall) {
|
|
124
91
|
return {
|
|
125
92
|
...toolCall,
|
|
126
93
|
args: { ...toolCall.args },
|
|
127
94
|
};
|
|
128
95
|
}
|
|
129
|
-
function compactDisplayPart(part) {
|
|
130
|
-
if (part.type === "text") {
|
|
131
|
-
return part;
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
type: "tools",
|
|
135
|
-
toolCalls: part.toolCalls.map(compactToolCall),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
function compactToolCall(toolCall) {
|
|
139
|
-
if (toolCall.result === undefined) {
|
|
140
|
-
return toolCall;
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
...toolCall,
|
|
144
|
-
result: undefined,
|
|
145
|
-
resultCollapsed: true,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
96
|
const PREVIOUS_SUMMARY_PREFIX = /^Previous conversation summary:\s*\n?([\s\S]*)$/;
|
|
149
97
|
const TURN_SUMMARY_PREFIX = /^Earlier in this turn \(compacted to free context\):\s*\n?([\s\S]*)$/;
|
|
150
98
|
/**
|
package/dist/tui-ink/footer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { useTheme } from "./theme.js";
|
|
4
4
|
import { PERMISSION_MODE_INFO } from "../permission/mode.js";
|
|
@@ -30,7 +30,7 @@ function ModeBadge({ mode }) {
|
|
|
30
30
|
const info = PERMISSION_MODE_INFO[mode];
|
|
31
31
|
const color = theme[info.color] ?? theme.muted;
|
|
32
32
|
const symbol = info.symbol ? `${info.symbol} ` : "";
|
|
33
|
-
return (_jsxs(
|
|
33
|
+
return (_jsxs(Text, { color: color, bold: true, children: [symbol, info.shortTitle, " on"] }));
|
|
34
34
|
}
|
|
35
35
|
export function buildFooterData(input) {
|
|
36
36
|
return input;
|
|
@@ -8,7 +8,7 @@ import { useTheme } from "./theme.js";
|
|
|
8
8
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
9
9
|
import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
10
|
import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
|
-
import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer,
|
|
11
|
+
import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
|
|
12
12
|
import { isKeyReleaseEvent } from "./key-events.js";
|
|
13
13
|
import { PasteOperationTracker } from "./paste-operation-tracker.js";
|
|
14
14
|
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
@@ -26,6 +26,18 @@ export function createComposerDraftSnapshot(input) {
|
|
|
26
26
|
imageLabelStartOverride: input.imageLabelStartOverride,
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Rebuild a composer buffer from a persisted history entry. Text long enough to
|
|
31
|
+
* have been collapsed at paste time is collapsed again into a `[Pasted text #N …]`
|
|
32
|
+
* marker, but its full content is preserved in the buffer's paste spans so the
|
|
33
|
+
* composer stays compact, no content is lost, and the next submit re-expands it.
|
|
34
|
+
*/
|
|
35
|
+
function bufferFromHistoryText(text) {
|
|
36
|
+
if (shouldCollapsePastedContent(text)) {
|
|
37
|
+
return insertComposerPaste(createComposerBuffer(), text);
|
|
38
|
+
}
|
|
39
|
+
return createComposerBuffer(text);
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* Browse persisted history without turning marker-shaped text into live paste
|
|
31
43
|
* tokens, while preserving the exact unsent draft in memory for the final
|
|
@@ -68,7 +80,7 @@ export function stepComposerHistory(state, history, direction) {
|
|
|
68
80
|
};
|
|
69
81
|
}
|
|
70
82
|
return {
|
|
71
|
-
buffer:
|
|
83
|
+
buffer: bufferFromHistoryText(result.text),
|
|
72
84
|
attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
|
|
73
85
|
imageLabelStartOverride: result.imageDisplayStart ?? null,
|
|
74
86
|
historyIndex: result.index,
|
|
@@ -716,10 +728,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
716
728
|
return;
|
|
717
729
|
submittedPayloadFingerprintRef.current = fingerprint;
|
|
718
730
|
deliver(payload);
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
if (
|
|
731
|
+
// Always record the submitted text so ↑/↓ can recall it. The full expanded
|
|
732
|
+
// text is persisted (content is never lost); on recall, long text is
|
|
733
|
+
// re-collapsed into a paste marker that still carries its full content.
|
|
734
|
+
if (expandedText.trim().length > 0 || attachments.length > 0) {
|
|
723
735
|
const historyEntry = {
|
|
724
736
|
text: expandedText,
|
|
725
737
|
images: attachments,
|
|
@@ -10,6 +10,7 @@ import { buildTraceGroups, executeCommandBlock, formatTracePath, shouldInlineExe
|
|
|
10
10
|
import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
|
|
11
11
|
import { latestSubagentNote, sortSubagents, subagentDescriptor, subagentLabel, subagentStatusColor, subagentSummary, } from "./subagent-view.js";
|
|
12
12
|
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
13
|
+
import { stripFileBlocks } from "../context/compaction-files.js";
|
|
13
14
|
import { splitImageDisplayContent } from "../tui/image-display.js";
|
|
14
15
|
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
15
16
|
/**
|
|
@@ -152,7 +153,9 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
152
153
|
: sanitizeInternalReminderBlocks(part.content).trim() !== "");
|
|
153
154
|
const toolCallCount = message.toolCalls?.length ?? 0;
|
|
154
155
|
const hasVisibleAssistantContent = (hasParts ? hasVisibleParts : (!!visibleContent.trim() || toolCallCount > 0)) ||
|
|
155
|
-
|
|
156
|
+
// Reasoning now always renders (collapsed by default), so its presence
|
|
157
|
+
// alone makes a turn visible.
|
|
158
|
+
!!visibleReasoning ||
|
|
156
159
|
// A finalized turn carries taskElapsedMs and renders a TaskDurationLine even
|
|
157
160
|
// when its text/parts are empty — mirror that so the duration isn't dropped.
|
|
158
161
|
// (The verbose TurnDigest needs no term: whenever it renders, the same
|
|
@@ -160,7 +163,7 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
160
163
|
message.taskElapsedMs !== undefined;
|
|
161
164
|
if (!hasVisibleAssistantContent)
|
|
162
165
|
return null;
|
|
163
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (
|
|
166
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (_jsx(ReasoningTraceBlock, { reasoning: visibleReasoning, expanded: showThinking || verboseTrace })), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() && (
|
|
164
167
|
// Thread the real terminal width down (minus the row's paddingX
|
|
165
168
|
// inset); without it, width-aware blocks like tables fall back to
|
|
166
169
|
// a stale/default useTerminalSize and mis-budget their columns.
|
|
@@ -174,7 +177,7 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, s
|
|
|
174
177
|
const visibleParts = deferredParts.length > 0
|
|
175
178
|
? deferredParts
|
|
176
179
|
: fallbackStreamingParts(deferredContent, tools);
|
|
177
|
-
return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && (
|
|
180
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning, expanded: showThinking || verboseTrace, streaming: true }) })), visibleParts.length > 0 && (
|
|
178
181
|
// marginTop=1 matches the committed MessageItem layout exactly, so the
|
|
179
182
|
// gap under the user message is identical while streaming and after the
|
|
180
183
|
// turn commits — no spacing jump at finalize time. (The old marginTop=0
|
|
@@ -279,22 +282,33 @@ function TraceGroupBlock({ group, terminalColumns, pendingApproval, compactTop,
|
|
|
279
282
|
const commandWidth = Math.max(14, terminalColumns - group.title.length - 20);
|
|
280
283
|
const detailWidth = Math.max(20, terminalColumns - 8);
|
|
281
284
|
const detailLines = group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
285
|
+
// Finished, error-free Execute rows collapse to a one-line summary: the
|
|
286
|
+
// multi-line command body and stdout preview only earn space while running
|
|
287
|
+
// (you want to see what's executing) or on failure (you need the error).
|
|
288
|
+
// Ctrl+O (verbose trace) shows everything.
|
|
289
|
+
const collapsedExecute = group.kind === "execute" && !group.pending && !waiting && !group.hasError;
|
|
282
290
|
// A model-provided bash description owns the header slot; the command then
|
|
283
291
|
// always renders as a block below. Without one, single-line commands that
|
|
284
292
|
// fit stay inline; anything longer becomes a wrapped block preserving the
|
|
285
293
|
// command's own line structure — commands are never clipped mid-line.
|
|
294
|
+
// Collapsed rows always inline the (truncated) command instead of a block.
|
|
286
295
|
const showDescription = group.kind === "execute" && !!group.description;
|
|
287
296
|
const inlineCommand = !showDescription && group.command
|
|
288
297
|
? (group.kind === "execute"
|
|
289
|
-
? (shouldInlineExecuteCommand(group, commandWidth)
|
|
298
|
+
? (collapsedExecute || shouldInlineExecuteCommand(group, commandWidth)
|
|
299
|
+
? truncateVisual(group.command.replace(/\s+/g, " "), commandWidth)
|
|
300
|
+
: undefined)
|
|
290
301
|
: (visualWidth(group.command) <= commandWidth ? group.command : undefined))
|
|
291
302
|
: undefined;
|
|
292
|
-
const commandBlock = group.command && !inlineCommand
|
|
303
|
+
const commandBlock = group.command && !inlineCommand && !collapsedExecute
|
|
293
304
|
? (group.kind === "execute"
|
|
294
305
|
? executeCommandBlock(group, EXECUTE_COMMAND_BLOCK_MAX_LINES)
|
|
295
306
|
: { lines: [group.command], omitted: 0 })
|
|
296
307
|
: null;
|
|
297
|
-
|
|
308
|
+
const collapsedOutputLines = group.previewLines.length + group.omitted;
|
|
309
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, color: titleColor, children: group.title }), showDescription ? (_jsxs(Text, { color: theme.traceDetail, children: [" ", truncateVisual(group.description, commandWidth)] })) : inlineCommand ? (_jsxs(Text, { color: theme.traceCommand, children: [" ", inlineCommand] })) : !group.command && group.count !== undefined && group.noun ? (_jsxs(Text, { color: theme.traceCount, children: [" ", group.count, " ", group.noun] })) : null, status && _jsxs(Text, { color: status.color, children: [" ", status.text] })] }), commandBlock && commandBlock.lines.length > 0 && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [commandBlock.lines.flatMap((line, idx) => wrapByVisualWidth(line || " ", Math.max(10, detailWidth - 2)).map((seg, segIdx) => (_jsx(Text, { color: theme.traceCommand, children: seg }, `cmd-${idx}-${segIdx}`)))), commandBlock.omitted > 0 && (_jsxs(Text, { color: theme.traceDetail, children: ["\u2026 ", commandBlock.omitted, " more line", commandBlock.omitted === 1 ? "" : "s"] }))] })), collapsedExecute ? (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["\u23BF ", collapsedOutputLines > 0
|
|
310
|
+
? `${collapsedOutputLines} line${collapsedOutputLines === 1 ? "" : "s"} output · Ctrl+O to view`
|
|
311
|
+
: "no output"] }) })) : detailLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: detailLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: detailColor, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, index))) })), group.errorLines.length > 0 && (_jsx(Box, { flexDirection: "column", marginLeft: 2, children: group.errorLines.map((line, index) => (_jsxs(Box, { marginLeft: index === 0 ? 0 : 2, children: [index === 0 && _jsx(Text, { color: theme.traceDetail, children: "\u21B3 " }), _jsx(Text, { color: theme.error, children: truncateVisual(line, detailWidth - (index === 0 ? 2 : 0)) })] }, `error-${index}`))) })), group.omitted > 0 && !collapsedExecute && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: theme.traceDetail, children: ["... ", group.omitted, " more, Ctrl+O to view"] }) }))] }));
|
|
298
312
|
}
|
|
299
313
|
function EditTraceBlock({ tool, details, terminalColumns, compactTop, status, }) {
|
|
300
314
|
const theme = useTheme();
|
|
@@ -336,12 +350,33 @@ function approvalMatchesTool(hint, tc) {
|
|
|
336
350
|
return !hint.path || hint.path === tc.args.path;
|
|
337
351
|
}
|
|
338
352
|
function isToolPending(tool) {
|
|
339
|
-
return tool.result === undefined
|
|
353
|
+
return tool.result === undefined;
|
|
340
354
|
}
|
|
341
|
-
|
|
355
|
+
/** Live rolling window size — enough to read the model's train of thought. */
|
|
356
|
+
const REASONING_STREAM_WINDOW = 5;
|
|
357
|
+
/** Lines kept when a finished turn's reasoning is collapsed. */
|
|
358
|
+
const REASONING_COLLAPSED_LINES = 2;
|
|
359
|
+
/**
|
|
360
|
+
* Reasoning trace with three states, because a reasoning model can spend
|
|
361
|
+
* minutes here (qwen3.8-max at high effort drafts entire files in thought):
|
|
362
|
+
* hiding it looks like a hang, printing all of it buries the transcript.
|
|
363
|
+
*
|
|
364
|
+
* streaming + compact rolling last N lines, so there is always motion
|
|
365
|
+
* settled + compact opening lines + "(ctrl+o to expand)"
|
|
366
|
+
* expanded everything (Ctrl+T thinking, or Ctrl+O verbose)
|
|
367
|
+
*/
|
|
368
|
+
function ReasoningTraceBlock({ reasoning, expanded = false, streaming = false, }) {
|
|
342
369
|
const theme = useTheme();
|
|
343
370
|
const lines = React.useMemo(() => reasoning.split("\n").filter((l) => l.trim() !== ""), [reasoning]);
|
|
344
|
-
|
|
371
|
+
if (lines.length === 0)
|
|
372
|
+
return null;
|
|
373
|
+
const visible = expanded
|
|
374
|
+
? lines
|
|
375
|
+
: streaming
|
|
376
|
+
? lines.slice(-REASONING_STREAM_WINDOW)
|
|
377
|
+
: lines.slice(0, REASONING_COLLAPSED_LINES);
|
|
378
|
+
const hidden = lines.length - visible.length;
|
|
379
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: theme.thinkingDim, dimColor: true, children: ["\u273B ", streaming && !expanded ? "Thinking…" : "Thinking"] }), visible.map((line, i) => (_jsx(Text, { color: theme.thinkingDim, dimColor: true, italic: true, children: line }, i))), hidden > 0 && (_jsx(Text, { color: theme.thinkingDim, dimColor: true, children: streaming ? "…" : "… (ctrl+o to expand)" }))] }));
|
|
345
380
|
}
|
|
346
381
|
function CompactionSummaryBlock({ message }) {
|
|
347
382
|
const theme = useTheme();
|
|
@@ -351,22 +386,29 @@ function CompactionSummaryBlock({ message }) {
|
|
|
351
386
|
// markup before rendering, so a summary that echoed it never reaches the
|
|
352
387
|
// transcript. Belt-and-suspenders — the summarizer is fed sanitized history,
|
|
353
388
|
// but the summary is model-generated and also re-injected as context.
|
|
354
|
-
|
|
389
|
+
// File-tracking blocks (<read-files>/<modified-files>) are model-facing
|
|
390
|
+
// bookkeeping welded onto the summary — never render them raw either.
|
|
391
|
+
const summary = stripFileBlocks(sanitizeInternalReminderBlocks(message.compactionSummary ?? "")).trim() || undefined;
|
|
355
392
|
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, paddingX: 1, flexDirection: "column", borderStyle: "round", borderColor: theme.borderActive, children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.success, bold: true, children: "\u2713 " }), _jsx(Text, { color: theme.accent, bold: true, children: "Compaction checkpoint" }), _jsxs(Text, { color: theme.muted, children: [" \u00B7 ", status] })] }), summary && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: theme.muted, dimColor: true, children: "Preserved context summary" }), _jsx(Box, { paddingLeft: 2, flexDirection: "column", children: _jsx(MarkdownContent, { content: summary }) })] }))] }));
|
|
356
393
|
}
|
|
357
394
|
function UserMessageBlock({ content, terminalColumns, inputStatus, separateFromPrevious = false, }) {
|
|
358
395
|
const theme = useTheme();
|
|
359
396
|
const badge = userInputStatusBadgeLabel(inputStatus);
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
|
|
397
|
+
// Defense in depth: pure internal-block rows are dropped upstream
|
|
398
|
+
// (isInternalBlockOnlyContent), but a block with trailing text — or a
|
|
399
|
+
// truncated block — would slip that anchored filter. Never render markup.
|
|
400
|
+
const visibleContent = sanitizeInternalReminderBlocks(content);
|
|
401
|
+
// Preserve the message's existing text inset after removing the decorative
|
|
402
|
+
// rail: the former two rail columns become ordinary background padding.
|
|
403
|
+
const leftPaddingWidth = 3;
|
|
404
|
+
const rightPaddingWidth = 1;
|
|
363
405
|
const horizontalRoom = Math.max(20, terminalColumns - 2);
|
|
364
|
-
const bubbleTextWidth = Math.max(1, horizontalRoom -
|
|
365
|
-
const { bodyLines, referenceLines } = splitImageDisplayContent(
|
|
406
|
+
const bubbleTextWidth = Math.max(1, horizontalRoom - leftPaddingWidth - rightPaddingWidth);
|
|
407
|
+
const { bodyLines, referenceLines } = splitImageDisplayContent(visibleContent);
|
|
366
408
|
const wrappedLines = bodyLines
|
|
367
409
|
.flatMap((line) => wrapByVisualWidth(line, bubbleTextWidth));
|
|
368
|
-
const attachmentReferenceIndent = " ".repeat(
|
|
369
|
-
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) => (
|
|
410
|
+
const attachmentReferenceIndent = " ".repeat(leftPaddingWidth);
|
|
411
|
+
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) => (_jsx(Box, { children: _jsx(Text, { backgroundColor: theme.userMessageBg, color: theme.userMessageText, children: `${" ".repeat(leftPaddingWidth)}${padVisual(line || " ", bubbleTextWidth)}${" ".repeat(rightPaddingWidth)}` }) }, index))), referenceLines.map((line, index) => (_jsx(Box, { children: _jsx(Text, { color: theme.muted, children: `${attachmentReferenceIndent}${line}` }) }, `attachment-${index}`)))] }));
|
|
370
412
|
}
|
|
371
413
|
function PendingInputMessagesBlock({ messages, terminalColumns, title, hint, bulletColor, }) {
|
|
372
414
|
const theme = useTheme();
|
|
@@ -434,8 +476,6 @@ function getToolHeader(toolCall) {
|
|
|
434
476
|
}
|
|
435
477
|
}
|
|
436
478
|
function summarizeToolResult(tc) {
|
|
437
|
-
if (tc.resultCollapsed)
|
|
438
|
-
return tc.isError ? "error output collapsed" : "result collapsed";
|
|
439
479
|
if (tc.result === undefined)
|
|
440
480
|
return "pending";
|
|
441
481
|
const raw = tc.result.replace(/\r\n/g, "\n");
|
|
@@ -557,13 +597,12 @@ function ToolCallDisplay({ toolCall, isStreaming, verbose, terminalColumns, show
|
|
|
557
597
|
summaryColor = theme.success;
|
|
558
598
|
}
|
|
559
599
|
const editDetails = getEditDiffDetails(toolCall);
|
|
560
|
-
const isEditDiff = editDetails !== null && toolCall.result !== undefined
|
|
561
|
-
const showSummary = !toolCall.resultCollapsed || waitingApproval;
|
|
600
|
+
const isEditDiff = editDetails !== null && toolCall.result !== undefined;
|
|
562
601
|
// Only show the file preview once the tool actually executed. During the
|
|
563
602
|
// streaming-args phase, args.content is incomplete and re-rendering the
|
|
564
603
|
// entire body per delta both looks chaotic and breaks on partial escapes.
|
|
565
|
-
const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined
|
|
566
|
-
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: bulletColor, children: [glyph, " "] }), _jsx(Text, { bold: true, color: theme.toolName, children: name }), header && _jsxs(Text, { color: theme.muted, children: ["(", header, ")"] })] }),
|
|
604
|
+
const isWritePreview = toolCall.name === "write" && !toolCall.isError && toolCall.result !== undefined;
|
|
605
|
+
return (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsxs(Box, { children: [_jsxs(Text, { color: bulletColor, children: [glyph, " "] }), _jsx(Text, { bold: true, color: theme.toolName, children: name }), header && _jsxs(Text, { color: theme.muted, children: ["(", header, ")"] })] }), _jsx(Box, { marginLeft: 2, children: _jsxs(Text, { color: summaryColor, children: ["\u23BF ", summary] }) }), toolCall.isError && toolCall.result && (_jsx(Box, { marginLeft: 4, flexDirection: "column", children: toolCall.result.replace(/\r\n/g, "\n").split("\n").slice(0, 6).map((line, i) => (_jsx(Text, { color: theme.error, children: line }, i))) })), isEditDiff && (_jsx(DiffBlock, { diff: editDetails.diff, terminalColumns: terminalColumns, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), isWritePreview && (_jsx(WritePreview, { content: String(toolCall.args.content || ""), maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint })), !toolCall.isError && !isEditDiff && !isWritePreview && highlighted && (_jsx(OutputPreview, { text: highlighted, maxLines: maxLines, verbose: verbose, showExpandHint: showExpandHint }))] }));
|
|
567
606
|
}
|
|
568
607
|
function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }) {
|
|
569
608
|
const theme = useTheme();
|
package/dist/tui-ink/run.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
+
import { END_SYNC, wrapSynchronizedOutput } from "./sync-output.js";
|
|
3
4
|
import { App } from "./app.js";
|
|
4
5
|
import { warmHighlighter } from "./code-highlight.js";
|
|
5
6
|
export function createInkAppElement(agent, args, options, onExit) {
|
|
@@ -8,16 +9,17 @@ export function createInkAppElement(agent, args, options, onExit) {
|
|
|
8
9
|
/**
|
|
9
10
|
* Best-effort terminal restore for abnormal exits. Bubble renders into the
|
|
10
11
|
* primary screen (no alt-screen, no mouse reporting) so the transcript flows
|
|
11
|
-
* into the terminal's native scrollback
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* into the terminal's native scrollback. We end synchronized output first —
|
|
13
|
+
* a crash between a frame's ?2026h and ?2026l would otherwise leave the
|
|
14
|
+
* terminal buffering until its own timeout — then restore the cursor,
|
|
15
|
+
* mirroring Ink's teardown (idempotent when Ink already ran; load-bearing
|
|
16
|
+
* when it didn't).
|
|
15
17
|
*/
|
|
16
18
|
function restoreTerminal() {
|
|
17
19
|
if (!process.stdout.isTTY)
|
|
18
20
|
return;
|
|
19
21
|
try {
|
|
20
|
-
process.stdout.write(
|
|
22
|
+
process.stdout.write(`${END_SYNC}\x1b[?25h`);
|
|
21
23
|
}
|
|
22
24
|
catch {
|
|
23
25
|
// stdout may already be destroyed during shutdown
|
|
@@ -77,6 +79,9 @@ export async function runTui(agent, args, options = {}) {
|
|
|
77
79
|
// and frees the arrow keys for composer history. Only the streaming tail
|
|
78
80
|
// and the composer live in the repainting region at the bottom.
|
|
79
81
|
alternateScreen: false,
|
|
82
|
+
// Frames composite atomically via synchronized output (CSI ?2026) —
|
|
83
|
+
// kills erase-then-repaint flicker without alt-screen.
|
|
84
|
+
stdout: wrapSynchronizedOutput(process.stdout),
|
|
80
85
|
});
|
|
81
86
|
try {
|
|
82
87
|
await instance.waitUntilExit();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronized-output wrapper (CSI ?2026, "sync updates").
|
|
3
|
+
*
|
|
4
|
+
* Every frame Ink writes gets bracketed in begin/end synchronized-output so
|
|
5
|
+
* the terminal composites it atomically instead of painting mid-erase state —
|
|
6
|
+
* the primary source of visible flicker during streaming. This is exactly
|
|
7
|
+
* what pi's main-screen renderer does per paint batch; alt-screen is NOT
|
|
8
|
+
* required for it.
|
|
9
|
+
*
|
|
10
|
+
* Terminals that don't implement mode 2026 ignore unknown DEC private modes,
|
|
11
|
+
* so emitting unconditionally is safe; BUBBLE_NO_SYNC_OUTPUT=1 is the escape
|
|
12
|
+
* hatch for anything that misbehaves. Only string chunks are bracketed —
|
|
13
|
+
* Ink writes frames as strings; binary writes pass through untouched.
|
|
14
|
+
*/
|
|
15
|
+
export declare const BEGIN_SYNC = "\u001B[?2026h";
|
|
16
|
+
export declare const END_SYNC = "\u001B[?2026l";
|
|
17
|
+
export declare function wrapSynchronizedOutput(stream: NodeJS.WriteStream): NodeJS.WriteStream;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synchronized-output wrapper (CSI ?2026, "sync updates").
|
|
3
|
+
*
|
|
4
|
+
* Every frame Ink writes gets bracketed in begin/end synchronized-output so
|
|
5
|
+
* the terminal composites it atomically instead of painting mid-erase state —
|
|
6
|
+
* the primary source of visible flicker during streaming. This is exactly
|
|
7
|
+
* what pi's main-screen renderer does per paint batch; alt-screen is NOT
|
|
8
|
+
* required for it.
|
|
9
|
+
*
|
|
10
|
+
* Terminals that don't implement mode 2026 ignore unknown DEC private modes,
|
|
11
|
+
* so emitting unconditionally is safe; BUBBLE_NO_SYNC_OUTPUT=1 is the escape
|
|
12
|
+
* hatch for anything that misbehaves. Only string chunks are bracketed —
|
|
13
|
+
* Ink writes frames as strings; binary writes pass through untouched.
|
|
14
|
+
*/
|
|
15
|
+
export const BEGIN_SYNC = "\x1b[?2026h";
|
|
16
|
+
export const END_SYNC = "\x1b[?2026l";
|
|
17
|
+
export function wrapSynchronizedOutput(stream) {
|
|
18
|
+
if (!stream.isTTY || process.env.BUBBLE_NO_SYNC_OUTPUT === "1")
|
|
19
|
+
return stream;
|
|
20
|
+
return new Proxy(stream, {
|
|
21
|
+
get(target, prop) {
|
|
22
|
+
if (prop === "write") {
|
|
23
|
+
return (chunk, ...rest) => typeof chunk === "string" && chunk.length > 0
|
|
24
|
+
? target.write(BEGIN_SYNC + chunk + END_SYNC, ...rest)
|
|
25
|
+
: target.write(chunk, ...rest);
|
|
26
|
+
}
|
|
27
|
+
const value = Reflect.get(target, prop, target);
|
|
28
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
29
|
+
},
|
|
30
|
+
set(target, prop, value) {
|
|
31
|
+
Reflect.set(target, prop, value);
|
|
32
|
+
return true;
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
package/dist/tui-ink/theme.d.ts
CHANGED
package/dist/tui-ink/theme.js
CHANGED
|
@@ -46,7 +46,6 @@ export const darkTheme = {
|
|
|
46
46
|
userMessageBorder: "#8A7FC6",
|
|
47
47
|
userMessageBg: "#2a2a34",
|
|
48
48
|
userMessageText: "#f3f3f7",
|
|
49
|
-
userRail: "#8A7FC6",
|
|
50
49
|
diffAdd: "#1a3d1a",
|
|
51
50
|
diffRemove: "#3d1a1a",
|
|
52
51
|
diffAddFg: "green",
|
|
@@ -95,7 +94,6 @@ export const lightTheme = {
|
|
|
95
94
|
userMessageBorder: "#356FD2",
|
|
96
95
|
userMessageBg: "#F1F3F0",
|
|
97
96
|
userMessageText: "#234B93",
|
|
98
|
-
userRail: "#356FD2",
|
|
99
97
|
diffAdd: "#D7E8D8",
|
|
100
98
|
diffRemove: "#F7DADC",
|
|
101
99
|
diffAddFg: "#2F7D4A",
|
|
@@ -387,7 +387,7 @@ function isFailedSubagent(subagent) {
|
|
|
387
387
|
return subagent.status === "failed" || subagent.status === "blocked" || subagent.status === "cancelled";
|
|
388
388
|
}
|
|
389
389
|
function isToolPending(tool) {
|
|
390
|
-
return tool.result === undefined
|
|
390
|
+
return tool.result === undefined;
|
|
391
391
|
}
|
|
392
392
|
function isDirectoryLikeGlob(pattern) {
|
|
393
393
|
const normalized = pattern.trim();
|
package/dist/types.d.ts
CHANGED
|
@@ -130,7 +130,6 @@ export interface ToolResultMetadata {
|
|
|
130
130
|
searchSignature?: string;
|
|
131
131
|
searchFamily?: string;
|
|
132
132
|
reason?: string;
|
|
133
|
-
arbiterNote?: string;
|
|
134
133
|
diff?: string;
|
|
135
134
|
addedLines?: number;
|
|
136
135
|
removedLines?: number;
|
|
@@ -235,6 +234,15 @@ export interface ToolRegistryEntry extends ToolDefinition {
|
|
|
235
234
|
* fan-out never share it (design v2 §2). Tools without it are shared as-is.
|
|
236
235
|
*/
|
|
237
236
|
cloneForChild?: () => ToolRegistryEntry;
|
|
237
|
+
/**
|
|
238
|
+
* Optional availability predicate, evaluated on EVERY model call. When it
|
|
239
|
+
* returns false the tool is omitted from the provider's function list
|
|
240
|
+
* entirely — the model cannot see or call it — and reappears on the next
|
|
241
|
+
* call once the predicate flips. For state-gated tools (e.g. update_goal
|
|
242
|
+
* without an active goal) this removes the affordance itself instead of
|
|
243
|
+
* relying on description wording, which strong models ignore.
|
|
244
|
+
*/
|
|
245
|
+
enabled?: () => boolean;
|
|
238
246
|
/** Optional one-line summary for the Available tools section. */
|
|
239
247
|
promptSnippet?: string;
|
|
240
248
|
/** Optional tool-specific rules appended to the system prompt when this tool is active. */
|
|
@@ -314,6 +322,17 @@ export interface TokenUsage {
|
|
|
314
322
|
reasoningTokens?: number;
|
|
315
323
|
totalTokens?: number;
|
|
316
324
|
}
|
|
325
|
+
/**
|
|
326
|
+
* Priced token usage (see model-pricing.ts). Lives here rather than in
|
|
327
|
+
* model-pricing.ts because turn_end events carry it and model-pricing
|
|
328
|
+
* already imports from this module.
|
|
329
|
+
*/
|
|
330
|
+
export interface UsageCost {
|
|
331
|
+
currency: "USD" | "CNY";
|
|
332
|
+
cost: number;
|
|
333
|
+
/** True when the provider gave no cache breakdown and everything was billed at the miss rate. */
|
|
334
|
+
estimated: boolean;
|
|
335
|
+
}
|
|
317
336
|
export interface Provider {
|
|
318
337
|
streamChat(messages: ProviderMessage[], options: {
|
|
319
338
|
model: string;
|
|
@@ -410,6 +429,7 @@ export type AgentEvent = {
|
|
|
410
429
|
} | {
|
|
411
430
|
type: "turn_end";
|
|
412
431
|
usage?: TokenUsage;
|
|
432
|
+
cost?: UsageCost;
|
|
413
433
|
willContinue?: boolean;
|
|
414
434
|
} | {
|
|
415
435
|
type: "context_recovered";
|