@bubblebrain-ai/bubble 0.0.28 → 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.
Files changed (63) hide show
  1. package/README.md +23 -3
  2. package/dist/agent/categories.d.ts +2 -0
  3. package/dist/agent/categories.js +4 -0
  4. package/dist/agent/child-runner.d.ts +5 -1
  5. package/dist/agent/child-runner.js +35 -2
  6. package/dist/agent/profiles.js +3 -0
  7. package/dist/agent/structured-output.d.ts +37 -0
  8. package/dist/agent/structured-output.js +193 -0
  9. package/dist/agent/subagent-control.d.ts +3 -0
  10. package/dist/agent/subagent-scheduler.d.ts +10 -0
  11. package/dist/agent/subagent-scheduler.js +31 -0
  12. package/dist/agent/workflow/control.d.ts +37 -0
  13. package/dist/agent/workflow/control.js +20 -0
  14. package/dist/agent/workflow/errors.d.ts +16 -0
  15. package/dist/agent/workflow/errors.js +24 -0
  16. package/dist/agent/workflow/runtime.d.ts +75 -0
  17. package/dist/agent/workflow/runtime.js +237 -0
  18. package/dist/agent.d.ts +105 -0
  19. package/dist/agent.js +425 -17
  20. package/dist/context/compact-llm.d.ts +10 -1
  21. package/dist/context/compact-llm.js +13 -5
  22. package/dist/context/compact.d.ts +30 -0
  23. package/dist/context/compact.js +34 -17
  24. package/dist/goal/format.d.ts +1 -1
  25. package/dist/goal/format.js +1 -1
  26. package/dist/network/provider-transport.d.ts +9 -0
  27. package/dist/network/provider-transport.js +19 -1
  28. package/dist/provider.d.ts +14 -0
  29. package/dist/provider.js +24 -0
  30. package/dist/session.d.ts +16 -0
  31. package/dist/session.js +33 -1
  32. package/dist/slash-commands/commands.js +41 -113
  33. package/dist/slash-commands/types.d.ts +14 -9
  34. package/dist/tools/agent-lifecycle.d.ts +6 -0
  35. package/dist/tools/agent-lifecycle.js +285 -0
  36. package/dist/tools/child-tools.d.ts +10 -0
  37. package/dist/tools/child-tools.js +12 -0
  38. package/dist/tools/read.d.ts +1 -1
  39. package/dist/tools/read.js +9 -0
  40. package/dist/tui/image-display.d.ts +6 -0
  41. package/dist/tui/image-display.js +26 -1
  42. package/dist/tui-ink/app.d.ts +0 -18
  43. package/dist/tui-ink/app.js +168 -230
  44. package/dist/tui-ink/compaction-progress.d.ts +19 -0
  45. package/dist/tui-ink/compaction-progress.js +74 -0
  46. package/dist/tui-ink/input-box.d.ts +10 -1
  47. package/dist/tui-ink/input-box.js +56 -16
  48. package/dist/tui-ink/markdown.d.ts +18 -0
  49. package/dist/tui-ink/markdown.js +172 -16
  50. package/dist/tui-ink/message-list.d.ts +1 -2
  51. package/dist/tui-ink/message-list.js +50 -107
  52. package/dist/tui-ink/run.js +5 -0
  53. package/dist/tui-ink/subagent-inspector.d.ts +17 -0
  54. package/dist/tui-ink/subagent-inspector.js +189 -0
  55. package/dist/tui-ink/subagent-view.d.ts +47 -0
  56. package/dist/tui-ink/subagent-view.js +163 -0
  57. package/dist/tui-ink/terminal-env.d.ts +15 -0
  58. package/dist/tui-ink/terminal-env.js +22 -0
  59. package/dist/tui-ink/use-terminal-size.js +33 -6
  60. package/dist/tui-ink/width.d.ts +18 -0
  61. package/dist/tui-ink/width.js +130 -0
  62. package/dist/types.d.ts +35 -0
  63. package/package.json +2 -1
