@bubblebrain-ai/bubble 0.0.15 → 0.0.16
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 +24 -0
- package/dist/agent/discovery-barrier.d.ts +21 -0
- package/dist/agent/discovery-barrier.js +173 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +171 -0
- package/dist/agent/task-classifier.js +23 -5
- package/dist/agent.js +119 -26
- package/dist/context/projector.js +4 -3
- package/dist/model-catalog.js +6 -0
- package/dist/model-pricing.d.ts +3 -2
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.d.ts +16 -0
- package/dist/network/chatgpt-transport.js +240 -0
- package/dist/oauth/openai-codex.d.ts +7 -2
- package/dist/oauth/openai-codex.js +7 -4
- package/dist/orchestrator/default-hooks.js +13 -2
- package/dist/orchestrator/hooks.d.ts +2 -0
- package/dist/provider-openai-codex.d.ts +3 -0
- package/dist/provider-openai-codex.js +11 -2
- package/dist/provider-transform.js +9 -0
- package/dist/reasoning-debug.js +4 -1
- package/dist/session-log.js +4 -1
- package/dist/stats/usage.d.ts +4 -0
- package/dist/stats/usage.js +48 -11
- package/dist/tools/glob.js +3 -0
- package/dist/tools/grep.js +7 -0
- package/dist/tui/run.js +22 -12
- package/dist/tui-ink/app.js +3 -0
- package/dist/tui-ink/message-list.js +6 -3
- package/dist/tui-opentui/app.js +3 -0
- package/dist/tui-opentui/message-list.js +6 -3
- package/dist/types.d.ts +1 -1
- package/package.json +2 -1
package/dist/stats/usage.js
CHANGED
|
@@ -61,11 +61,15 @@ export function formatCompactNumber(value) {
|
|
|
61
61
|
return String(Math.round(value));
|
|
62
62
|
}
|
|
63
63
|
export function formatCurrency(value) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
return formatCurrencyFor(value, "USD");
|
|
65
|
+
}
|
|
66
|
+
function formatCurrencyFor(value, currency) {
|
|
67
|
+
const amount = value >= 1
|
|
68
|
+
? value.toFixed(2)
|
|
69
|
+
: value >= 0.01
|
|
70
|
+
? value.toFixed(3)
|
|
71
|
+
: value.toFixed(4);
|
|
72
|
+
return currency === "USD" ? `$${amount}` : `CNY ${amount}`;
|
|
69
73
|
}
|
|
70
74
|
function createAccumulator(range, days, now) {
|
|
71
75
|
const end = startOfLocalDay(now);
|
|
@@ -146,7 +150,11 @@ function finalizeAccumulator(accumulator) {
|
|
|
146
150
|
.filter((model) => model.totalTokens > 0)
|
|
147
151
|
.sort((a, b) => b.totalTokens - a.totalTokens);
|
|
148
152
|
const totalTokens = models.reduce((sum, model) => sum + model.totalTokens, 0);
|
|
149
|
-
const
|
|
153
|
+
const trackedCosts = aggregateCosts(models);
|
|
154
|
+
const trackedCostEntries = trackedCosts
|
|
155
|
+
? Object.entries(trackedCosts)
|
|
156
|
+
: [];
|
|
157
|
+
const trackedCostEntry = trackedCostEntries.length === 1 ? trackedCostEntries[0] : undefined;
|
|
150
158
|
return {
|
|
151
159
|
range: accumulator.range,
|
|
152
160
|
days: accumulator.days,
|
|
@@ -156,7 +164,9 @@ function finalizeAccumulator(accumulator) {
|
|
|
156
164
|
heatmap: buildHeatmap(daily),
|
|
157
165
|
models,
|
|
158
166
|
totalTokens,
|
|
159
|
-
|
|
167
|
+
trackedCosts,
|
|
168
|
+
trackedCost: trackedCostEntry ? trackedCostEntry[1] : undefined,
|
|
169
|
+
trackedCostCurrency: trackedCostEntry ? trackedCostEntry[0] : undefined,
|
|
160
170
|
activeDays: daily.filter((day) => day.active).length,
|
|
161
171
|
sessionsScanned: accumulator.sessionsScanned,
|
|
162
172
|
sessionsWithoutTokenData: accumulator.sessionsWithoutTokenData,
|
|
@@ -189,8 +199,10 @@ function addModelUsage(accumulator, model, message, usage) {
|
|
|
189
199
|
existing.totalTokens += tokenTotal(usage);
|
|
190
200
|
if (providerId && modelId) {
|
|
191
201
|
const cost = calculateUsageCost(providerId, modelId, usage);
|
|
192
|
-
if (cost)
|
|
202
|
+
if (cost) {
|
|
193
203
|
existing.cost = (existing.cost ?? 0) + cost.cost;
|
|
204
|
+
existing.costCurrency = cost.currency;
|
|
205
|
+
}
|
|
194
206
|
}
|
|
195
207
|
accumulator.modelUsage.set(key, existing);
|
|
196
208
|
}
|
|
@@ -254,7 +266,9 @@ function formatModelUsageLines(stats, width) {
|
|
|
254
266
|
const percentText = `${Math.round(percent * 100)}%`.padStart(4, " ");
|
|
255
267
|
const tokenText = formatCompactNumber(model.totalTokens).padStart(6, " ");
|
|
256
268
|
const turnsText = `${model.turns}t`.padStart(4, " ");
|
|
257
|
-
const costText = showCost
|
|
269
|
+
const costText = showCost
|
|
270
|
+
? ` ${(model.cost !== undefined ? formatCurrencyFor(model.cost, model.costCurrency ?? "USD") : "").padStart(7, " ")}`
|
|
271
|
+
: "";
|
|
258
272
|
return ` ${truncate(model.displayName, labelWidth).padEnd(labelWidth, " ")} ${bar} ${percentText} ${tokenText} ${turnsText}${costText}`.trimEnd();
|
|
259
273
|
});
|
|
260
274
|
if (stats.models.length > MAX_MODEL_ROWS) {
|
|
@@ -270,14 +284,37 @@ function formatSummaryLines(stats, width) {
|
|
|
270
284
|
if (favorite) {
|
|
271
285
|
lines.push(` Favorite model ${truncate(favorite, Math.max(12, width - 17))}`);
|
|
272
286
|
}
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
const trackedCostText = formatTrackedCosts(stats);
|
|
288
|
+
if (trackedCostText)
|
|
289
|
+
lines.push(` Tracked cost ${trackedCostText}`);
|
|
275
290
|
lines.push(` Sessions scanned ${stats.sessionsScanned}`);
|
|
276
291
|
if (stats.sessionsWithoutTokenData > 0) {
|
|
277
292
|
lines.push(` Sessions without token data ${stats.sessionsWithoutTokenData}`);
|
|
278
293
|
}
|
|
279
294
|
return lines;
|
|
280
295
|
}
|
|
296
|
+
function aggregateCosts(models) {
|
|
297
|
+
const totals = {};
|
|
298
|
+
for (const model of models) {
|
|
299
|
+
if (model.cost === undefined)
|
|
300
|
+
continue;
|
|
301
|
+
const currency = model.costCurrency ?? "USD";
|
|
302
|
+
totals[currency] = (totals[currency] ?? 0) + model.cost;
|
|
303
|
+
}
|
|
304
|
+
return Object.keys(totals).length > 0 ? totals : undefined;
|
|
305
|
+
}
|
|
306
|
+
function formatTrackedCosts(stats) {
|
|
307
|
+
if (stats.trackedCosts) {
|
|
308
|
+
const parts = Object.entries(stats.trackedCosts)
|
|
309
|
+
.filter(([, value]) => value > 0)
|
|
310
|
+
.map(([currency, value]) => formatCurrencyFor(value, currency));
|
|
311
|
+
return parts.length > 0 ? parts.join(" + ") : undefined;
|
|
312
|
+
}
|
|
313
|
+
if (stats.trackedCost !== undefined) {
|
|
314
|
+
return formatCurrencyFor(stats.trackedCost, stats.trackedCostCurrency ?? "USD");
|
|
315
|
+
}
|
|
316
|
+
return undefined;
|
|
317
|
+
}
|
|
281
318
|
function heatmapCell(day, maxTokens) {
|
|
282
319
|
if (!day)
|
|
283
320
|
return " ";
|
package/dist/tools/glob.js
CHANGED
|
@@ -65,6 +65,7 @@ export function createGlobTool(cwd) {
|
|
|
65
65
|
}
|
|
66
66
|
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path));
|
|
67
67
|
const matches = files.slice(0, MAX_RESULTS).map((item) => item.path);
|
|
68
|
+
const absoluteMatches = matches.map((item) => resolve(root, item));
|
|
68
69
|
const wasTruncated = truncated.value || files.length > MAX_RESULTS;
|
|
69
70
|
if (matches.length === 0) {
|
|
70
71
|
return {
|
|
@@ -78,6 +79,7 @@ export function createGlobTool(cwd) {
|
|
|
78
79
|
truncated: false,
|
|
79
80
|
searchSignature: `glob:${root}:${pattern}`,
|
|
80
81
|
searchFamily: `glob:${pattern}`,
|
|
82
|
+
paths: [],
|
|
81
83
|
},
|
|
82
84
|
};
|
|
83
85
|
}
|
|
@@ -92,6 +94,7 @@ export function createGlobTool(cwd) {
|
|
|
92
94
|
truncated: wasTruncated,
|
|
93
95
|
searchSignature: `glob:${root}:${pattern}`,
|
|
94
96
|
searchFamily: `glob:${pattern}`,
|
|
97
|
+
paths: absoluteMatches,
|
|
95
98
|
},
|
|
96
99
|
};
|
|
97
100
|
},
|
package/dist/tools/grep.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Grep tool - search file contents using ripgrep.
|
|
3
3
|
*/
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
|
+
import { resolve as resolvePath } from "node:path";
|
|
5
6
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
6
7
|
import { analyzeToolIntent } from "../agent/tool-intent.js";
|
|
7
8
|
import { resolveToolPath } from "./path-utils.js";
|
|
@@ -57,6 +58,7 @@ export function createGrepTool(cwd) {
|
|
|
57
58
|
// rg returns exit code 1 when no matches found, which is not an error for us
|
|
58
59
|
const lines = stdout.split("\n").filter((l) => l.trim() !== "");
|
|
59
60
|
const matches = [];
|
|
61
|
+
const matchedPaths = new Set();
|
|
60
62
|
for (const line of lines) {
|
|
61
63
|
try {
|
|
62
64
|
const obj = JSON.parse(line);
|
|
@@ -64,6 +66,9 @@ export function createGrepTool(cwd) {
|
|
|
64
66
|
const path = obj.data.path.text;
|
|
65
67
|
const lineNum = obj.data.line_number;
|
|
66
68
|
const text = obj.data.lines.text?.trim() ?? "";
|
|
69
|
+
if (typeof path === "string" && path.trim()) {
|
|
70
|
+
matchedPaths.add(resolvePath(cwd, path));
|
|
71
|
+
}
|
|
67
72
|
matches.push(`${path}:${lineNum}: ${text}`);
|
|
68
73
|
}
|
|
69
74
|
}
|
|
@@ -83,6 +88,7 @@ export function createGrepTool(cwd) {
|
|
|
83
88
|
truncated: false,
|
|
84
89
|
searchSignature: intent.search?.signature,
|
|
85
90
|
searchFamily: intent.search?.familyKey,
|
|
91
|
+
paths: [],
|
|
86
92
|
},
|
|
87
93
|
});
|
|
88
94
|
return;
|
|
@@ -103,6 +109,7 @@ export function createGrepTool(cwd) {
|
|
|
103
109
|
truncated,
|
|
104
110
|
searchSignature: intent.search?.signature,
|
|
105
111
|
searchFamily: intent.search?.familyKey,
|
|
112
|
+
paths: [...matchedPaths],
|
|
106
113
|
},
|
|
107
114
|
});
|
|
108
115
|
});
|
package/dist/tui/run.js
CHANGED
|
@@ -9,6 +9,8 @@ import { homedir } from "node:os";
|
|
|
9
9
|
import { AgentAbortError } from "../agent.js";
|
|
10
10
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
11
11
|
import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js";
|
|
12
|
+
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
13
|
+
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
12
14
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
|
|
13
15
|
import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
14
16
|
import { calculateUsageCost } from "../model-pricing.js";
|
|
@@ -60,6 +62,7 @@ const PROVIDER_PRIORITY = new Map([
|
|
|
60
62
|
["zai", 5],
|
|
61
63
|
["zai-coding-plan", 6],
|
|
62
64
|
["kimi-for-coding", 7],
|
|
65
|
+
["stepfun", 8],
|
|
63
66
|
]);
|
|
64
67
|
const DEFAULT_THEME = {
|
|
65
68
|
primary: "#fab283",
|
|
@@ -5075,6 +5078,7 @@ function OpenTuiApp(props) {
|
|
|
5075
5078
|
"zhipuai-coding-plan": "Coding Plan",
|
|
5076
5079
|
"zai-coding-plan": "Coding Plan",
|
|
5077
5080
|
"kimi-for-coding": "Coding Plan",
|
|
5081
|
+
stepfun: "Step Plan API key",
|
|
5078
5082
|
local: "OpenAI-compatible local endpoint",
|
|
5079
5083
|
};
|
|
5080
5084
|
return descriptions[providerId] ?? "API key";
|
|
@@ -6581,7 +6585,7 @@ function OpenTuiApp(props) {
|
|
|
6581
6585
|
completionTokens: usage.completionTokens,
|
|
6582
6586
|
reasoningTokens: usage.reasoningTokens,
|
|
6583
6587
|
turns: usage.turns,
|
|
6584
|
-
costText: cost ? `${formatCurrency(cost.cost)} spent${cost.estimated ? " est." : ""}` : "cost unavailable",
|
|
6588
|
+
costText: cost ? `${formatCurrency(cost.cost, cost.currency)} spent${cost.estimated ? " est." : ""}` : "cost unavailable",
|
|
6585
6589
|
};
|
|
6586
6590
|
}
|
|
6587
6591
|
function sidebarMcpStates() {
|
|
@@ -6950,12 +6954,14 @@ function renderUserMessage(message, index) {
|
|
|
6950
6954
|
}, h("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: theme.backgroundPanel, flexShrink: 0, flexDirection: "column" }, ...userChildren));
|
|
6951
6955
|
}
|
|
6952
6956
|
function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false, width = 80) {
|
|
6957
|
+
const visibleReasoning = showThinking
|
|
6958
|
+
? sanitizeInternalReminderBlocks(message.reasoning ?? "").trim()
|
|
6959
|
+
: "";
|
|
6953
6960
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
6954
|
-
if (modelSwitch && !
|
|
6961
|
+
if (modelSwitch && !visibleReasoning && !(message.toolCalls?.length)) {
|
|
6955
6962
|
return renderModelSwitchMessage(modelSwitch);
|
|
6956
6963
|
}
|
|
6957
6964
|
const children = [];
|
|
6958
|
-
const visibleReasoning = showThinking ? message.reasoning?.trim() : "";
|
|
6959
6965
|
const parts = message.parts ?? [];
|
|
6960
6966
|
const hasParts = parts.length > 0;
|
|
6961
6967
|
if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length) && !hasParts) {
|
|
@@ -8433,12 +8439,14 @@ function pickerTitle(kind, providerId) {
|
|
|
8433
8439
|
function getModelPickerReasoningLevels(providerId, modelId) {
|
|
8434
8440
|
// Only expand into one picker row per effort for models that genuinely have a
|
|
8435
8441
|
// reasoning-effort spectrum: OpenAI's reasoning models (codex gpt-5.x:
|
|
8436
|
-
// off/minimal/low/medium/high/xhigh)
|
|
8442
|
+
// off/minimal/low/medium/high/xhigh), DeepSeek's v4 models, and StepFun
|
|
8443
|
+
// Step Plan models. Other providers
|
|
8437
8444
|
// (e.g. GLM, Moonshot/Kimi) only have a thinking on/off toggle, not an effort
|
|
8438
8445
|
// control, so they stay as a single row.
|
|
8439
8446
|
const isOpenAIReasoning = providerId === "openai" || providerId === "openai-codex";
|
|
8440
8447
|
const isDeepseekReasoning = providerId === "deepseek" && (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro");
|
|
8441
|
-
|
|
8448
|
+
const isStepFunReasoning = providerId === "stepfun";
|
|
8449
|
+
if (!isOpenAIReasoning && !isDeepseekReasoning && !isStepFunReasoning)
|
|
8442
8450
|
return [];
|
|
8443
8451
|
const levels = getAvailableThinkingLevels(providerId, modelId);
|
|
8444
8452
|
// gpt-4o and friends report only ["off"] — keep those as a single row too.
|
|
@@ -8451,8 +8459,9 @@ function displayModelWithThinking(model, thinkingLevel) {
|
|
|
8451
8459
|
if (!providerId)
|
|
8452
8460
|
return displayModel(model);
|
|
8453
8461
|
// Use the same scoping as the picker: only models with a real reasoning-effort
|
|
8454
|
-
// spectrum (OpenAI codex gpt-5.x, deepseek v4
|
|
8455
|
-
// on/off thinking toggle on GLM / Moonshot(Kimi) is
|
|
8462
|
+
// spectrum (OpenAI codex gpt-5.x, deepseek v4, StepFun Step Plan) get the
|
|
8463
|
+
// "(level)" suffix. The on/off thinking toggle on GLM / Moonshot(Kimi) is
|
|
8464
|
+
// not an effort control.
|
|
8456
8465
|
const levels = getModelPickerReasoningLevels(providerId, modelId);
|
|
8457
8466
|
if (levels.length > 1 && thinkingLevel !== "off") {
|
|
8458
8467
|
return `${displayModel(model)} (${thinkingLevel})`;
|
|
@@ -8649,6 +8658,8 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
8649
8658
|
}
|
|
8650
8659
|
catch { }
|
|
8651
8660
|
const toolResult = agentMessages.find((candidate) => candidate.role === "tool" && candidate.toolCallId === tc.id);
|
|
8661
|
+
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
8662
|
+
continue;
|
|
8652
8663
|
toolCalls.push({
|
|
8653
8664
|
id: tc.id,
|
|
8654
8665
|
name: tc.name,
|
|
@@ -9389,12 +9400,11 @@ function formatCompactNumber(value) {
|
|
|
9389
9400
|
return `${(value / 1_000).toFixed(1)}K`;
|
|
9390
9401
|
return String(value);
|
|
9391
9402
|
}
|
|
9392
|
-
function formatCurrency(value) {
|
|
9403
|
+
function formatCurrency(value, currency = "USD") {
|
|
9393
9404
|
if (value < 0.0001)
|
|
9394
|
-
return "$0.0000";
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
return `$${value.toFixed(2)}`;
|
|
9405
|
+
return currency === "USD" ? "$0.0000" : "CNY 0.0000";
|
|
9406
|
+
const amount = value < 1 ? value.toFixed(4) : value.toFixed(2);
|
|
9407
|
+
return currency === "USD" ? `$${amount}` : `CNY ${amount}`;
|
|
9398
9408
|
}
|
|
9399
9409
|
function sidebarStatusColor(kind) {
|
|
9400
9410
|
if (kind === "connected")
|
package/dist/tui-ink/app.js
CHANGED
|
@@ -2,6 +2,7 @@ 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 } from "../agent.js";
|
|
5
|
+
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
5
6
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
6
7
|
import { UserConfig, maskKey } from "../config.js";
|
|
7
8
|
import { createPastedContentMarker, InputBox, isCtrlCInput, shouldCollapsePastedContent, } from "./input-box.js";
|
|
@@ -82,6 +83,8 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
82
83
|
args = {};
|
|
83
84
|
}
|
|
84
85
|
const toolResult = agentMessages.find((tm) => tm.role === "tool" && tm.toolCallId === tc.id);
|
|
86
|
+
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
87
|
+
continue;
|
|
85
88
|
toolCalls.push({
|
|
86
89
|
id: tc.id,
|
|
87
90
|
name: tc.name,
|
|
@@ -7,6 +7,7 @@ import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
|
|
|
7
7
|
import { buildTraceGroups, formatTracePath, traceGroupLabel } from "./trace-groups.js";
|
|
8
8
|
import { EDIT_COLLAPSED_DIFF_LINES, formatEditSuccessSummary, getEditDiffDetails } from "./edit-diff.js";
|
|
9
9
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
10
|
+
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
10
11
|
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, verboseTrace, pendingApproval, nowTick, welcomeBanner, }) {
|
|
11
12
|
const hasStreaming = !!(streamingContent ||
|
|
12
13
|
streamingReasoning ||
|
|
@@ -44,22 +45,24 @@ function MessageItem({ message, terminalColumns, verboseTrace, showExpandHint, n
|
|
|
44
45
|
if (message.syntheticKind === "ui_compact_summary") {
|
|
45
46
|
return _jsx(CompactionSummaryBlock, { message: message });
|
|
46
47
|
}
|
|
48
|
+
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
47
49
|
const hasVisibleAssistantContent = !!message.content ||
|
|
48
50
|
(message.toolCalls?.length ?? 0) > 0 ||
|
|
49
51
|
(message.parts?.length ?? 0) > 0 ||
|
|
50
|
-
(!!
|
|
52
|
+
(!!visibleReasoning && verboseTrace);
|
|
51
53
|
if (!hasVisibleAssistantContent)
|
|
52
54
|
return null;
|
|
53
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [
|
|
55
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && verboseTrace && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), 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 })), message.content && _jsx(MarkdownContent, { content: message.content })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
54
56
|
}
|
|
55
57
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, pendingApproval, nowTick, }) {
|
|
56
58
|
const deferredContent = React.useDeferredValue(content);
|
|
57
59
|
const deferredReasoning = React.useDeferredValue(reasoning);
|
|
58
60
|
const deferredParts = React.useDeferredValue(parts);
|
|
61
|
+
const visibleReasoning = sanitizeInternalReminderBlocks(deferredReasoning).trim();
|
|
59
62
|
const visibleParts = deferredParts.length > 0
|
|
60
63
|
? deferredParts
|
|
61
64
|
: fallbackStreamingParts(deferredContent, tools);
|
|
62
|
-
return (_jsxs(Box, { flexDirection: "column", children: [
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visibleReasoning && verboseTrace && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }) })), visibleParts.length > 0 && (
|
|
63
66
|
// marginTop intentionally 0: this Box only mounts on the first non-empty
|
|
64
67
|
// streaming frame, so a marginTop=1 here would visibly insert a blank
|
|
65
68
|
// line under the user message right at that moment (the "spinner sits
|
package/dist/tui-opentui/app.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
5
5
|
import { AgentAbortError } from "../agent.js";
|
|
6
|
+
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
6
7
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
7
8
|
import { UserConfig, maskKey } from "../config.js";
|
|
8
9
|
import { createPastedContentMarker, InputBox, shouldCollapsePastedContent, } from "./input-box.js";
|
|
@@ -96,6 +97,8 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
96
97
|
args = {};
|
|
97
98
|
}
|
|
98
99
|
const toolResult = agentMessages.find((tm) => tm.role === "tool" && tm.toolCallId === tc.id);
|
|
100
|
+
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
101
|
+
continue;
|
|
99
102
|
toolCalls.push({
|
|
100
103
|
id: tc.id,
|
|
101
104
|
name: tc.name,
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
|
|
|
2
2
|
import { useTheme } from "./theme.js";
|
|
3
3
|
import { MarkdownContent, StreamingMarkdown } from "./markdown.js";
|
|
4
4
|
import { EDIT_COLLAPSED_DIFF_LINES, getEditDiffDetails } from "./edit-diff.js";
|
|
5
|
+
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
5
6
|
/**
|
|
6
7
|
* Scrollback-style message list following opencode visual rules:
|
|
7
8
|
*
|
|
@@ -29,18 +30,20 @@ function MessageItem({ message, terminalColumns, verboseTrace, }) {
|
|
|
29
30
|
if (message.syntheticKind === "ui_compact_summary") {
|
|
30
31
|
return _jsx(CompactionSummaryBlock, { message: message, theme: theme });
|
|
31
32
|
}
|
|
33
|
+
const visibleReasoning = sanitizeInternalReminderBlocks(message.reasoning ?? "").trim();
|
|
32
34
|
const hasVisible = !!message.content ||
|
|
33
35
|
(message.toolCalls?.length ?? 0) > 0 ||
|
|
34
36
|
(message.parts?.length ?? 0) > 0 ||
|
|
35
|
-
(!!
|
|
37
|
+
(!!visibleReasoning && verboseTrace);
|
|
36
38
|
if (!hasVisible)
|
|
37
39
|
return null;
|
|
38
|
-
return (_jsxs("box", { style: { marginTop: 1, marginBottom: 1, flexDirection: "column" }, children: [
|
|
40
|
+
return (_jsxs("box", { style: { marginTop: 1, marginBottom: 1, flexDirection: "column" }, children: [visibleReasoning && verboseTrace && _jsx(ReasoningBlock, { reasoning: visibleReasoning, theme: theme }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, theme: theme })) : (_jsxs(_Fragment, { children: [message.toolCalls && _jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, theme: theme }), message.content && _jsx(MarkdownContent, { content: message.content, terminalColumns: terminalColumns })] }))] }));
|
|
39
41
|
}
|
|
40
42
|
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, verboseTrace, }) {
|
|
41
43
|
const theme = useTheme();
|
|
44
|
+
const visibleReasoning = sanitizeInternalReminderBlocks(reasoning).trim();
|
|
42
45
|
const visibleParts = parts.length > 0 ? parts : fallbackStreamingParts(content, tools);
|
|
43
|
-
return (_jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [
|
|
46
|
+
return (_jsxs("box", { style: { flexDirection: "column", marginTop: 1 }, children: [visibleReasoning && verboseTrace && _jsx(ReasoningBlock, { reasoning: visibleReasoning, theme: theme }), visibleParts.length > 0 && (_jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, streaming: true, theme: theme }))] }));
|
|
44
47
|
}
|
|
45
48
|
function fallbackStreamingParts(content, tools) {
|
|
46
49
|
const out = [];
|
package/dist/types.d.ts
CHANGED
|
@@ -102,7 +102,7 @@ export interface ParsedToolCall extends ToolCall {
|
|
|
102
102
|
}
|
|
103
103
|
export type ToolResultStatus = "success" | "no_match" | "partial" | "timeout" | "blocked" | "cancelled" | "command_error";
|
|
104
104
|
export interface ToolResultMetadata {
|
|
105
|
-
kind?: "search" | "read" | "write" | "edit" | "patch" | "shell" | "server" | "web" | "security" | "lsp" | "question" | "subagent";
|
|
105
|
+
kind?: "search" | "read" | "write" | "edit" | "patch" | "shell" | "server" | "web" | "security" | "lsp" | "question" | "subagent" | "internal";
|
|
106
106
|
path?: string;
|
|
107
107
|
pattern?: string;
|
|
108
108
|
matches?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bubblebrain-ai/bubble",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
4
4
|
"description": "A terminal coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"solid-js": "^1.9.12",
|
|
45
45
|
"string-width": "^8.2.1",
|
|
46
46
|
"typescript-language-server": "^5.1.3",
|
|
47
|
+
"undici": "^6.26.0",
|
|
47
48
|
"vscode-jsonrpc": "^8.2.1",
|
|
48
49
|
"vscode-langservers-extracted": "^4.10.0"
|
|
49
50
|
},
|