@bubblebrain-ai/bubble 0.0.14 → 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/cli.d.ts +3 -1
- package/dist/cli.js +12 -0
- package/dist/context/projector.js +4 -3
- package/dist/main.js +13 -0
- 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.d.ts +2 -0
- package/dist/tui/run.js +104 -42
- 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/dist/update/index.d.ts +46 -0
- package/dist/update/index.js +240 -0
- package/package.json +2 -1
package/dist/session-log.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeInternalReminderBlocks } from "./agent/internal-reminder-sanitizer.js";
|
|
1
2
|
export class SessionLog {
|
|
2
3
|
entries = [];
|
|
3
4
|
load(lines) {
|
|
@@ -190,7 +191,9 @@ function normalizeMessageToEntries(message, id, timestamp) {
|
|
|
190
191
|
message: {
|
|
191
192
|
role: "assistant",
|
|
192
193
|
content: message.content,
|
|
193
|
-
reasoning: message.reasoning
|
|
194
|
+
reasoning: message.reasoning !== undefined
|
|
195
|
+
? sanitizeInternalReminderBlocks(message.reasoning)
|
|
196
|
+
: undefined,
|
|
194
197
|
model: message.model,
|
|
195
198
|
providerId: message.providerId,
|
|
196
199
|
modelId: message.modelId,
|
package/dist/stats/usage.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PricingCurrency } from "../model-pricing.js";
|
|
1
2
|
export type StatsRange = "7d" | "30d";
|
|
2
3
|
export interface DailyUsage {
|
|
3
4
|
date: string;
|
|
@@ -22,6 +23,7 @@ export interface ModelUsageStats {
|
|
|
22
23
|
reasoningTokens: number;
|
|
23
24
|
totalTokens: number;
|
|
24
25
|
cost?: number;
|
|
26
|
+
costCurrency?: PricingCurrency;
|
|
25
27
|
}
|
|
26
28
|
export interface UsageStats {
|
|
27
29
|
range: StatsRange;
|
|
@@ -32,7 +34,9 @@ export interface UsageStats {
|
|
|
32
34
|
heatmap: HeatmapColumn[];
|
|
33
35
|
models: ModelUsageStats[];
|
|
34
36
|
totalTokens: number;
|
|
37
|
+
trackedCosts?: Partial<Record<PricingCurrency, number>>;
|
|
35
38
|
trackedCost?: number;
|
|
39
|
+
trackedCostCurrency?: PricingCurrency;
|
|
36
40
|
activeDays: number;
|
|
37
41
|
sessionsScanned: number;
|
|
38
42
|
sessionsWithoutTokenData: number;
|
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.d.ts
CHANGED
|
@@ -41,5 +41,7 @@ export interface RunTuiOptions {
|
|
|
41
41
|
runMemoryCompaction?: () => Promise<string>;
|
|
42
42
|
runMemorySummary?: (scope?: MemoryScope) => Promise<string>;
|
|
43
43
|
runMemoryRefresh?: (scope?: MemoryScope) => Promise<string>;
|
|
44
|
+
/** One-line "update available" notice shown on the home screen, if any. */
|
|
45
|
+
updateNotice?: string;
|
|
44
46
|
}
|
|
45
47
|
export declare function runTui(agent: Agent, args: CliArgs, options?: RunTuiOptions): Promise<void>;
|
package/dist/tui/run.js
CHANGED
|
@@ -9,10 +9,13 @@ 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";
|
|
15
17
|
import { getAvailableThinkingLevels } from "../provider-transform.js";
|
|
18
|
+
import { getCurrentVersion } from "../update/index.js";
|
|
16
19
|
import { collectUsageStatsBundle, formatStatsPanelBody } from "../stats/usage.js";
|
|
17
20
|
import { parseSkillInvocation } from "../skills/invocation.js";
|
|
18
21
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
@@ -59,6 +62,7 @@ const PROVIDER_PRIORITY = new Map([
|
|
|
59
62
|
["zai", 5],
|
|
60
63
|
["zai-coding-plan", 6],
|
|
61
64
|
["kimi-for-coding", 7],
|
|
65
|
+
["stepfun", 8],
|
|
62
66
|
]);
|
|
63
67
|
const DEFAULT_THEME = {
|
|
64
68
|
primary: "#fab283",
|
|
@@ -181,7 +185,7 @@ const PROMPT_SCANNER_IDLE_FRAMES = [" "];
|
|
|
181
185
|
const PROMPT_SCANNER_INTERVAL_MS = 80;
|
|
182
186
|
const SESSION_SIDEBAR_WIDTH = 42;
|
|
183
187
|
const SESSION_SIDEBAR_AUTO_WIDTH = 120;
|
|
184
|
-
const PROVIDER_DIALOG_ROWS =
|
|
188
|
+
const PROVIDER_DIALOG_ROWS = 13;
|
|
185
189
|
const QUESTION_MAX_TABS = 4;
|
|
186
190
|
const QUESTION_MAX_OPTIONS = 10;
|
|
187
191
|
const QUESTION_MAX_CONFIRM_ROWS = 3;
|
|
@@ -1104,7 +1108,7 @@ function OpenTuiApp(props) {
|
|
|
1104
1108
|
if (!safeSetText(ref, promptModeBadge()))
|
|
1105
1109
|
promptModeLabels.delete(ref);
|
|
1106
1110
|
};
|
|
1107
|
-
const promptModelTitle = () =>
|
|
1111
|
+
const promptModelTitle = () => displayModelWithThinking(props.agent.model, props.agent.thinking) || "no model";
|
|
1108
1112
|
const syncModelChrome = () => {
|
|
1109
1113
|
if (uiDisposed)
|
|
1110
1114
|
return;
|
|
@@ -3046,6 +3050,31 @@ function OpenTuiApp(props) {
|
|
|
3046
3050
|
// Keep the already-rendered local catalog when remote model discovery fails.
|
|
3047
3051
|
}
|
|
3048
3052
|
}
|
|
3053
|
+
function providerDialogMatchScore(item, query) {
|
|
3054
|
+
const label = (item.label || "").toLowerCase();
|
|
3055
|
+
const value = (item.value || "").toLowerCase();
|
|
3056
|
+
const haystack = [
|
|
3057
|
+
item.label,
|
|
3058
|
+
item.detail,
|
|
3059
|
+
item.value,
|
|
3060
|
+
item.category,
|
|
3061
|
+
item.footer,
|
|
3062
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
3063
|
+
if (label.startsWith(query))
|
|
3064
|
+
return 100;
|
|
3065
|
+
if (label.includes(query))
|
|
3066
|
+
return 80;
|
|
3067
|
+
if (value.includes(query))
|
|
3068
|
+
return 60;
|
|
3069
|
+
if (haystack.includes(query))
|
|
3070
|
+
return 40;
|
|
3071
|
+
// Fuzzy (subsequence) match is a last resort, and only against label+value
|
|
3072
|
+
// so long provider descriptions (e.g. "platform.moonshot.cn") don't produce
|
|
3073
|
+
// spurious hits like "gpt" matching "kimi-k2-thinking".
|
|
3074
|
+
if (fuzzyMatch(`${label} ${value}`, query))
|
|
3075
|
+
return 20;
|
|
3076
|
+
return 0;
|
|
3077
|
+
}
|
|
3049
3078
|
function providerDialogFilteredItems(state = providerDialog) {
|
|
3050
3079
|
if (!state || state.step === "key")
|
|
3051
3080
|
return [];
|
|
@@ -3053,16 +3082,12 @@ function OpenTuiApp(props) {
|
|
|
3053
3082
|
const query = state.query.trim().toLowerCase();
|
|
3054
3083
|
if (!query)
|
|
3055
3084
|
return items;
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
item.footer,
|
|
3063
|
-
].filter(Boolean).join(" ").toLowerCase();
|
|
3064
|
-
return haystack.includes(query) || fuzzyMatch(haystack, query);
|
|
3065
|
-
});
|
|
3085
|
+
const scored = items
|
|
3086
|
+
.map((item, order) => ({ item, order, score: providerDialogMatchScore(item, query) }))
|
|
3087
|
+
.filter((entry) => entry.score > 0);
|
|
3088
|
+
// Stable sort by score desc, preserving original catalog order within a tier.
|
|
3089
|
+
scored.sort((a, b) => b.score - a.score || a.order - b.order);
|
|
3090
|
+
return scored.map((entry) => entry.item);
|
|
3066
3091
|
}
|
|
3067
3092
|
function providerDialogVisibleRows(state = providerDialog) {
|
|
3068
3093
|
if (!state)
|
|
@@ -3103,7 +3128,7 @@ function OpenTuiApp(props) {
|
|
|
3103
3128
|
providerDialogRoot.requestRender();
|
|
3104
3129
|
return;
|
|
3105
3130
|
}
|
|
3106
|
-
const width = Math.max(
|
|
3131
|
+
const width = Math.max(56, Math.min(76, dimensions().width - 4));
|
|
3107
3132
|
const height = PROVIDER_DIALOG_ROWS + 7;
|
|
3108
3133
|
providerDialogRoot.visible = true;
|
|
3109
3134
|
providerDialogRoot.width = dimensions().width;
|
|
@@ -5053,6 +5078,7 @@ function OpenTuiApp(props) {
|
|
|
5053
5078
|
"zhipuai-coding-plan": "Coding Plan",
|
|
5054
5079
|
"zai-coding-plan": "Coding Plan",
|
|
5055
5080
|
"kimi-for-coding": "Coding Plan",
|
|
5081
|
+
stepfun: "Step Plan API key",
|
|
5056
5082
|
local: "OpenAI-compatible local endpoint",
|
|
5057
5083
|
};
|
|
5058
5084
|
return descriptions[providerId] ?? "API key";
|
|
@@ -5494,6 +5520,10 @@ function OpenTuiApp(props) {
|
|
|
5494
5520
|
paddingRight: 2,
|
|
5495
5521
|
}, [
|
|
5496
5522
|
h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, ...logoLines.map((line) => renderHomeLogoLine(line))),
|
|
5523
|
+
h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center", paddingTop: 1 }, h("text", { fg: theme.textMuted, content: `v${getCurrentVersion()}` })),
|
|
5524
|
+
...(props.options.updateNotice
|
|
5525
|
+
? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
|
|
5526
|
+
: []),
|
|
5497
5527
|
h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
|
|
5498
5528
|
h("box", {
|
|
5499
5529
|
ref: (ref) => {
|
|
@@ -5996,7 +6026,8 @@ function OpenTuiApp(props) {
|
|
|
5996
6026
|
width: "100%",
|
|
5997
6027
|
value: "",
|
|
5998
6028
|
placeholder: "",
|
|
5999
|
-
|
|
6029
|
+
textColor: theme.text,
|
|
6030
|
+
focusedTextColor: theme.text,
|
|
6000
6031
|
backgroundColor: theme.backgroundElement,
|
|
6001
6032
|
focusedBackgroundColor: theme.backgroundElement,
|
|
6002
6033
|
cursorColor: theme.primary,
|
|
@@ -6141,7 +6172,7 @@ function OpenTuiApp(props) {
|
|
|
6141
6172
|
},
|
|
6142
6173
|
visible: false,
|
|
6143
6174
|
position: "absolute",
|
|
6144
|
-
width:
|
|
6175
|
+
width: 76,
|
|
6145
6176
|
height: PROVIDER_DIALOG_ROWS + 7,
|
|
6146
6177
|
backgroundColor: theme.backgroundPanel,
|
|
6147
6178
|
flexDirection: "column",
|
|
@@ -6178,7 +6209,8 @@ function OpenTuiApp(props) {
|
|
|
6178
6209
|
width: "100%",
|
|
6179
6210
|
value: "",
|
|
6180
6211
|
placeholder: "Search",
|
|
6181
|
-
|
|
6212
|
+
textColor: theme.text,
|
|
6213
|
+
focusedTextColor: theme.text,
|
|
6182
6214
|
backgroundColor: theme.backgroundPanel,
|
|
6183
6215
|
focusedBackgroundColor: theme.backgroundPanel,
|
|
6184
6216
|
cursorColor: theme.primary,
|
|
@@ -6191,15 +6223,11 @@ function OpenTuiApp(props) {
|
|
|
6191
6223
|
providerDialog = { ...state, apiKey: value, error: undefined };
|
|
6192
6224
|
}
|
|
6193
6225
|
else {
|
|
6226
|
+
const query = value.trim().toLowerCase();
|
|
6194
6227
|
const items = providerDialogItemsFor(state.step, state.providerId).filter((item) => {
|
|
6195
|
-
const query = value.trim().toLowerCase();
|
|
6196
6228
|
if (!query)
|
|
6197
6229
|
return true;
|
|
6198
|
-
|
|
6199
|
-
.filter(Boolean)
|
|
6200
|
-
.join(" ")
|
|
6201
|
-
.toLowerCase();
|
|
6202
|
-
return haystack.includes(query) || fuzzyMatch(haystack, query);
|
|
6230
|
+
return providerDialogMatchScore(item, query) > 0;
|
|
6203
6231
|
});
|
|
6204
6232
|
providerDialog = {
|
|
6205
6233
|
...state,
|
|
@@ -6557,7 +6585,7 @@ function OpenTuiApp(props) {
|
|
|
6557
6585
|
completionTokens: usage.completionTokens,
|
|
6558
6586
|
reasoningTokens: usage.reasoningTokens,
|
|
6559
6587
|
turns: usage.turns,
|
|
6560
|
-
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",
|
|
6561
6589
|
};
|
|
6562
6590
|
}
|
|
6563
6591
|
function sidebarMcpStates() {
|
|
@@ -6926,12 +6954,14 @@ function renderUserMessage(message, index) {
|
|
|
6926
6954
|
}, h("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: theme.backgroundPanel, flexShrink: 0, flexDirection: "column" }, ...userChildren));
|
|
6927
6955
|
}
|
|
6928
6956
|
function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false, width = 80) {
|
|
6957
|
+
const visibleReasoning = showThinking
|
|
6958
|
+
? sanitizeInternalReminderBlocks(message.reasoning ?? "").trim()
|
|
6959
|
+
: "";
|
|
6929
6960
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
6930
|
-
if (modelSwitch && !
|
|
6961
|
+
if (modelSwitch && !visibleReasoning && !(message.toolCalls?.length)) {
|
|
6931
6962
|
return renderModelSwitchMessage(modelSwitch);
|
|
6932
6963
|
}
|
|
6933
6964
|
const children = [];
|
|
6934
|
-
const visibleReasoning = showThinking ? message.reasoning?.trim() : "";
|
|
6935
6965
|
const parts = message.parts ?? [];
|
|
6936
6966
|
const hasParts = parts.length > 0;
|
|
6937
6967
|
if (message.status && !visibleReasoning && !message.content.trim() && !(message.toolCalls?.length) && !hasParts) {
|
|
@@ -7054,7 +7084,7 @@ function renderMarkdownContent(content, syntaxStyle, options) {
|
|
|
7054
7084
|
bg: theme.background,
|
|
7055
7085
|
width: "100%",
|
|
7056
7086
|
tableOptions: {
|
|
7057
|
-
widthMode: "
|
|
7087
|
+
widthMode: "content",
|
|
7058
7088
|
columnFitter: "balanced",
|
|
7059
7089
|
wrapMode: "word",
|
|
7060
7090
|
cellPadding: 1,
|
|
@@ -7274,7 +7304,13 @@ function syncMarkdownRenderable(markdown, content, streaming) {
|
|
|
7274
7304
|
return;
|
|
7275
7305
|
markdown.content = content;
|
|
7276
7306
|
markdown.streaming = streaming;
|
|
7277
|
-
markdown
|
|
7307
|
+
// While streaming, let OpenTUI's incremental markdown/code-block rendering do
|
|
7308
|
+
// its job — clearing the parse cache every delta forces the (syntax-
|
|
7309
|
+
// highlighted) code blocks to be rebuilt and re-highlighted on every token,
|
|
7310
|
+
// which is the source of the visible flicker on streamed code blocks. Clear
|
|
7311
|
+
// the cache only once streaming ends, to fully reparse the finalized content.
|
|
7312
|
+
if (!streaming)
|
|
7313
|
+
markdown.clearCache();
|
|
7278
7314
|
}
|
|
7279
7315
|
function updateAssistantPartEntries(entry, parts, options, streaming) {
|
|
7280
7316
|
const partsBox = entry.refs.partsBox;
|
|
@@ -7613,7 +7649,7 @@ function createMarkdown(ctx, content, syntaxStyle, options) {
|
|
|
7613
7649
|
width: "100%",
|
|
7614
7650
|
flexShrink: 0,
|
|
7615
7651
|
tableOptions: {
|
|
7616
|
-
widthMode: "
|
|
7652
|
+
widthMode: "content",
|
|
7617
7653
|
columnFitter: "balanced",
|
|
7618
7654
|
wrapMode: "word",
|
|
7619
7655
|
cellPadding: 1,
|
|
@@ -8293,7 +8329,7 @@ function renderTool(tool, syntaxStyle, width = 80) {
|
|
|
8293
8329
|
const color = toolColor(tool);
|
|
8294
8330
|
const diff = extractToolDiff(tool);
|
|
8295
8331
|
if (diff && !tool.resultCollapsed && !tool.isError && (tool.name === "edit" || tool.name === "apply_patch")) {
|
|
8296
|
-
return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0 }, renderDiffContent(diff, toolPath(tool), syntaxStyle, width)));
|
|
8332
|
+
return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", { fg: color }, `${icon} ${displayToolName(tool.name)}${toolHeader(tool) ? ` ${toolHeader(tool)}` : ""}`), h("box", { paddingLeft: 1, marginTop: 1, border: ["left"], borderColor: theme.borderSubtle, flexDirection: "column", flexShrink: 0, backgroundColor: theme.diffContextBg }, renderDiffContent(diff, toolPath(tool), syntaxStyle, width)));
|
|
8297
8333
|
}
|
|
8298
8334
|
if (!tool.resultCollapsed && isWritePreviewTool(tool)) {
|
|
8299
8335
|
const hasContent = typeof tool.args.content === "string";
|
|
@@ -8401,10 +8437,20 @@ function pickerTitle(kind, providerId) {
|
|
|
8401
8437
|
}
|
|
8402
8438
|
}
|
|
8403
8439
|
function getModelPickerReasoningLevels(providerId, modelId) {
|
|
8404
|
-
|
|
8440
|
+
// Only expand into one picker row per effort for models that genuinely have a
|
|
8441
|
+
// reasoning-effort spectrum: OpenAI's reasoning models (codex gpt-5.x:
|
|
8442
|
+
// off/minimal/low/medium/high/xhigh), DeepSeek's v4 models, and StepFun
|
|
8443
|
+
// Step Plan models. Other providers
|
|
8444
|
+
// (e.g. GLM, Moonshot/Kimi) only have a thinking on/off toggle, not an effort
|
|
8445
|
+
// control, so they stay as a single row.
|
|
8446
|
+
const isOpenAIReasoning = providerId === "openai" || providerId === "openai-codex";
|
|
8447
|
+
const isDeepseekReasoning = providerId === "deepseek" && (modelId === "deepseek-v4-flash" || modelId === "deepseek-v4-pro");
|
|
8448
|
+
const isStepFunReasoning = providerId === "stepfun";
|
|
8449
|
+
if (!isOpenAIReasoning && !isDeepseekReasoning && !isStepFunReasoning)
|
|
8405
8450
|
return [];
|
|
8406
|
-
|
|
8407
|
-
|
|
8451
|
+
const levels = getAvailableThinkingLevels(providerId, modelId);
|
|
8452
|
+
// gpt-4o and friends report only ["off"] — keep those as a single row too.
|
|
8453
|
+
return levels.length > 1 ? levels : [];
|
|
8408
8454
|
}
|
|
8409
8455
|
function displayModelWithThinking(model, thinkingLevel) {
|
|
8410
8456
|
if (!model)
|
|
@@ -8412,7 +8458,11 @@ function displayModelWithThinking(model, thinkingLevel) {
|
|
|
8412
8458
|
const { providerId, modelId } = decodeModel(model);
|
|
8413
8459
|
if (!providerId)
|
|
8414
8460
|
return displayModel(model);
|
|
8415
|
-
|
|
8461
|
+
// Use the same scoping as the picker: only models with a real reasoning-effort
|
|
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.
|
|
8465
|
+
const levels = getModelPickerReasoningLevels(providerId, modelId);
|
|
8416
8466
|
if (levels.length > 1 && thinkingLevel !== "off") {
|
|
8417
8467
|
return `${displayModel(model)} (${thinkingLevel})`;
|
|
8418
8468
|
}
|
|
@@ -8608,6 +8658,8 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
8608
8658
|
}
|
|
8609
8659
|
catch { }
|
|
8610
8660
|
const toolResult = agentMessages.find((candidate) => candidate.role === "tool" && candidate.toolCallId === tc.id);
|
|
8661
|
+
if (isHiddenToolMetadata(toolResult ? toolResult.metadata : undefined))
|
|
8662
|
+
continue;
|
|
8611
8663
|
toolCalls.push({
|
|
8612
8664
|
id: tc.id,
|
|
8613
8665
|
name: tc.name,
|
|
@@ -9066,11 +9118,18 @@ function toolPath(tool) {
|
|
|
9066
9118
|
?? (Array.isArray(tool.metadata?.paths) ? tool.metadata.paths[0] : undefined);
|
|
9067
9119
|
return typeof value === "string" ? value : undefined;
|
|
9068
9120
|
}
|
|
9121
|
+
// Strip only leading/trailing newlines — NOT a full .trim(). A blank context
|
|
9122
|
+
// line in a unified diff is a single space (" "); plain .trim() would delete a
|
|
9123
|
+
// trailing blank context line, leaving the hunk body shorter than its @@ header
|
|
9124
|
+
// count and breaking the diff parser ("Added line count did not match").
|
|
9125
|
+
function stripDiffEdgeNewlines(diff) {
|
|
9126
|
+
return diff.replace(/^\n+/, "").replace(/\n+$/, "");
|
|
9127
|
+
}
|
|
9069
9128
|
function extractToolDiff(tool) {
|
|
9070
9129
|
if (tool.resultCollapsed)
|
|
9071
9130
|
return undefined;
|
|
9072
9131
|
if (typeof tool.metadata?.diff === "string" && tool.metadata.diff.trim().length > 0) {
|
|
9073
|
-
return tool.metadata.diff
|
|
9132
|
+
return stripDiffEdgeNewlines(tool.metadata.diff);
|
|
9074
9133
|
}
|
|
9075
9134
|
if (!tool.result)
|
|
9076
9135
|
return undefined;
|
|
@@ -9086,10 +9145,14 @@ function extractToolDiff(tool) {
|
|
|
9086
9145
|
const rawDiff = tool.result.slice(index + marker.length);
|
|
9087
9146
|
const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
|
|
9088
9147
|
const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
|
|
9089
|
-
return diff.trim().length > 0 ? diff : undefined;
|
|
9148
|
+
return diff.trim().length > 0 ? stripDiffEdgeNewlines(diff) : undefined;
|
|
9090
9149
|
}
|
|
9091
|
-
function diffViewMode(
|
|
9092
|
-
|
|
9150
|
+
function diffViewMode(_width = 80) {
|
|
9151
|
+
// Always unified: split view pads the shorter side with empty filler rows that
|
|
9152
|
+
// OpenTUI's DiffRenderable leaves uncolored, which shows up as bright white
|
|
9153
|
+
// blocks in light mode. Unified view has no filler rows — every line is
|
|
9154
|
+
// add/remove/context and gets a background — so the edit area stays uniform.
|
|
9155
|
+
return "unified";
|
|
9093
9156
|
}
|
|
9094
9157
|
function filetype(filePath) {
|
|
9095
9158
|
if (!filePath)
|
|
@@ -9337,12 +9400,11 @@ function formatCompactNumber(value) {
|
|
|
9337
9400
|
return `${(value / 1_000).toFixed(1)}K`;
|
|
9338
9401
|
return String(value);
|
|
9339
9402
|
}
|
|
9340
|
-
function formatCurrency(value) {
|
|
9403
|
+
function formatCurrency(value, currency = "USD") {
|
|
9341
9404
|
if (value < 0.0001)
|
|
9342
|
-
return "$0.0000";
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
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}`;
|
|
9346
9408
|
}
|
|
9347
9409
|
function sidebarStatusColor(kind) {
|
|
9348
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,
|