@@ -6,8 +6,9 @@ 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
+ import { isMultiplexedTerminal } from "./terminal-env.js";
11
12
  import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
12
13
  import { AgentRunInputQueue } from "../agent/input-controller.js";
13
14
  import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
@@ -23,6 +24,7 @@ import { useTerminalSize } from "./use-terminal-size.js";
23
24
  import { WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
24
25
  import { expandAtMentions } from "./file-mentions.js";
25
26
  import { TodosPanel } from "./todos.js";
27
+ import { CompactionProgressCard } from "./compaction-progress.js";
26
28
  import { PlanConfirm } from "./plan-confirm.js";
27
29
  import { ApprovalDialog } from "./approval/approval-dialog.js";
28
30
  import { getNextPermissionMode } from "../permission/mode.js";
@@ -35,6 +37,8 @@ import { formatImageUserDisplayText, nextImageDisplayLabelStart } from "../tui/i
35
37
  import { decideStartingSubmitFingerprint, submitPayloadFingerprint } from "./submit-dedupe.js";
36
38
  import { isQueuedInputForCurrentSession, queuedAndPendingDisplayKeys, } from "./input-queue.js";
37
39
  import { SessionPicker } from "./session-picker.js";
40
+ import { SubagentInspector } from "./subagent-inspector.js";
41
+ import { collectSubagentGroups, subagentSummary } from "./subagent-view.js";
38
42
  import { sessionDisplayName } from "../tui/session-display.js";
39
43
  import { parseGoalCommand } from "../goal/command.js";
40
44
  import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
@@ -223,34 +227,10 @@ function withMessageKey(message) {
223
227
  // 40ms keeps perceived latency invisible while capping layout work at 25fps.
224
228
  const STREAMING_FLUSH_INTERVAL_MS = 40;
225
229
  export const INK_LOCAL_SLASH_COMMANDS = [
226
- {
227
- name: "thinking",
228
- description: "Toggle thinking block visibility",
229
- },
230
- {
231
- name: "toggle-thinking",
232
- description: "Toggle thinking block visibility",
233
- },
234
230
  {
235
231
  name: "goal",
236
232
  description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
237
233
  },
238
- {
239
- name: "trace",
240
- description: "Toggle verbose trace output",
241
- },
242
- {
243
- name: "verbose",
244
- description: "Toggle verbose trace output",
245
- },
246
- {
247
- name: "debug",
248
- description: "Toggle verbose trace output",
249
- },
250
- {
251
- name: "write-previews",
252
- description: "Toggle write preview expansion",
253
- },
254
234
  ];
255
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 }) {
256
236
  const [sessionManager, setSessionManager] = useState(initialSessionManager);
@@ -277,7 +257,31 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
277
257
  const [streamingReasoning, setStreamingReasoning] = useState("");
278
258
  const [streamingTools, setStreamingTools] = useState([]);
279
259
  const [streamingParts, setStreamingParts] = useState([]);
280
- const [thinkingLevel, setThinkingLevel] = useState(agent.thinking);
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.
262
+ const subagentGroups = useMemo(() => collectSubagentGroups(messages, streamingTools), [messages, streamingTools]);
263
+ const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
264
+ // Down-arrow from the composer focuses the subagent entry line; Enter then
265
+ // opens the inspector, Esc/Up returns to the composer (Claude Code parity).
266
+ const [subagentEntryFocused, setSubagentEntryFocused] = useState(false);
267
+ useEffect(() => {
268
+ if (subagentMembers.length === 0 && subagentEntryFocused)
269
+ setSubagentEntryFocused(false);
270
+ }, [subagentMembers.length, subagentEntryFocused]);
271
+ // Live progress for a manual `/compact` run (null when not compacting).
272
+ const [compaction, setCompaction] = useState(null);
273
+ // Normalize agent.thinking against the current model's supported levels so the
274
+ // banner displays the *effective* level, not a stale user-config value like
275
+ // "xhigh" when switching to a model that only supports ["high","max","off"].
276
+ const [thinkingLevel, setThinkingLevel] = useState(() => {
277
+ const modelParts = agent.model.includes(":")
278
+ ? agent.model.split(":")
279
+ : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
280
+ const providerId = modelParts[0];
281
+ const modelId = modelParts.slice(1).join(":");
282
+ const availableLevels = getAvailableThinkingLevels(providerId, modelId);
283
+ return normalizeThinkingLevel(agent.thinking, availableLevels);
284
+ });
281
285
  const [permissionMode, setPermissionMode] = useState(agent.mode);
282
286
  const [todos, setTodos] = useState(() => agent.getTodos());
283
287
  const [goalLine, setGoalLine] = useState("");
@@ -292,9 +296,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
292
296
  const [composerDraft, setComposerDraft] = useState(null);
293
297
  const [keyProviderId, setKeyProviderId] = useState(null);
294
298
  const [showThinking, setShowThinking] = useState(false);
295
- const [expandedToolOutput, setExpandedToolOutput] = useState(false);
296
299
  const [verboseTrace, setVerboseTrace] = useState(false);
297
- const [sidebarMode, setSidebarMode] = useState("collapsed");
298
300
  const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
299
301
  const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
300
302
  const showWelcome = shouldShowWelcomeBanner({
@@ -310,6 +312,12 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
310
312
  // MessageList so Ink discards its already-printed rows and re-prints the
311
313
  // rebuilt list onto a freshly-cleared screen instead of appending duplicates.
312
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
+ }, []);
313
321
  // Steer/queue while the agent runs:
314
322
  // Enter steers the current run via the agent's input controller; Tab (or an
315
323
  // ineligible input) queues for the next turn. Both render placeholder user
@@ -521,7 +529,25 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
521
529
  // also frees the arrow keys entirely for composer history.
522
530
  if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || statsPanel)
