@bubblebrain-ai/bubble 0.0.29 → 0.0.30

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 CHANGED
@@ -66,7 +66,6 @@ Bubble ships with a catalog of built-in providers. Configure them inside the app
66
66
  | --- | --- |
67
67
  | `/login` | OAuth sign-in for ChatGPT; unlocks the OpenAI Codex models without an API key. |
68
68
  | `/provider` | Open a picker to connect, switch, add, or remove a provider. |
69
- | `/key <provider> <key>` | Set the API key for a provider. |
70
69
  | `/model` | Pick the active model and reasoning effort. |
71
70
 
72
71
  Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, Doubao (Volcengine Ark), MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
@@ -155,7 +154,7 @@ Rules use a simple pattern syntax, for example:
155
154
  | --- | --- |
156
155
  | `/help` | List available commands. |
157
156
  | `/model` | Switch model and reasoning effort. |
158
- | `/provider`, `/login`, `/logout`, `/key` | Connect and manage providers. |
157
+ | `/provider`, `/login`, `/logout` | Connect and manage providers. |
159
158
  | `/session`, `/rewind`, `/clear` | Manage conversation history. |
160
159
  | `/skills` | Open the searchable skills picker. |
161
160
  | `/mcp` | List or reconnect MCP servers. |
@@ -163,7 +162,7 @@ Rules use a simple pattern syntax, for example:
163
162
  | `/permissions` | View or edit allow/deny rules. |
164
163
  | `/context`, `/stats`, `/compact` | Inspect context usage, model stats, and compact the session. |
165
164
  | `/lsp`, `/hooks` | Manage language servers and lifecycle hooks. |
166
- | `/theme`, `/sidebar` | Adjust the interface. |
165
+ | `/theme` | Adjust the interface theme. |
167
166
  | `/feedback` | Send feedback or report a bug. |
168
167
 
169
168
  ## Non-interactive mode
@@ -14,5 +14,5 @@ export declare function goalSummaryText(goal: GoalState): string;
14
14
  * update_goal tool can't report this — see goal/tools.ts).
15
15
  */
16
16
  export declare function goalCompleteNotice(goal: GoalState): string;
17
- /** Compact single-line indicator for the status line / sidebar. */
17
+ /** Compact single-line indicator for status surfaces. */
18
18
  export declare function goalIndicatorLine(goal: GoalState, maxObjective?: number): string;
