@bubblebrain-ai/bubble 0.0.46 → 0.0.47
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-registry.d.ts +13 -0
- package/dist/provider-registry.js +83 -0
- 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/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
|
@@ -987,7 +987,7 @@ const builtinSlashCommandEntries = [
|
|
|
987
987
|
},
|
|
988
988
|
{
|
|
989
989
|
name: "mcp",
|
|
990
|
-
description: "Manage MCP servers. Usage: /mcp [list|reconnect <name>]",
|
|
990
|
+
description: "Manage MCP servers. Usage: /mcp [list|tools <name>|reconnect <name>]",
|
|
991
991
|
async handler(args, ctx) {
|
|
992
992
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
993
993
|
const sub = tokens[0] ?? "list";
|
|
@@ -1009,46 +1009,60 @@ const builtinSlashCommandEntries = [
|
|
|
1009
1009
|
}
|
|
1010
1010
|
return `${name}: ${state.status.kind}`;
|
|
1011
1011
|
}
|
|
1012
|
+
if (sub === "tools") {
|
|
1013
|
+
const name = tokens[1];
|
|
1014
|
+
if (!name)
|
|
1015
|
+
return "Usage: /mcp tools <server-name>";
|
|
1016
|
+
const state = ctx.mcpManager.getStates().find((s) => s.name === name);
|
|
1017
|
+
if (!state)
|
|
1018
|
+
return `Unknown MCP server: ${name}`;
|
|
1019
|
+
if (state.status.kind !== "connected") {
|
|
1020
|
+
return `${name} is not connected — no tools to list. Try /mcp reconnect ${name}.`;
|
|
1021
|
+
}
|
|
1022
|
+
const lines = [`Tools from ${name} (${state.status.tools.length}):`, ""];
|
|
1023
|
+
for (const tool of state.status.tools) {
|
|
1024
|
+
lines.push(`- \`${tool.name}\`${tool.description ? ` — ${tool.description.replace(/\s+/g, " ").slice(0, 100)}` : ""}`);
|
|
1025
|
+
}
|
|
1026
|
+
return lines.join("\n");
|
|
1027
|
+
}
|
|
1012
1028
|
if (sub !== "list" && sub !== "") {
|
|
1013
|
-
return `Unknown /mcp subcommand "${sub}". Use /mcp list or /mcp reconnect <name>.`;
|
|
1029
|
+
return `Unknown /mcp subcommand "${sub}". Use /mcp list, /mcp tools <name>, or /mcp reconnect <name>.`;
|
|
1014
1030
|
}
|
|
1015
1031
|
const states = ctx.mcpManager.getStates();
|
|
1016
1032
|
if (states.length === 0) {
|
|
1017
1033
|
return "No MCP servers configured. Add entries under `mcpServers` in ~/.bubble/settings.json or <cwd>/.bubble/settings.json.";
|
|
1018
1034
|
}
|
|
1035
|
+
// Rendered as markdown in the TUI: each server is its own paragraph,
|
|
1036
|
+
// failures are bold + uppercase so they stand apart from healthy rows,
|
|
1037
|
+
// and tool lists stay collapsed behind /mcp tools <name>.
|
|
1019
1038
|
const lines = ["MCP servers:"];
|
|
1020
1039
|
for (const state of states) {
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1040
|
+
const meta = `${state.scope}/${state.config.type}`;
|
|
1041
|
+
lines.push("");
|
|
1023
1042
|
if (state.status.kind === "connected") {
|
|
1024
|
-
const info = state.status.serverInfo ? ` ${state.status.serverInfo.name}@${state.status.serverInfo.version}` : "";
|
|
1043
|
+
const info = state.status.serverInfo ? ` · ${state.status.serverInfo.name}@${state.status.serverInfo.version}` : "";
|
|
1025
1044
|
const tn = state.status.tools.length;
|
|
1026
1045
|
const pn = state.status.prompts.length;
|
|
1027
1046
|
const counts = [`${tn} tool${tn === 1 ? "" : "s"}`];
|
|
1028
1047
|
if (pn > 0)
|
|
1029
1048
|
counts.push(`${pn} prompt${pn === 1 ? "" : "s"}`);
|
|
1030
|
-
lines.push(
|
|
1031
|
-
for (const tool of state.status.tools) {
|
|
1032
|
-
lines.push(` · ${tool.name}${tool.description ? ` — ${tool.description.replace(/\s+/g, " ").slice(0, 80)}` : ""}`);
|
|
1033
|
-
}
|
|
1049
|
+
lines.push(`✔ ${state.name} — connected · ${counts.join(" · ")} (${meta}${info})`);
|
|
1034
1050
|
if (pn > 0) {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const argSig = p.arguments?.length
|
|
1038
|
-
? ` <${p.arguments.map((a) => (a.required ? a.name : `${a.name}?`)).join("> <")}>`
|
|
1039
|
-
: "";
|
|
1040
|
-
const cmdName = normalizeNameForMCP(p.name);
|
|
1041
|
-
lines.push(` · /${cmdName}${argSig}${p.description ? ` — ${p.description.replace(/\s+/g, " ").slice(0, 70)}` : ""}`);
|
|
1042
|
-
}
|
|
1051
|
+
const prompts = state.status.prompts.map((p) => `/${normalizeNameForMCP(p.name)}`);
|
|
1052
|
+
lines.push(` prompts: ${prompts.join(", ")}`);
|
|
1043
1053
|
}
|
|
1044
1054
|
}
|
|
1045
1055
|
else if (state.status.kind === "failed") {
|
|
1046
|
-
lines.push(
|
|
1056
|
+
lines.push(`**✘ ${state.name} — UNABLE TO CONNECT** (${meta})`);
|
|
1057
|
+
lines.push(` ${state.status.error.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
1058
|
+
lines.push(` retry: /mcp reconnect ${state.name}`);
|
|
1047
1059
|
}
|
|
1048
1060
|
else {
|
|
1049
|
-
lines.push(
|
|
1061
|
+
lines.push(`○ ${state.name} — disabled (${meta})`);
|
|
1050
1062
|
}
|
|
1051
1063
|
}
|
|
1064
|
+
lines.push("");
|
|
1065
|
+
lines.push("Details: /mcp tools <name> · /mcp reconnect <name>");
|
|
1052
1066
|
return lines.join("\n");
|
|
1053
1067
|
},
|
|
1054
1068
|
},
|
|
@@ -11,6 +11,14 @@ export function createExitPlanModeTool(controller) {
|
|
|
11
11
|
readOnly: true,
|
|
12
12
|
effect: "read",
|
|
13
13
|
requiresApproval: true,
|
|
14
|
+
// Outside plan mode the tool is not just useless, it is bait: models call
|
|
15
|
+
// it to "wrap up" an ordinary turn despite the description below saying
|
|
16
|
+
// not to (same failure seen with update_goal — wording loses to the
|
|
17
|
+
// trained close-out prior). Gating it out of the provider's function list
|
|
18
|
+
// removes the affordance; the predicate re-runs every model call, so
|
|
19
|
+
// entering plan mode surfaces it immediately. The runtime no-op in
|
|
20
|
+
// agent.ts stays as a backstop for a mode flip mid-turn.
|
|
21
|
+
enabled: () => controller.getMode() === "plan",
|
|
14
22
|
description: "ONLY call this tool when the harness has told you via a runtime reminder that plan mode is ACTIVE. " +
|
|
15
23
|
"Do NOT call it during ordinary work — in default mode you should just use the regular tools directly. " +
|
|
16
24
|
"In plan mode: after investigating, call this with a concrete step-by-step plan so the user can approve, edit, or reject. " +
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export type ResolvedTheme = "light" | "dark";
|
|
2
2
|
export declare function detectTerminalTheme(timeoutMs?: number): Promise<ResolvedTheme>;
|
|
3
|
+
export declare function parseColorFgBg(value: string | undefined): ResolvedTheme | null;
|
|
3
4
|
export declare function themeFromMacOsAppearance(output: string | null | undefined): ResolvedTheme;
|
package/dist/tui/detect-theme.js
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
export async function detectTerminalTheme(timeoutMs =
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
export async function detectTerminalTheme(timeoutMs = 300) {
|
|
3
|
+
// OSC 11 asks the terminal for its actual background color right now — the
|
|
4
|
+
// only signal that tracks a theme switched after the shell started. The
|
|
5
|
+
// launch-time signals below are tie-breakers for terminals that don't
|
|
6
|
+
// answer: COLORFGBG is frozen at shell startup (and propagated stale by
|
|
7
|
+
// tmux/ssh), and the OS appearance describes the system, not the terminal
|
|
8
|
+
// profile. Ordering OSC 11 first is what keeps "auto" matching reality.
|
|
6
9
|
if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
7
10
|
const fromOsc = await queryOsc11(timeoutMs);
|
|
8
11
|
if (fromOsc)
|
|
9
12
|
return fromOsc;
|
|
10
13
|
}
|
|
14
|
+
const fromEnv = parseColorFgBg(process.env.COLORFGBG);
|
|
15
|
+
if (fromEnv)
|
|
16
|
+
return fromEnv;
|
|
11
17
|
const fromOs = detectOsAppearanceTheme();
|
|
12
18
|
if (fromOs)
|
|
13
19
|
return fromOs;
|
|
14
20
|
return "dark";
|
|
15
21
|
}
|
|
16
|
-
function parseColorFgBg(value) {
|
|
22
|
+
export function parseColorFgBg(value) {
|
|
17
23
|
if (!value)
|
|
18
24
|
return null;
|
|
19
25
|
const parts = value.split(";");
|
package/dist/tui-ink/app.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { QuestionController } from "../question/index.js";
|
|
|
15
15
|
import type { MemoryScope } from "../memory/index.js";
|
|
16
16
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
17
17
|
import type { GoalStore } from "../goal/store.js";
|
|
18
|
+
import { isInternalBlockOnlyContent } from "../agent/internal-reminder-sanitizer.js";
|
|
18
19
|
import type { ProcessManager } from "../tasks/manager.js";
|
|
19
20
|
import type { PromotionChannel } from "../tasks/promotion.js";
|
|
20
21
|
import type { ExternalRuntimeManager } from "../external-runtime/types.js";
|
|
@@ -70,11 +71,7 @@ export interface ExitSummary {
|
|
|
70
71
|
/** Wall-clock duration of the session, in milliseconds. */
|
|
71
72
|
wallMs: number;
|
|
72
73
|
}
|
|
73
|
-
|
|
74
|
-
* True when a user message is nothing but harness-injected internal blocks
|
|
75
|
-
* (goal kicks, task wakes) — hidden live, so hidden on reconstruction too.
|
|
76
|
-
*/
|
|
77
|
-
export declare function isInternalBlockOnlyContent(content: Message["content"]): boolean;
|
|
74
|
+
export { isInternalBlockOnlyContent };
|
|
78
75
|
export declare function reconstructDisplayMessages(agentMessages: Message[]): DisplayMessage[];
|
|
79
76
|
export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
|
|
80
77
|
readonly name: "goal";
|
|
@@ -84,4 +81,3 @@ export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
|
|
|
84
81
|
readonly description: "Run a prompt on a recurring interval (/loop 5m <prompt>|list|stop [id])";
|
|
85
82
|
}];
|
|
86
83
|
export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, processManager, tasksAutoResume, promotionChannel, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, externalRuntime, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
|
|
87
|
-
export {};
|
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;
|
|
@@ -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;
|