523
531
  return;
524
- if (key.ctrl && input.toLowerCase() === "p" && !pickerMode && !activeAbortRef.current) {
532
+ // Subagent entry is focused (the composer is disabled): Enter opens the
533
+ // inspector, Up/Esc returns to the composer. Other keys just return focus.
534
+ if (subagentEntryFocused && !pickerMode) {
535
+ if (key.return) {
536
+ setSubagentEntryFocused(false);
537
+ setStatsPanel(null);
538
+ setPickerMode("agents");
539
+ return;
540
+ }
541
+ if (key.escape || key.upArrow) {
542
+ setSubagentEntryFocused(false);
543
+ return;
544
+ }
545
+ if (key.downArrow)
546
+ return; // stay focused
547
+ setSubagentEntryFocused(false);
548
+ return;
549
+ }
550
+ if (isCtrlLetterInput(input, key, "p") && !pickerMode && !activeAbortRef.current) {
525
551
  setStatsPanel(null);
526
552
  setPickerMode("slash");
527
553
  return;
@@ -536,7 +562,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
536
562
  }
537
563
  return;
538
564
  }
539
- if (key.ctrl && input.toLowerCase() === "t" && !pickerMode) {
565
+ if (isCtrlLetterInput(input, key, "t") && !pickerMode) {
540
566
  setShowThinking((current) => {
541
567
  const next = !current;
542
568
  addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
@@ -544,12 +570,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
544
570
  });
545
571
  return;
546
572
  }
547
- if (key.ctrl && input === "o" && !pickerMode) {
573
+ if (isCtrlLetterInput(input, key, "o") && !pickerMode) {
548
574
  setVerboseTrace((v) => !v);
575
+ reprintTranscript();
549
576
  return;
550
577
  }
551
578
  // Ctrl+R: cycle thinking level (formerly Shift+Tab)
552
- if (key.ctrl && input === "r" && !pickerMode) {
579
+ if (isCtrlLetterInput(input, key, "r") && !pickerMode) {
553
580
  const modelParts = agent.model.includes(":")
554
581
  ? agent.model.split(":")
555
582
  : [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
@@ -593,12 +620,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
593
620
  // un-printed, so we wipe the screen + scrollback and bump the Static key:
594
621
  // Ink then re-prints the rebuilt list fresh instead of appending duplicates.
595
622
  const resetTranscript = useCallback((updater) => {
596
- if (process.stdout.isTTY) {
597
- process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
598
- }
599
- setStaticGeneration((generation) => generation + 1);
623
+ reprintTranscript();
600
624
  updateDisplayMessages(updater);
601
- }, [updateDisplayMessages]);
625
+ }, [reprintTranscript, updateDisplayMessages]);
602
626
  const addMessage = useCallback((role, content) => {
603
627
  updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
604
628
  }, [updateDisplayMessages]);
@@ -642,7 +666,11 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
642
666
  // <Static>), so clearing React state is not enough — resetTranscript wipes
643
667
  // the screen + scrollback and re-prints the (now empty) transcript.
644
668
  resetTranscript(() => []);
645
- }, [resetTranscript]);
669
+ // The todos panel renders off React state, not the transcript, so wiping
670
+ // messages alone leaves a stale To-Do list on screen. /clear already reset
671
+ // the agent's todos; mirror that into the UI (same as session switch).
672
+ setTodos(agent.getTodos());
673
+ }, [resetTranscript, agent]);
646
674
  // Render a placeholder user row for input waiting to enter the run.