@@ -95,7 +95,7 @@ function completionTokenUsagePhrase(goal) {
95
95
  ? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok used`
96
96
  : `${formatTokensCompact(goal.tokensUsed)} tok used`;
97
97
  }
98
- /** Compact single-line indicator for the status line / sidebar. */
98
+ /** Compact single-line indicator for status surfaces. */
99
99
  export function goalIndicatorLine(goal, maxObjective = 48) {
100
100
  const segments = [`goal: ${goalStatusLabel(goal.status)}`, `${goal.turnsSpent} turns`];
101
101
  const tokens = tokensPart(goal);
@@ -1,4 +1,4 @@
1
- import { UserConfig, maskKey } from "../config.js";
1
+ import { UserConfig } from "../config.js";
2
2
  import { formatContextUsage } from "../context/usage.js";
3
3
  import { formatDiagnostics } from "../lsp/index.js";
4
4
  import { normalizeNameForMCP } from "../mcp/name.js";
@@ -8,7 +8,6 @@ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
8
8
  import { SessionManager } from "../session.js";
9
9
  import { buildSystemPrompt } from "../system-prompt.js";
10
10
  import { normalizeSingleLine } from "../text-display.js";
11
- import { copyToClipboard } from "../clipboard.js";
12
11
  import { formatRelativeTime } from "../tui/recent-activity.js";
13
12
  import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
14
13
  import { isThinkingLevel } from "../variant/thinking-level.js";
@@ -287,17 +286,6 @@ async function handleMemoryCommand(args, ctx) {
287
286
  }
288
287
  return "Usage: /memory [status|search|compact|summarize|refresh|reset]";
289
288
  }
290
- function parseKeyArgs(args, ctx) {
291
- const trimmed = args.trim();
292
- const [first, ...rest] = trimmed.split(/\s+/);
293
- const explicitProvider = first
294
- ? ctx.registry.getConfigured().find((provider) => provider.id === first)
295
- : undefined;
296
- if (explicitProvider) {
297
- return { provider: explicitProvider, apiKey: rest.join(" ") };
298
- }
299
- return { provider: ctx.registry.getDefault(), apiKey: trimmed };
300
- }
301
289
  const builtinSlashCommandEntries = [
302
290
  {
303
291
  name: "skills",
@@ -306,13 +294,6 @@ const builtinSlashCommandEntries = [
306
294
  ctx.openPicker("skill");
307
295
  },
308
296
  },
309
- {
310
- name: "agents",
311
- description: "Inspect spawned subagents and their working traces (also Ctrl+G)",
312
- async handler(_args, ctx) {
313
- ctx.openPicker("agents");
314
- },
315
- },
316
297
  {
317
298
  name: "help",
318
299
  description: "Show available slash commands",
@@ -370,33 +351,6 @@ const builtinSlashCommandEntries = [
370
351
  return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
371
352
  },
372
353
  },
373
- {
374
- name: "sidebar",
375
- description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
376
- async handler(args, ctx) {
377
- if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
378
- return "Sidebar control is only available inside the TUI.";
379
- }
380
- const arg = args.trim().toLowerCase();
381
- if (!arg) {
382
- ctx.toggleSidebar();
383
- return;
384
- }
385
- if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
386
- ctx.setSidebarMode("expanded");
387
- return;
388
- }
389
- if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
390
- ctx.setSidebarMode("collapsed");
391
- return;
392
- }
393
- if (arg === "auto") {
394
- ctx.setSidebarMode("auto");
395
- return;
396
- }
397
- return "Usage: /sidebar [open|close|auto]";
398
- },
399
- },
400
354
  {
401
355
  name: "clear",
402
356
  description: "Clear the current conversation history",
@@ -410,27 +364,6 @@ const builtinSlashCommandEntries = [
410
364
  ctx.clearMessages();
411
365
  },
412
366
  },
413
- {
414
- name: "copy",
415
- description: "Copy the last assistant message to the system clipboard",
416
- async handler(args, ctx) {
417
- const lastAssistant = [...ctx.agent.messages]
418
- .reverse()
419
- .find((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim().length > 0);
420
- if (!lastAssistant || typeof lastAssistant.content !== "string") {
421
- return "No assistant message to copy yet.";
422
- }
423
- const text = lastAssistant.content;
424
- try {
425
- await copyToClipboard(text);
426
- }
427
- catch (err) {
428
- return `Failed to copy to clipboard: ${err?.message || String(err)}`;
429
- }
430
- const chars = text.length;
431
- return `Copied last assistant message to clipboard (${chars} character${chars === 1 ? "" : "s"}).`;
432
- },
433
- },
434
367
  {
435
368
  name: "rewind",
436
369
  description: "Rewind conversation and/or file edits to before an earlier message. Usage: /rewind [n] [--code|--chat]",
@@ -674,31 +607,6 @@ const builtinSlashCommandEntries = [
674
607
  return `Model switched to ${displaySelectedModel(next, ctx.agent.thinking)}.`;
675
608
  },
676
609
  },
677
- {
678
- name: "key",
679
- description: "Set API key for the current or a specific provider. Usage: /key [provider-id] <key>",
680
- async handler(args, ctx) {
681
- if (!args) {
682
- ctx.openPicker("key");
683
- return;
684
- }
685
- const { provider, apiKey } = parseKeyArgs(args, ctx);
686
- if (!provider) {
687
- return "No provider configured. Use /provider --add <id> first.";
688
- }
689
- if (!apiKey) {
690
- return `Usage: /key ${provider.id} <key>`;
691
- }
692
- if (ctx.registry.getModelConfig().hasProvider(provider.id)) {
693
- return `API key for ${provider.name} is managed in ~/.bubble/models.json. Please edit that file directly.`;
694
- }
695
- ctx.registry.updateProviderKey(provider.id, apiKey);
696
- ctx.registry.setDefault(provider.id);
697
- ctx.agent.setProvider(ctx.createProvider(provider.id, apiKey, provider.baseURL));
698
- ctx.agent.providerId = provider.id;
699
- return `API key updated for ${provider.name} to ${maskKey(apiKey)}.`;
700
- },
701
- },
702
610
  {
703
611
  name: "logout",
704
612
  description: "Remove OAuth credentials for a provider. Usage: /logout [openai]",
@@ -739,32 +647,6 @@ const builtinSlashCommandEntries = [
739
647
  : "Exited plan mode.";
740
648
  },
741
649
  },
742
- {
743
- name: "todos",
744
- description: "Show the current todo list. Use /todos clear to reset it.",
745
- async handler(args, ctx) {
746
- const sub = args.trim();
747
- if (sub === "clear") {
748
- const previous = ctx.agent.getTodos().length;
749
- if (previous === 0) {
750
- return "Todo list is already empty.";
751
- }
752
- ctx.agent.setTodos([]);
753
- return `Cleared ${previous} todo item${previous === 1 ? "" : "s"}.`;
754
- }
755
- const todos = ctx.agent.getTodos();
756
- if (todos.length === 0) {
757
- return "No todos yet. The assistant will create some when working on multi-step tasks.";
758
- }
759
- const glyph = (status) => status === "completed" ? "✔" : status === "in_progress" ? "▶" : "○";
760
- const lines = ["Todos:"];
761
- for (const todo of todos) {
762
- const label = todo.status === "in_progress" ? (todo.activeForm || todo.content) : todo.content;
763
- lines.push(` ${glyph(todo.status)} ${label}`);
764
- }
765
- return lines.join("\n");
766
- },
767
- },
768
650
  {
769
651
  name: "permissions",
770
652
  description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session allowlist), reload.",
@@ -10,7 +10,6 @@ import type { LspService } from "../lsp/index.js";
10
10
  import type { MemoryScope } from "../memory/index.js";
11
11
  import type { ThemeMode } from "../config.js";
12
12
  import type { ExternalHookController } from "../hooks/controller.js";
13
- export type SidebarMode = "auto" | "expanded" | "collapsed";
14
13
  /**
15
14
  * Live progress for a manual `/compact` run, pushed to the TUI so it can render
16
15
  * a progress bar. `phase` advances collecting → summarizing → applying;
@@ -21,11 +20,6 @@ export interface CompactionProgress {
21
20
  phase: "collecting" | "summarizing" | "applying";
22
21
  streamedChars: number;
23
22
  }
24
- export interface SidebarCommandState {
25
- mode: SidebarMode;
26
- visible: boolean;
27
- active: boolean;
28
- }
29
23
  export interface SlashCommandContext {
30
24
  agent: Agent;
31
25
  addMessage: (role: "user" | "assistant" | "error", content: string) => void;
@@ -34,7 +28,7 @@ export interface SlashCommandContext {
34
28
  exit: () => void;
35
29
  sessionManager?: SessionManager;
36
30
  createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
37
- openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup" | "agents", providerId?: string) => void;
31
+ openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup", providerId?: string) => void;
38
32
  registry: ProviderRegistry;
39
33
  skillRegistry: SkillRegistry;
40
34
  bashAllowlist?: BashAllowlist;
@@ -52,10 +46,6 @@ export interface SlashCommandContext {
52
46
  getResolvedTheme?: () => "light" | "dark";
53
47
  /** Persist a new theme mode AND apply it to the running TUI. */
54
48
  setThemeMode?: (mode: ThemeMode) => void;
55
- /** Toggle the right session sidebar in the running TUI. */
56
- toggleSidebar?: () => SidebarCommandState;
57
- /** Set the right session sidebar mode in the running TUI. */
58
- setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
59
49
  /** Open the feedback dialog. `initialDescription` prefills the description field. */
60
50
  openFeedback?: (initialDescription: string) => void;
61
51
  /** Open the interactive rewind picker. When absent, /rewind falls back to a text listing. */
@@ -60,26 +60,8 @@ export interface ExitSummary {
60
60
  wallMs: number;
61
61
  }
62
62
  export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
63
- readonly name: "thinking";
64
- readonly description: "Toggle thinking block visibility";
65
- }, {
66
- readonly name: "toggle-thinking";
67
- readonly description: "Toggle thinking block visibility";
68
- }, {
69
63
  readonly name: "goal";
70
64
  readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
71
- }, {
72
- readonly name: "trace";
73
- readonly description: "Toggle verbose trace output";
74
- }, {
75
- readonly name: "verbose";
76
- readonly description: "Toggle verbose trace output";
77
- }, {
78
- readonly name: "debug";
79
- readonly description: "Toggle verbose trace output";
80
- }, {
81
- readonly name: "write-previews";
82
- readonly description: "Toggle write preview expansion";
83
65
  }];
84
66
  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, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
85
67
  export {};
@@ -6,7 +6,7 @@ import { isHiddenToolMetadata } from "../agent/discovery-barrier.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
- import { InputBox, isCtrlCInput, } from "./input-box.js";
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
12
  import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
@@ -227,34 +227,10 @@ function withMessageKey(message) {
227
227
  // 40ms keeps perceived latency invisible while capping layout work at 25fps.
228
228
  const STREAMING_FLUSH_INTERVAL_MS = 40;
229
229
  export const INK_LOCAL_SLASH_COMMANDS = [
230
- {
231
- name: "thinking",
232
- description: "Toggle thinking block visibility",
233
- },
234
- {
235
- name: "toggle-thinking",
236
- description: "Toggle thinking block visibility",
237
- },
238
230
  {
239
231
  name: "goal",
240
232
  description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
241
233
  },
242
- {
243
- name: "trace",
244
- description: "Toggle verbose trace output",
245
- },
246
- {
247
- name: "verbose",
248
- description: "Toggle verbose trace output",
249
- },
250
- {
251
- name: "debug",
252
- description: "Toggle verbose trace output",
253
- },
254
- {
255
- name: "write-previews",
256
- description: "Toggle write preview expansion",
257
- },
258
234
  ];
259
235
  export 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, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }) {
260
236
  const [sessionManager, setSessionManager] = useState(initialSessionManager);
@@ -281,8 +257,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
281
257
  const [streamingReasoning, setStreamingReasoning] = useState("");
282
258
  const [streamingTools, setStreamingTools] = useState([]);
283
259
  const [streamingParts, setStreamingParts] = useState([]);
284
- // Live subagent groups for the Ctrl+G inspector; recomputed each render so it
285
- // reflects members as their events stream into the transcript.
260
+ // Live subagent groups for the inspector opened from the subagent entry line;
261
+ // recomputed each render so it reflects members as their events stream into the transcript.
286
262
  const subagentGroups = useMemo(() => collectSubagentGroups(messages, streamingTools), [messages, streamingTools]);
287
263
  const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
288
264
  // Down-arrow from the composer focuses the subagent entry line; Enter then
@@ -320,9 +296,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
320
296
  const [composerDraft, setComposerDraft] = useState(null);
321
297
  const [keyProviderId, setKeyProviderId] = useState(null);
322
298
  const [showThinking, setShowThinking] = useState(false);
323
- const [expandedToolOutput, setExpandedToolOutput] = useState(false);
324
299
  const [verboseTrace, setVerboseTrace] = useState(false);
325
- const [sidebarMode, setSidebarMode] = useState("collapsed");
326
300
  const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
327
301
  const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
328
302
  const showWelcome = shouldShowWelcomeBanner({
@@ -338,6 +312,12 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
338
312
  // MessageList so Ink discards its already-printed rows and re-prints the
339
313
  // rebuilt list onto a freshly-cleared screen instead of appending duplicates.
340
314
  const [staticGeneration, setStaticGeneration] = useState(0);
315
+ const reprintTranscript = useCallback(() => {
316
+ if (process.stdout.isTTY) {
317
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
318
+ }
319
+ setStaticGeneration((generation) => generation + 1);
320
+ }, []);
341
321
  // Steer/queue while the agent runs:
342
322
  // Enter steers the current run via the agent's input controller; Tab (or an
343
323
  // ineligible input) queues for the next turn. Both render placeholder user
@@ -567,7 +547,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
567
547
  setSubagentEntryFocused(false);
568
548
  return;
569
549
  }
570
- if (key.ctrl && input.toLowerCase() === "p" && !pickerMode && !activeAbortRef.current) {
550
+ if (isCtrlLetterInput(input, key, "p") && !pickerMode && !activeAbortRef.current) {
571
551
  setStatsPanel(null);
572
552
  setPickerMode("slash");
573
553
  return;
@@ -582,7 +562,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
582
562
  }
583
563
  return;
584
564
  }
585
- if (key.ctrl && input.toLowerCase() === "t" && !pickerMode) {
565
+ if (isCtrlLetterInput(input, key, "t") && !pickerMode) {
586
566
  setShowThinking((current) => {
587
567
  const next = !current;
588
568
  addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
@@ -590,12 +570,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
590
570
  });
591
571
  return;
592
572
  }
593
- if (key.ctrl && input === "o" && !pickerMode) {
573
+ if (isCtrlLetterInput(input, key, "o") && !pickerMode) {
594
574
  setVerboseTrace((v) => !v);
575
+ reprintTranscript();
595
576
  return;
596
577
  }
597
578
  // Ctrl+R: cycle thinking level (formerly Shift+Tab)
598
- if (key.ctrl && input === "r" && !pickerMode) {
579
+ if (isCtrlLetterInput(input, key, "r") && !pickerMode) {
599
580
  const modelParts = agent.model.includes(":")
600
581
  ? agent.model.split(":")
601
582
  : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
@@ -639,12 +620,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
639
620
  // un-printed, so we wipe the screen + scrollback and bump the Static key:
640
621
  // Ink then re-prints the rebuilt list fresh instead of appending duplicates.
641
622
  const resetTranscript = useCallback((updater) => {
642
- if (process.stdout.isTTY) {
643
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
644
- }
645
- setStaticGeneration((generation) => generation + 1);
623
+ reprintTranscript();
646
624
  updateDisplayMessages(updater);
647
- }, [updateDisplayMessages]);
625
+ }, [reprintTranscript, updateDisplayMessages]);
648
626
  const addMessage = useCallback((role, content) => {
649
627
  updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
650
628
  }, [updateDisplayMessages]);
@@ -759,21 +737,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
759
737
  const { description: _drop, ...rest } = base;
760
738
  setPendingFeedback({ base: rest, initialDescription });
761
739
  }, [agent]);
762
- const sidebarFits = terminalColumns > 120;
763
- const sidebarVisible = sidebarMode === "expanded" ? sidebarFits : sidebarMode === "auto" && sidebarFits;
764
- const currentSidebarCommandState = useCallback((mode = sidebarMode) => {
765
- const visible = mode === "expanded" ? sidebarFits : mode === "auto" && sidebarFits;
766
- return { mode, visible, active: visible };
767
- }, [sidebarFits, sidebarMode]);
768
- const toggleSidebar = useCallback(() => {
769
- const next = sidebarVisible ? "collapsed" : "expanded";
770
- setSidebarMode(next);
771
- return currentSidebarCommandState(next);
772
- }, [currentSidebarCommandState, sidebarVisible]);
773
- const applySidebarMode = useCallback((mode) => {
774
- setSidebarMode(mode);
775
- return currentSidebarCommandState(mode);
776
- }, [currentSidebarCommandState]);
777
740
  const openSessionPicker = useCallback(() => {
778
741
  if (activeAbortRef.current) {
779
742
  addMessage("error", "Stop the current run before switching sessions.");
@@ -1553,30 +1516,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1553
1516
  requestExit();
1554
1517
  return;
1555
1518
  }
1556
- if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
1557
- setShowThinking((current) => {
1558
- const next = !current;
1559
- addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
1560
- return next;
1561
- });
1562
- return;
1563
- }
1564
- if (/^\/(?:trace|verbose|debug)(?:\s|$)/.test(input.trim())) {
1565
- setVerboseTrace((current) => {
1566
- const next = !current;
1567
- addMessage("assistant", next ? "Verbose trace visible" : "Compact trace visible");
1568
- return next;
1569
- });
1570
- return;
1571
- }
1572
- if (/^\/write-previews(?:\s|$)/.test(input.trim())) {
1573
- setExpandedToolOutput((current) => {
1574
- const next = !current;
1575
- addMessage("assistant", next ? "Write previews expanded" : "Write previews collapsed");
1576
- return next;
1577
- });
1578
- return;
1579
- }
1580
1519
  if (/^\/goal(?:\s|$)/.test(input.trim())) {
1581
1520
  await handleGoalCommand(input);
1582
1521
  return;
@@ -1615,8 +1554,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1615
1554
  getThemeMode: () => themeMode,
1616
1555
  getResolvedTheme: () => themeResolved,
1617
1556
  setThemeMode: applyThemeMode,
1618
- toggleSidebar,
1619
- setSidebarMode: applySidebarMode,
1620
1557
  openStats: openStatsPanel,
1621
1558
  compactionProgress: setCompaction,
1622
1559
  });
@@ -1682,7 +1619,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1682
1619
  setStartingSubmit(null);
1683
1620
  }
1684
1621
  }
1685
- }, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit, toggleSidebar, applySidebarMode, setStartingSubmit]);
1622
+ }, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
1686
1623
  // Drain the queue once the run ends and no modal needs the user first.
1687
1624
  // The placeholder row is removed right before resubmitting — handleSubmit
1688
1625
  // renders the message again as a regular user row.
@@ -1747,81 +1684,80 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1747
1684
  // tail, pickers, composer, footer) occupies the live region. Letting it size
1748
1685
  // to its content keeps the composer pinned just below the latest output the
1749
1686
  // way ordinary shell programs do.
1750
- const sidebarWidth = sidebarVisible ? Math.min(42, Math.max(28, Math.floor(terminalColumns * 0.34))) : 0;
1751
- const mainWidth = Math.max(40, terminalColumns - sidebarWidth);
1752
- return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "row", width: terminalColumns, backgroundColor: palette.background, children: [_jsxs(Box, { flexDirection: "column", width: mainWidth, backgroundColor: palette.background, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1753
- .filter((p) => isUserVisibleProvider(p.id))
1754
- .map((p) => {
1755
- const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
1756
- const configuredLabel = configured?.apiKey ? "configured" : "needs key";
1757
- return {
1758
- id: p.id,
1759
- name: `${p.name} [${configuredLabel}]`,
1760
- enabled: true,
1761
- };
1762
- }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1763
- .filter((p) => isUserVisibleProvider(p.id))
1764
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1765
- .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
1766
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
1767
- .filter((p) => safeRegistry.getAuthStorage().has(p.id))
1768
- .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
1769
- closePicker();
1770
- setKeyProviderId(null);
1771
- } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
1772
- fillComposer(`/${name} `);
1773
- closePicker();
1774
- }, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1775
- closePicker();
1776
- if (item.action === "insert-skill") {
1777
- fillComposer(`/${item.value} `);
1778
- }
1779
- else {
1780
- void handleSubmit(item.command);
1781
- }
1782
- }, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1783
- closePicker();
1784
- void handleSubmit(item.command);
1785
- }, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
1786
- closePicker();
1787
- void handleSubmit(command);
1788
- }, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
1789
- closePicker();
1790
- addMessage("assistant", summary);
1791
- }, onCancel: () => {
1792
- closePicker();
1793
- addMessage("assistant", "已取消 Feishu setup。");
1794
- } }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), todos.length > 0 && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
1795
- const resolve = pendingPlan.resolve;
1796
- setPendingPlan(null);
1797
- resolve({ action: "approve", plan: finalPlan });
1798
- }, onReject: (reason) => {
1799
- const resolve = pendingPlan.resolve;
1800
- setPendingPlan(null);
1801
- resolve({ action: "reject", reason });
1802
- } }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
1803
- const resolve = pendingApproval.resolve;
1804
- setPendingApproval(null);
1805
- resolve(decision);
1806
- }, onAllowBashPrefix: (prefix) => {
1807
- bashAllowlist?.add(prefix);
1808
- } }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
1809
- questionController?.reply(pendingQuestion.id, answers);
1810
- setPendingQuestion(null);
1811
- }, onCancel: () => {
1812
- questionController?.reject(pendingQuestion.id);
1813
- setPendingQuestion(null);
1814
- } }) })), pendingFeedback && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
1815
- if (result.kind === "success") {
1816
- addMessage("assistant", `Feedback submitted: ${result.url}`);
1817
- }
1818
- else if (result.kind === "error") {
1819
- addMessage("error", `Feedback failed: ${result.message}`);
1820
- }
1821
- } }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
1822
- if (subagentMembers.length > 0 && !pickerMode)
1823
- setSubagentEntryFocused(true);
1824
- }, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }), sidebarVisible && (_jsx(InkSidebar, { width: sidebarWidth, agent: agent, sessionManager: sessionManager, cwd: args.cwd, mode: permissionMode, goalLine: goalLine, todos: todos, mcpManager: mcpManager, lspService: lspService }))] }) }));
1687
+ const mainWidth = Math.max(40, terminalColumns);
1688
+ return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, backgroundColor: palette.background, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1689
+ .filter((p) => isUserVisibleProvider(p.id))
1690
+ .map((p) => {
1691
+ const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
1692
+ const configuredLabel = configured?.apiKey ? "configured" : "needs key";
1693
+ return {
1694
+ id: p.id,
1695
+ name: `${p.name} [${configuredLabel}]`,
1696
+ enabled: true,
1697
+ };
1698
+ }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1699
+ .filter((p) => isUserVisibleProvider(p.id))
1700
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1701
+ .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
1702
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
1703
+ .filter((p) => safeRegistry.getAuthStorage().has(p.id))
1704
+ .map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
1705
+ closePicker();
1706
+ setKeyProviderId(null);
1707
+ } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
1708
+ fillComposer(`/${name} `);
1709
+ closePicker();
1710
+ }, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1711
+ closePicker();
1712
+ if (item.action === "insert-skill") {
1713
+ fillComposer(`/${item.value} `);
1714
+ }
1715
+ else {
1716
+ void handleSubmit(item.command);
1717
+ }
1718
+ }, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1719
+ closePicker();
1720
+ void handleSubmit(item.command);
1721
+ }, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
1722
+ closePicker();
1723
+ void handleSubmit(command);
1724
+ }, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
1725
+ closePicker();
1726
+ addMessage("assistant", summary);
1727
+ }, onCancel: () => {
1728
+ closePicker();
1729
+ addMessage("assistant", "已取消 Feishu setup。");
1730
+ } }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), todos.length > 0 && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
1731
+ const resolve = pendingPlan.resolve;
1732
+ setPendingPlan(null);
1733
+ resolve({ action: "approve", plan: finalPlan });
1734
+ }, onReject: (reason) => {
1735
+ const resolve = pendingPlan.resolve;
1736
+ setPendingPlan(null);
1737
+ resolve({ action: "reject", reason });
1738
+ } }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
1739
+ const resolve = pendingApproval.resolve;
1740
+ setPendingApproval(null);
1741
+ resolve(decision);
1742
+ }, onAllowBashPrefix: (prefix) => {
1743
+ bashAllowlist?.add(prefix);
1744
+ } }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
1745
+ questionController?.reply(pendingQuestion.id, answers);
1746
+ setPendingQuestion(null);
1747
+ }, onCancel: () => {
1748
+ questionController?.reject(pendingQuestion.id);
1749
+ setPendingQuestion(null);
1750
+ } }) })), pendingFeedback && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
1751
+ if (result.kind === "success") {
1752
+ addMessage("assistant", `Feedback submitted: ${result.url}`);
1753
+ }
1754
+ else if (result.kind === "error") {
1755
+ addMessage("error", `Feedback failed: ${result.message}`);
1756
+ }
1757
+ } }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
1758
+ if (subagentMembers.length > 0 && !pickerMode)
1759
+ setSubagentEntryFocused(true);
1760
+ }, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }) }));
1825
1761
  }
1826
1762
  function buildCommandPaletteItems(skillRegistry) {
1827
1763
  const items = new Map();
@@ -2109,82 +2045,6 @@ function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCan
2109
2045
  return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
2110
2046
  }) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
2111
2047
  }
2112
- function summarizeMcpStates(states) {
2113
- const summary = { connected: 0, starting: 0, failed: 0, disabled: 0, tools: 0 };
2114
- for (const state of states) {
2115
- if (state.status.kind === "connected") {
2116
- summary.connected += 1;
2117
- summary.tools += state.status.tools.length;
2118
- }
2119
- else if (state.status.kind === "failed") {
2120
- summary.failed += 1;
2121
- }
2122
- else {
2123
- summary.disabled += 1;
2124
- }
2125
- }
2126
- return summary;
2127
- }
2128
- function summarizeLspStatuses(statuses) {
2129
- const summary = { connected: 0, starting: 0, failed: 0, disabled: 0 };
2130
- for (const status of statuses) {
2131
- if (status.status === "connected")
2132
- summary.connected += 1;
2133
- else if (status.status === "starting")
2134
- summary.starting += 1;
2135
- else
2136
- summary.failed += 1;
2137
- }
2138
- return summary;
2139
- }
2140
- function formatStatusCount(summary) {
2141
- const parts = [];
2142
- if (summary.connected > 0)
2143
- parts.push(`${summary.connected} up`);
2144
- if (summary.starting > 0)
2145
- parts.push(`${summary.starting} starting`);
2146
- if (summary.failed > 0)
2147
- parts.push(`${summary.failed} failed`);
2148
- if (summary.disabled > 0)
2149
- parts.push(`${summary.disabled} disabled`);
2150
- return parts.join(" · ") || "none";
2151
- }
2152
- function SidebarSection({ title, children }) {
2153
- const theme = useTheme();
2154
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), children] }));
2155
- }
2156
- function SidebarRow({ label, value, color, }) {
2157
- const theme = useTheme();
2158
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [label, ": "] }), _jsx(Text, { color: color ?? theme.userMessageText, children: value })] }));
2159
- }
2160
- function InkSidebar({ width, agent, sessionManager, cwd, mode, goalLine, todos, mcpManager, lspService, }) {
2161
- const theme = useTheme();
2162
- const innerWidth = Math.max(12, width - 4);
2163
- const todoCounts = todos.reduce((acc, todo) => {
2164
- acc[todo.status] = (acc[todo.status] ?? 0) + 1;
2165
- return acc;
2166
- }, {});
2167
- const todoSummary = todos.length === 0
2168
- ? "none"
2169
- : [
2170
- todoCounts.in_progress ? `${todoCounts.in_progress} active` : "",
2171
- todoCounts.pending ? `${todoCounts.pending} pending` : "",
2172
- todoCounts.completed ? `${todoCounts.completed} done` : "",
2173
- ].filter(Boolean).join(" · ");
2174
- const mcpStates = mcpManager?.getStates() ?? [];
2175
- const mcpSummary = summarizeMcpStates(mcpStates);
2176
- const lspSummary = lspService?.isDisabled()
2177
- ? { connected: 0, starting: 0, failed: 0, disabled: 1 }
2178
- : summarizeLspStatuses(lspService?.status() ?? []);
2179
- const latestMcpFailure = mcpStates.find((state) => state.status.kind === "failed");
2180
- const latestLspFailure = lspService?.status().find((status) => status.status === "error");
2181
- const sessionTitle = truncate(sessionDisplayName(sessionManager), innerWidth);
2182
- const modelLabel = agent.model ? displayModel(agent.model) : "not selected";
2183
- const route = agent.providerId
2184
- ? `${agent.providerId}/${modelLabel}`
2185
- : modelLabel;
2186
- return (_jsxs(Box, { flexDirection: "column", width: width, height: "100%", borderStyle: "single", borderColor: theme.border, paddingX: 1, paddingY: 1, flexShrink: 0, children: [_jsx(Text, { color: theme.borderActive, bold: true, children: "Session" }), _jsx(Text, { color: theme.userMessageText, children: sessionTitle }), _jsx(Text, { color: theme.muted, children: truncate(friendlyCwd(cwd), innerWidth) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(SidebarSection, { title: "Runtime", children: [_jsx(SidebarRow, { label: "model", value: truncate(route, innerWidth - 7) }), _jsx(SidebarRow, { label: "mode", value: mode, color: mode === "bypassPermissions" ? theme.warning : theme.userMessageText }), _jsx(SidebarRow, { label: "thinking", value: agent.thinking || "off" })] }), goalLine && (_jsx(SidebarSection, { title: "Goal", children: _jsx(Text, { color: theme.userMessageText, children: truncate(goalLine, innerWidth) }) })), _jsx(SidebarSection, { title: "Todos", children: _jsx(Text, { color: todos.length > 0 ? theme.userMessageText : theme.muted, children: truncate(todoSummary, innerWidth) }) }), _jsxs(SidebarSection, { title: "MCP", children: [_jsx(Text, { color: mcpSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(`${formatStatusCount(mcpSummary)}${mcpSummary.tools > 0 ? ` · ${mcpSummary.tools} tools` : ""}`, innerWidth) }), latestMcpFailure?.status.kind === "failed" && (_jsx(Text, { color: theme.muted, children: truncate(latestMcpFailure.status.error, innerWidth) }))] }), _jsxs(SidebarSection, { title: "LSP", children: [_jsx(Text, { color: lspSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(formatStatusCount(lspSummary), innerWidth) }), latestLspFailure?.message && (_jsx(Text, { color: theme.muted, children: truncate(latestLspFailure.message, innerWidth) }))] })] })] }));
2187
- }
2188
2048
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2189
2049
  const GENERIC_PHRASES = [
2190
2050
  "mapping the workspace",
@@ -47,6 +47,9 @@ export declare function resolveCursorRowCompensation(input: {
47
47
  viewportRows: number;
48
48
  previousOutputHeight: number | null;
49
49
  }): number;
50
+ export declare function isCtrlLetterInput(input: string, key: {
51
+ ctrl?: boolean;
52
+ }, letter: string): boolean;
50
53
  export declare function isCtrlCInput(input: string, key: {
51
54
  ctrl?: boolean;
52
55
  }): boolean;
@@ -37,8 +37,15 @@ export function resolveCursorRowCompensation(input) {
37
37
  return input.previousRowCompensation;
38
38
  return needsCursorRowCompensation(input.nextOutputHeight, input.viewportRows, input.previousOutputHeight) ? 1 : 0;
39
39
  }
40
+ export function isCtrlLetterInput(input, key, letter) {
41
+ const normalized = letter.toLowerCase();
42
+ if (!/^[a-z]$/.test(normalized))
43
+ return false;
44
+ const rawControlInput = String.fromCharCode(normalized.charCodeAt(0) - 96);
45
+ return input === rawControlInput || (key.ctrl === true && input.toLowerCase() === normalized);
46
+ }
40
47
  export function isCtrlCInput(input, key) {
41
- return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
48
+ return isCtrlLetterInput(input, key, "c");
42
49
  }
43
50
  export function shouldUseLineComposerFrame(_background) {
44
51
  return true;
@@ -18,7 +18,6 @@ interface MessageListProps {
18
18
  streamingParts: DisplayMessagePart[];
19
19
  terminalColumns: number;
20
20
  showThinking?: boolean;
21
- expandedToolOutput?: boolean;
22
21
  verboseTrace: boolean;
23
22
  pendingApproval?: PendingApprovalHint | null;
24
23
  /** Animation tick used to refresh in-progress elapsed counters. */
@@ -42,5 +41,5 @@ interface MessageListProps {
42
41
  */
43
42
  maxStreamRows?: number;
44
43
  }
45
- export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration, paddingX, maxStreamRows, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
44
+ export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration, paddingX, maxStreamRows, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
46
45
  export {};
@@ -12,7 +12,7 @@ import { latestSubagentNote, sortSubagents, subagentDescriptor, subagentLabel, s
12
12
  import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
13
13
  import { splitImageDisplayContent } from "../tui/image-display.js";
14
14
  const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
15
- export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, expandedToolOutput = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
15
+ export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
16
16
  const theme = useTheme();
17
17
  const hasStreaming = !!(streamingContent ||
18
18
  streamingReasoning ||
@@ -47,8 +47,8 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
47
47
  if (item.kind === "welcome") {
48
48
  return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
49
49
  }
50
- return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
51
- } }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
50
+ return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
51
+ } }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
52
52
  }
53
53
  /**
54
54
  * Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
@@ -99,7 +99,7 @@ function DynamicClamp({ maxRows, paddingX, children, }) {
99
99
  // append-only (compaction reuses already-compacted instances), keys are
100
100
  // stable, and nowTick is only threaded to the last row, so memo hits for all
101
101
  // settled history rows.
102
- const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, expandedToolOutput, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
102
+ const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
103
103
  const theme = useTheme();
104
104
  if (message.role === "user") {
105
105
  return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus, separateFromPrevious: separateFromPrevious }));
@@ -139,9 +139,9 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
139
139
  message.taskElapsedMs !== undefined;
140
140
  if (!hasVisibleAssistantContent)
141
141
  return null;
142
- return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
142
+ return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, 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() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
143
143
  });
144
- function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, expandedToolOutput, verboseTrace, pendingApproval, nowTick, }) {
144
+ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, }) {
145
145
  const deferredContent = React.useDeferredValue(content);
146
146
  const deferredReasoning = React.useDeferredValue(reasoning);
147
147
  const deferredParts = React.useDeferredValue(parts);
@@ -155,16 +155,16 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, s
155
155
  // turn commits — no spacing jump at finalize time. (The old marginTop=0
156
156
  // was a flicker mitigation for the main-screen <Static> renderer; the
157
157
  // alt-screen viewport repaints frames atomically, so it's obsolete.)
158
- _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
158
+ _jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
159
159
  }
160
- function MessageParts({ parts, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
160
+ function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
161
161
  const lastToolsPartIndex = findLastToolsPartIndex(parts);
162
162
  const lastTextPartIndex = findLastTextPartIndex(parts);
163
163
  return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
164
164
  if (part.type === "text") {
165
165
  return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
166
166
  }
167
- return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, expandedToolOutput: expandedToolOutput, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
167
+ return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
168
168
  }) }));
169
169
  }
170
170
  function findLastTextPartIndex(parts) {
@@ -195,17 +195,16 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
195
195
  const trimmed = visible.trim();
196
196
  return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
197
197
  }
198
- function ToolsPart({ toolCalls, terminalColumns, expandedToolOutput, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
198
+ function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
199
199
  if (toolCalls.length === 0)
200
200
  return null;
201
- const expandTools = verboseTrace || expandedToolOutput;
202
- if (!expandTools) {
201
+ if (!verboseTrace) {
203
202
  return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
204
203
  }
205
204
  const lastIdx = toolCalls.length - 1;
206
205
  return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
207
206
  const isWaitingApproval = isToolPending(tc) && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
208
- return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: expandTools, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
207
+ return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
209
208
  }) }));
210
209
  }
211
210
  function fallbackStreamingParts(content, tools) {
@@ -553,7 +552,7 @@ function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }
553
552
  const descriptor = padVisual(truncateVisual(subagentDescriptor(subagent), descriptorWidth), descriptorWidth);
554
553
  const note = truncateVisual(latestSubagentNote(subagent), Math.max(12, detailWidth - 16 - descriptorWidth - 10));
555
554
  return (_jsxs(Box, { children: [_jsx(Text, { color: subagentStatusColor(status, theme), children: label }), _jsxs(Text, { color: theme.traceAction, children: [" ", descriptor] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", padVisual(status, 9)] }), note && _jsxs(Text, { color: subagent.error ? theme.error : theme.traceDetail, children: [" ", note] })] }, subagent.subAgentId ?? `${subagentLabel(subagent)}-${index}`));
556
- }), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7 Ctrl+G to inspect traces"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
555
+ }), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7 \u2193 then Enter to inspect traces"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
557
556
  }
558
557
  function TruncationHint({ remaining, verbose, showExpandHint, }) {
559
558
  const theme = useTheme();
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Full-screen subagent inspector (Ctrl+G / /agents).
2
+ * Full-screen subagent inspector opened from the subagent entry line.
3
3
  *
4
4
  * Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
5
5
  * subagents (each spawn_agent is one member; each agent_team/agent_batch is a
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Full-screen subagent inspector (Ctrl+G / /agents).
3
+ * Full-screen subagent inspector opened from the subagent entry line.
4
4
  *
5
5
  * Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
6
6
  * subagents (each spawn_agent is one member; each agent_team/agent_batch is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bubblebrain-ai/bubble",
3
- "version": "0.0.29",
3
+ "version": "0.0.30",
4
4
  "description": "A terminal coding agent",
5
5
  "type": "module",
6
6
  "engines": {