647
675
  const addStatusUserMessage = useCallback((content, status) => {
648
676
  const key = nextDisplayMessageKey("user");
@@ -709,21 +737,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
709
737
  const { description: _drop, ...rest } = base;
710
738
  setPendingFeedback({ base: rest, initialDescription });
711
739
  }, [agent]);
712
- const sidebarFits = terminalColumns > 120;
713
- const sidebarVisible = sidebarMode === "expanded" ? sidebarFits : sidebarMode === "auto" && sidebarFits;
714
- const currentSidebarCommandState = useCallback((mode = sidebarMode) => {
715
- const visible = mode === "expanded" ? sidebarFits : mode === "auto" && sidebarFits;
716
- return { mode, visible, active: visible };
717
- }, [sidebarFits, sidebarMode]);
718
- const toggleSidebar = useCallback(() => {
719
- const next = sidebarVisible ? "collapsed" : "expanded";
720
- setSidebarMode(next);
721
- return currentSidebarCommandState(next);
722
- }, [currentSidebarCommandState, sidebarVisible]);
723
- const applySidebarMode = useCallback((mode) => {
724
- setSidebarMode(mode);
725
- return currentSidebarCommandState(mode);
726
- }, [currentSidebarCommandState]);
727
740
  const openSessionPicker = useCallback(() => {
728
741
  if (activeAbortRef.current) {
729
742
  addMessage("error", "Stop the current run before switching sessions.");
@@ -1238,7 +1251,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1238
1251
  if (steer) {
1239
1252
  pendingSteersRef.current.delete(event.id);
1240
1253
  setPendingSteerCount(pendingSteersRef.current.size);
1241
- resetTranscript((prev) => moveStatusMessageToEnd(prev, steer.displayKey));
1254
+ // Moving the steer placeholder out of the live region into
1255
+ // <Static> is a pure append (it was never in the settled list,
1256
+ // only the dynamic block). Off a multiplexer Ink erases the
1257
+ // vacated live rows in place, so a plain append avoids the
1258
+ // full-screen reprint flash. Under tmux/screen the in-place
1259
+ // erase can't reach scrolled rows, so keep the clean reprint.
1260
+ const commit = isMultiplexedTerminal() ? resetTranscript : updateDisplayMessages;
1261
+ commit((prev) => moveStatusMessageToEnd(prev, steer.displayKey));
1242
1262
  }
1243
1263
  break;
1244
1264
  }
@@ -1287,7 +1307,24 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1287
1307
  commitAssistantMessage();
1288
1308
  if (err instanceof AgentAbortError || err?.name === "AbortError") {
1289
1309
  runCancelled = true;
1290
- resetTranscript(() => reconstructDisplayMessages(agent.messages));
1310
+ // commitAssistantMessage already appended the partial answer; the
1311
+ // interrupt is otherwise a pure append (the partial + a "Interrupted"
1312
+ // row). Off a multiplexer, append just the interrupt row so settled
1313
+ // history is never reprinted — no flash. Under tmux/screen, fall back
1314
+ // to the full reprint that rebuilds from the canonical agent.messages.
1315
+ if (isMultiplexedTerminal()) {
1316
+ resetTranscript(() => reconstructDisplayMessages(agent.messages));
1317
+ }
1318
+ else {
1319
+ updateDisplayMessages((prev) => [
1320
+ ...prev,
1321
+ withMessageKey({
1322
+ role: "assistant",
1323
+ content: "Interrupted by user",
1324
+ syntheticKind: "ui_interrupt",
1325
+ }),
1326
+ ]);
1327
+ }
1291
1328
  }
1292
1329
  else {
1293
1330
  runErrored = true;
@@ -1479,30 +1516,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1479
1516
  requestExit();
1480
1517
  return;
1481
1518
  }
1482
- if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
1483
- setShowThinking((current) => {
1484
- const next = !current;
1485
- addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
1486
- return next;
1487
- });
1488
- return;
1489
- }
1490
- if (/^\/(?:trace|verbose|debug)(?:\s|$)/.test(input.trim())) {
1491
- setVerboseTrace((current) => {
1492
- const next = !current;
1493
- addMessage("assistant", next ? "Verbose trace visible" : "Compact trace visible");
1494
- return next;
1495
- });
1496
- return;
1497
- }
1498
- if (/^\/write-previews(?:\s|$)/.test(input.trim())) {
1499
- setExpandedToolOutput((current) => {
1500
- const next = !current;
1501
- addMessage("assistant", next ? "Write previews expanded" : "Write previews collapsed");
1502
- return next;
1503
- });
1504
- return;
1505
- }
1506
1519
  if (/^\/goal(?:\s|$)/.test(input.trim())) {
1507
1520
  await handleGoalCommand(input);
1508
1521
  return;
@@ -1541,9 +1554,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1541
1554
  getThemeMode: () => themeMode,
1542
1555
  getResolvedTheme: () => themeResolved,
1543
1556
  setThemeMode: applyThemeMode,
1544
- toggleSidebar,
1545
- setSidebarMode: applySidebarMode,
1546
1557
  openStats: openStatsPanel,
1558
+ compactionProgress: setCompaction,
1547
1559
  });
1548
1560
  if (handled) {
1549
1561
  if (agent.mode !== permissionMode) {
@@ -1607,7 +1619,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1607
1619
  setStartingSubmit(null);
1608
1620
  }
1609
1621
  }
1610
- }, [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]);
1611
1623
  // Drain the queue once the run ends and no modal needs the user first.
1612
1624
  // The placeholder row is removed right before resubmitting — handleSubmit
1613
1625
  // renders the message again as a regular user row.
@@ -1672,78 +1684,80 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
1672
1684
  // tail, pickers, composer, footer) occupies the live region. Letting it size
1673
1685
  // to its content keeps the composer pinned just below the latest output the
1674
1686
  // way ordinary shell programs do.
1675
- const sidebarWidth = sidebarVisible ? Math.min(42, Math.max(28, Math.floor(terminalColumns * 0.34))) : 0;
1676
- const mainWidth = Math.max(40, terminalColumns - sidebarWidth);
1677
- 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
1678
- .filter((p) => isUserVisibleProvider(p.id))
1679
- .map((p) => {
1680
- const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
1681
- const configuredLabel = configured?.apiKey ? "configured" : "needs key";
1682
- return {
1683
- id: p.id,
1684
- name: `${p.name} [${configuredLabel}]`,
1685
- enabled: true,
1686
- };
1687
- }), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
1688
- .filter((p) => isUserVisibleProvider(p.id))
1689
- .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
1690
- .filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
1691
- .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()
1692
- .filter((p) => safeRegistry.getAuthStorage().has(p.id))
1693
- .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: () => {
1694
- closePicker();
1695
- setKeyProviderId(null);
1696
- } }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
1697
- fillComposer(`/${name} `);
1698
- closePicker();
1699
- }, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1700
- closePicker();
1701
- if (item.action === "insert-skill") {
1702
- fillComposer(`/${item.value} `);
1703
- }
1704
- else {
1705
- void handleSubmit(item.command);
1706
- }
1707
- }, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
1708
- closePicker();
1709
- void handleSubmit(item.command);
1710
- }, 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 === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
1711
- closePicker();
1712
- void handleSubmit(command);
1713
- }, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
1714
- closePicker();
1715
- addMessage("assistant", summary);
1716
- }, onCancel: () => {
1717
- closePicker();
1718
- addMessage("assistant", "已取消 Feishu setup。");
1719
- } }) })), 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) => {
1720
- const resolve = pendingPlan.resolve;
1721
- setPendingPlan(null);
1722
- resolve({ action: "approve", plan: finalPlan });
1723
- }, onReject: (reason) => {
1724
- const resolve = pendingPlan.resolve;
1725
- setPendingPlan(null);
1726
- resolve({ action: "reject", reason });
1727
- } }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
1728
- const resolve = pendingApproval.resolve;
1729
- setPendingApproval(null);
1730
- resolve(decision);
1731
- }, onAllowBashPrefix: (prefix) => {
1732
- bashAllowlist?.add(prefix);
1733
- } }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
1734
- questionController?.reply(pendingQuestion.id, answers);
1735
- setPendingQuestion(null);
1736
- }, onCancel: () => {
1737
- questionController?.reject(pendingQuestion.id);
1738
- setPendingQuestion(null);
1739
- } }) })), 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) => {
1740
- if (result.kind === "success") {
1741
- addMessage("assistant", `Feedback submitted: ${result.url}`);
1742
- }
1743
- else if (result.kind === "error") {
1744
- addMessage("error", `Feedback failed: ${result.message}`);
1745
- }
1746
- } }) })), !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, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel, 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 && (_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 }) }) }))] }) }));
1747
1761
  }
1748
1762
  function buildCommandPaletteItems(skillRegistry) {
1749
1763
  const items = new Map();
@@ -2031,82 +2045,6 @@ function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCan
2031
2045
  return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
2032
2046
  }) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
2033
2047
  }
2034
- function summarizeMcpStates(states) {
2035
- const summary = { connected: 0, starting: 0, failed: 0, disabled: 0, tools: 0 };
2036
- for (const state of states) {
2037
- if (state.status.kind === "connected") {
2038
- summary.connected += 1;
2039
- summary.tools += state.status.tools.length;
2040
- }
2041
- else if (state.status.kind === "failed") {
2042
- summary.failed += 1;
2043
- }
2044
- else {
2045
- summary.disabled += 1;
2046
- }
2047
- }
2048
- return summary;
2049
- }
2050
- function summarizeLspStatuses(statuses) {
2051
- const summary = { connected: 0, starting: 0, failed: 0, disabled: 0 };
2052
- for (const status of statuses) {
2053
- if (status.status === "connected")
2054
- summary.connected += 1;
2055
- else if (status.status === "starting")
2056
- summary.starting += 1;
2057
- else
2058
- summary.failed += 1;
2059
- }
2060
- return summary;
2061
- }
2062
- function formatStatusCount(summary) {
2063
- const parts = [];
2064
- if (summary.connected > 0)
2065
- parts.push(`${summary.connected} up`);
2066
- if (summary.starting > 0)
2067
- parts.push(`${summary.starting} starting`);
2068
- if (summary.failed > 0)
2069
- parts.push(`${summary.failed} failed`);
2070
- if (summary.disabled > 0)
2071
- parts.push(`${summary.disabled} disabled`);
2072
- return parts.join(" · ") || "none";
2073
- }
2074
- function SidebarSection({ title, children }) {
2075
- const theme = useTheme();
2076
- return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), children] }));
2077
- }
2078
- function SidebarRow({ label, value, color, }) {
2079
- const theme = useTheme();
2080
- return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [label, ": "] }), _jsx(Text, { color: color ?? theme.userMessageText, children: value })] }));
2081
- }
2082
- function InkSidebar({ width, agent, sessionManager, cwd, mode, goalLine, todos, mcpManager, lspService, }) {
2083
- const theme = useTheme();
2084
- const innerWidth = Math.max(12, width - 4);
2085
- const todoCounts = todos.reduce((acc, todo) => {
2086
- acc[todo.status] = (acc[todo.status] ?? 0) + 1;
2087
- return acc;
2088
- }, {});
2089
- const todoSummary = todos.length === 0
2090
- ? "none"
2091
- : [
2092
- todoCounts.in_progress ? `${todoCounts.in_progress} active` : "",
2093
- todoCounts.pending ? `${todoCounts.pending} pending` : "",
2094
- todoCounts.completed ? `${todoCounts.completed} done` : "",
2095
- ].filter(Boolean).join(" · ");
2096
- const mcpStates = mcpManager?.getStates() ?? [];
2097
- const mcpSummary = summarizeMcpStates(mcpStates);
2098
- const lspSummary = lspService?.isDisabled()
2099
- ? { connected: 0, starting: 0, failed: 0, disabled: 1 }
2100
- : summarizeLspStatuses(lspService?.status() ?? []);
2101
- const latestMcpFailure = mcpStates.find((state) => state.status.kind === "failed");
2102
- const latestLspFailure = lspService?.status().find((status) => status.status === "error");
2103
- const sessionTitle = truncate(sessionDisplayName(sessionManager), innerWidth);
2104
- const modelLabel = agent.model ? displayModel(agent.model) : "not selected";
2105
- const route = agent.providerId
2106
- ? `${agent.providerId}/${modelLabel}`
2107
- : modelLabel;
2108
- 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) }))] })] })] }));
2109
- }
2110
2048
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2111
2049
  const GENERIC_PHRASES = [
2112
2050
  "mapping the workspace",
@@ -0,0 +1,19 @@
1
+ import type { CompactionProgress } from "../slash-commands/types.js";
2
+ /**
3
+ * Map a compaction phase + streamed length onto a 0..1 bar fill. There is no
4
+ * true denominator for a single LLM call, so the curve is honest by design:
5
+ * it ramps but never reaches 1.0 (or even 0.9) until the work is actually done.
6
+ */
7
+ export declare function compactionFraction(progress: CompactionProgress): number;
8
+ export declare function renderBar(fraction: number, width?: number): {
9
+ filled: string;
10
+ empty: string;
11
+ };
12
+ /**
13
+ * Bottom-stack progress card for a manual `/compact` run. Mount it only while a
14
+ * compaction is in flight (i.e. render conditionally on a non-null progress) so
15
+ * its elapsed-time clock resets per run.
16
+ */
17
+ export declare function CompactionProgressCard({ progress }: {
18
+ progress: CompactionProgress | null;
19
+ }): import("react/jsx-runtime").JSX.Element | null;