muonroi-cli 1.8.5 → 1.9.0

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 (206) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
  3. package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
  4. package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
  7. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  8. package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
  9. package/dist/src/agent-harness/mock-model.d.ts +10 -0
  10. package/dist/src/agent-harness/mock-model.js +6 -2
  11. package/dist/src/chat/chat-keychain.d.ts +7 -12
  12. package/dist/src/chat/chat-keychain.js +19 -86
  13. package/dist/src/cli/keys.d.ts +8 -45
  14. package/dist/src/cli/keys.js +18 -324
  15. package/dist/src/council/clarifier.d.ts +4 -2
  16. package/dist/src/council/clarifier.js +158 -36
  17. package/dist/src/council/debate-planner.js +3 -2
  18. package/dist/src/council/debate.js +59 -5
  19. package/dist/src/council/index.d.ts +23 -0
  20. package/dist/src/council/index.js +37 -2
  21. package/dist/src/council/llm.d.ts +62 -0
  22. package/dist/src/council/llm.js +123 -23
  23. package/dist/src/council/panel-select.js +13 -3
  24. package/dist/src/council/planner.js +17 -0
  25. package/dist/src/council/preflight.d.ts +10 -0
  26. package/dist/src/council/preflight.js +36 -0
  27. package/dist/src/council/prompts.d.ts +9 -1
  28. package/dist/src/council/prompts.js +23 -6
  29. package/dist/src/council/types.d.ts +10 -0
  30. package/dist/src/ee/auth.d.ts +19 -0
  31. package/dist/src/ee/auth.js +39 -0
  32. package/dist/src/ee/client.js +28 -3
  33. package/dist/src/ee/ee-onboarding.js +6 -26
  34. package/dist/src/flow/compaction/compress.d.ts +2 -2
  35. package/dist/src/flow/compaction/compress.js +21 -8
  36. package/dist/src/flow/compaction/extract.d.ts +3 -3
  37. package/dist/src/flow/compaction/extract.js +6 -6
  38. package/dist/src/flow/compaction/index.d.ts +2 -1
  39. package/dist/src/flow/compaction/index.js +29 -3
  40. package/dist/src/flow/compaction/progress.d.ts +35 -0
  41. package/dist/src/flow/compaction/progress.js +35 -0
  42. package/dist/src/generated/version.d.ts +1 -1
  43. package/dist/src/generated/version.js +1 -1
  44. package/dist/src/gsd/flags.d.ts +11 -0
  45. package/dist/src/gsd/flags.js +19 -0
  46. package/dist/src/gsd/plan-council.js +104 -72
  47. package/dist/src/gsd/verdict-schema.d.ts +1 -1
  48. package/dist/src/headless/council-answers.js +4 -0
  49. package/dist/src/index.js +129 -260
  50. package/dist/src/lsp/builtins.js +3 -1
  51. package/dist/src/lsp/manager.d.ts +5 -1
  52. package/dist/src/lsp/manager.js +249 -3
  53. package/dist/src/lsp/npm-cache.d.ts +11 -1
  54. package/dist/src/lsp/npm-cache.js +17 -1
  55. package/dist/src/lsp/runtime.d.ts +6 -1
  56. package/dist/src/lsp/runtime.js +17 -1
  57. package/dist/src/lsp/types.d.ts +83 -1
  58. package/dist/src/lsp/types.js +10 -0
  59. package/dist/src/mcp/client-pool.js +43 -15
  60. package/dist/src/mcp/lsp-tools.d.ts +5 -1
  61. package/dist/src/mcp/lsp-tools.js +93 -2
  62. package/dist/src/mcp/mcp-keychain.d.ts +3 -5
  63. package/dist/src/mcp/mcp-keychain.js +9 -49
  64. package/dist/src/mcp/setup-guide-text.d.ts +1 -1
  65. package/dist/src/mcp/setup-guide-text.js +22 -2
  66. package/dist/src/mcp/tools-server.d.ts +10 -0
  67. package/dist/src/mcp/tools-server.js +10 -2
  68. package/dist/src/models/catalog.json +19 -19
  69. package/dist/src/orchestrator/ask-user.d.ts +61 -0
  70. package/dist/src/orchestrator/ask-user.js +65 -0
  71. package/dist/src/orchestrator/compaction.d.ts +2 -3
  72. package/dist/src/orchestrator/compaction.js +8 -8
  73. package/dist/src/orchestrator/council-manager.js +9 -8
  74. package/dist/src/orchestrator/council-request.d.ts +49 -0
  75. package/dist/src/orchestrator/council-request.js +62 -0
  76. package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
  77. package/dist/src/orchestrator/interactive-pause.js +36 -0
  78. package/dist/src/orchestrator/message-processor.d.ts +4 -0
  79. package/dist/src/orchestrator/message-processor.js +26 -8
  80. package/dist/src/orchestrator/orchestrator.d.ts +25 -0
  81. package/dist/src/orchestrator/orchestrator.js +204 -50
  82. package/dist/src/orchestrator/preprocessor.js +2 -2
  83. package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
  84. package/dist/src/orchestrator/safety-askcard.js +5 -2
  85. package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
  86. package/dist/src/orchestrator/safety-intercept.js +7 -0
  87. package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
  88. package/dist/src/orchestrator/stall-watchdog.js +24 -3
  89. package/dist/src/orchestrator/stream-runner.d.ts +13 -3
  90. package/dist/src/orchestrator/stream-runner.js +54 -21
  91. package/dist/src/orchestrator/tool-engine.d.ts +19 -0
  92. package/dist/src/orchestrator/tool-engine.js +241 -25
  93. package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
  94. package/dist/src/orchestrator/turn-watchdog.js +38 -9
  95. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  96. package/dist/src/pil/agent-operating-contract.js +6 -4
  97. package/dist/src/pil/discovery.d.ts +1 -1
  98. package/dist/src/pil/discovery.js +2 -2
  99. package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
  100. package/dist/src/pil/layer1_5-complexity-size.js +31 -5
  101. package/dist/src/pil/llm-classify.d.ts +78 -3
  102. package/dist/src/pil/llm-classify.js +351 -111
  103. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  104. package/dist/src/pil/native-capabilities-workbook.js +7 -0
  105. package/dist/src/pil/pipeline.js +2 -0
  106. package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
  107. package/dist/src/pil/repo-grounding-probe.js +136 -0
  108. package/dist/src/pil/repo-structure-hints.d.ts +7 -0
  109. package/dist/src/pil/repo-structure-hints.js +45 -0
  110. package/dist/src/product-loop/artifact-io.js +4 -0
  111. package/dist/src/product-loop/criteria-seed.d.ts +51 -0
  112. package/dist/src/product-loop/criteria-seed.js +200 -0
  113. package/dist/src/product-loop/discovery-interview.d.ts +9 -0
  114. package/dist/src/product-loop/discovery-interview.js +37 -18
  115. package/dist/src/product-loop/discovery-recommender.js +2 -1
  116. package/dist/src/product-loop/discovery-schema.js +14 -1
  117. package/dist/src/product-loop/discovery-triage.d.ts +23 -0
  118. package/dist/src/product-loop/discovery-triage.js +109 -0
  119. package/dist/src/product-loop/gather.js +150 -2
  120. package/dist/src/product-loop/index.js +7 -0
  121. package/dist/src/product-loop/loop-driver.js +21 -8
  122. package/dist/src/product-loop/phase-plan.d.ts +16 -0
  123. package/dist/src/product-loop/phase-plan.js +42 -4
  124. package/dist/src/product-loop/phase-rituals.d.ts +3 -0
  125. package/dist/src/product-loop/phase-rituals.js +8 -3
  126. package/dist/src/product-loop/phase-runner.js +30 -11
  127. package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
  128. package/dist/src/product-loop/plan-adherence-review.js +144 -0
  129. package/dist/src/product-loop/sprint-runner.d.ts +62 -0
  130. package/dist/src/product-loop/sprint-runner.js +309 -8
  131. package/dist/src/product-loop/types.d.ts +25 -0
  132. package/dist/src/providers/anthropic.d.ts +9 -8
  133. package/dist/src/providers/anthropic.js +13 -47
  134. package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
  135. package/dist/src/providers/auth/grok-oauth.js +30 -5
  136. package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
  137. package/dist/src/providers/auth/openai-oauth.js +14 -0
  138. package/dist/src/providers/auth/token-store.d.ts +9 -9
  139. package/dist/src/providers/auth/token-store.js +8 -70
  140. package/dist/src/providers/auth/types.d.ts +8 -0
  141. package/dist/src/providers/env-store.d.ts +17 -0
  142. package/dist/src/providers/env-store.js +228 -0
  143. package/dist/src/providers/keychain.d.ts +21 -17
  144. package/dist/src/providers/keychain.js +124 -135
  145. package/dist/src/providers/runtime.d.ts +24 -9
  146. package/dist/src/providers/runtime.js +48 -37
  147. package/dist/src/providers/strategies/thinking-mode.js +9 -1
  148. package/dist/src/providers/strategies/xai.strategy.js +27 -0
  149. package/dist/src/providers/warm.d.ts +65 -0
  150. package/dist/src/providers/warm.js +145 -0
  151. package/dist/src/self-qa/agentic-loop.js +3 -2
  152. package/dist/src/storage/transcript.js +56 -2
  153. package/dist/src/tools/git-safety.d.ts +19 -0
  154. package/dist/src/tools/git-safety.js +168 -0
  155. package/dist/src/tools/native-tools.d.ts +1 -1
  156. package/dist/src/tools/native-tools.js +76 -1
  157. package/dist/src/tools/registry.d.ts +17 -0
  158. package/dist/src/tools/registry.js +116 -1
  159. package/dist/src/types/index.d.ts +29 -1
  160. package/dist/src/ui/app.js +67 -4
  161. package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
  162. package/dist/src/ui/components/agent-rail-activities.js +47 -0
  163. package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
  164. package/dist/src/ui/components/compact-progress-card.js +42 -0
  165. package/dist/src/ui/components/council-phase-timeline.js +17 -2
  166. package/dist/src/ui/components/council-question-card.js +1 -0
  167. package/dist/src/ui/components/message-view.d.ts +15 -0
  168. package/dist/src/ui/components/message-view.js +50 -1
  169. package/dist/src/ui/components/tool-group.d.ts +15 -3
  170. package/dist/src/ui/components/tool-group.js +69 -11
  171. package/dist/src/ui/containers/modals-layer.d.ts +2 -1
  172. package/dist/src/ui/containers/modals-layer.js +2 -2
  173. package/dist/src/ui/council-harness-event.d.ts +57 -0
  174. package/dist/src/ui/council-harness-event.js +46 -0
  175. package/dist/src/ui/heartbeat-debug.d.ts +29 -0
  176. package/dist/src/ui/heartbeat-debug.js +45 -0
  177. package/dist/src/ui/modals/api-key-modal.js +1 -1
  178. package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
  179. package/dist/src/ui/modals/model-picker-modal.js +8 -10
  180. package/dist/src/ui/slash/ee.js +81 -0
  181. package/dist/src/ui/slash/menu-items.js +11 -2
  182. package/dist/src/ui/use-app-logic.js +354 -224
  183. package/dist/src/ui/utils/agent-activities.d.ts +39 -0
  184. package/dist/src/ui/utils/agent-activities.js +96 -0
  185. package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
  186. package/dist/src/ui/utils/group-tool-entries.js +111 -0
  187. package/dist/src/ui/utils/tool-summary.d.ts +21 -0
  188. package/dist/src/ui/utils/tool-summary.js +91 -0
  189. package/dist/src/utils/event-loop-monitor.d.ts +85 -0
  190. package/dist/src/utils/event-loop-monitor.js +107 -0
  191. package/dist/src/utils/llm-deadline.d.ts +14 -0
  192. package/dist/src/utils/llm-deadline.js +19 -0
  193. package/dist/src/utils/loop-profiler.d.ts +102 -0
  194. package/dist/src/utils/loop-profiler.js +202 -0
  195. package/dist/src/utils/settings.d.ts +27 -0
  196. package/dist/src/utils/settings.js +38 -2
  197. package/dist/src/utils/side-question.d.ts +1 -2
  198. package/dist/src/utils/side-question.js +2 -2
  199. package/dist/src/verify/entrypoint.js +51 -16
  200. package/dist/src/verify/orchestrator.d.ts +1 -1
  201. package/dist/src/verify/orchestrator.js +20 -3
  202. package/package.json +1 -2
  203. package/dist/src/cli/bw-vault.d.ts +0 -55
  204. package/dist/src/cli/bw-vault.js +0 -133
  205. package/dist/src/mcp/ee-tools.d.ts +0 -46
  206. package/dist/src/mcp/ee-tools.js +0 -194
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { Semantic, SemanticProvider, useAgentInputBridge } from "@muonroi/agent-harness-opentui";
3
3
  import os from "os";
4
+ import { useEffect, useMemo, useState } from "react";
4
5
  import { isContextRailEnabled, isRoundGroupsEnabled } from "../gsd/flags.js";
5
6
  import { getCurrentModel, } from "../utils/settings.js";
6
7
  import { formatSubagentName } from "../utils/subagent-display.js";
@@ -9,6 +10,8 @@ import { ProductStatusCard } from "./cards/product-status-card.js";
9
10
  import { BtwOverlay } from "./components/btw-overlay.js";
10
11
  import { makePairKey } from "./components/bubble-layout.js";
11
12
  import { ContextRail } from "./components/context-rail.js";
13
+ import { AgentRailActivities } from "./components/agent-rail-activities.js";
14
+ import { CompactProgressCard } from "./components/compact-progress-card.js";
12
15
  import { CopyFlashBanner } from "./components/copy-flash-banner.js";
13
16
  import { CouncilDebatePill } from "./components/council-debate-pill.js";
14
17
  import { CouncilInfoCardView } from "./components/council-info-card.js";
@@ -64,6 +67,7 @@ import "./slash/status.js";
64
67
  import "./slash/ponytail.js";
65
68
  import { CONNECT_CHANNELS, } from "./constants.js";
66
69
  import { buildPreflightQuestion, } from "./utils/format.js";
70
+ import { collectAgentActivities } from "./utils/agent-activities.js";
67
71
  import { toolArgs, toolLabel, tryParseArg } from "./utils/tools.js";
68
72
  /**
69
73
  * Render the EE "experience injected" chunk for the TUI: the count line PLUS a
@@ -373,7 +377,15 @@ ${prompt}`;
373
377
  const SPLASH_PROVIDERS = ["deepseek", "zai", "opencode-go", "xai"];
374
378
  import { useAppLogic } from "./use-app-logic.js";
375
379
  export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }) {
376
- const { activeHaltCard, activeSubagent, activeToast, activeToolCalls, agentRows, agentRuntime, agentsEditorDraft, agentsEditorError, agentsEditorField, agentsEditorModelIndex, agentsEditorSyncKey, agentsModalIndex, agentsSearchQuery, apiKeyError, apiKeyInputRef, apiKeyPrompt, blockPrompt, btwState, bwSync, configuredProviders, connectModalIndex, contextStats, copyFlashId, councilCardState, councilInfoCards, councilMeta, councilRounds, selectedRound, setSelectedRound, councilMessages, councilTranscriptExpanded, councilPhases, councilPlaceholders, councilProgress, councilStatuses, councilTodoExpanded, defaultProvider, disabledModels, disabledProviders, dismissToast, editingMcpId, editingSubagent, expandedMessages, filteredModels, filteredSlashItems, getPartnerLast, getSide, haltSelectedIndex, handlePaste, handleRootMouseDown, handleRootMouseUp, handleSubmit, hasMessages, height, initNewForm, inputRef, isProcessing, isUpdating, lastReasoningElapsedMs, liveTurnSourceLabel, mcpArgsRef, mcpCommandRef, mcpCwdRef, mcpEditorDraft, mcpEditorError, mcpEditorField, mcpEditorSyncKey, mcpEnvRef, mcpHeadersRef, mcpLabelRef, mcpModalIndex, mcpRows, mcpSearchQuery, mcpUrlRef, messages, modeInfo, model, modelInfo, modelPickerFocus, modelPickerIndex, modelSearchQuery, pasteBlocks, pendingCouncilPreflight, pendingCouncilQuestion, pendingPaymentApproval, planQuestions, pointToExistingForm, pqs, preflightCardState, productStatus, providerChipIndex, providersWithKey, queuedMessages, reasoningActive, reasoningEffortByModel, resolveStyle, sandboxMode, sandboxSettings, sandboxSettingsEditBuffer, sandboxSettingsEditing, sandboxSettingsFocusIndex, scheduleModalIndex, scheduleRows, scheduleSearchQuery, scrollRef, newSinceLock, scrollLockedAway, railVisible, sessionId, sessionPickerIndex, sessionPickerList, sessionTitle, sessionTree, showAgentsEditor, showAgentsModal, showApiKeyModal, showConnectModal, showCopyBanner, showMcpEditor, showMcpModal, showModelPicker, showPlanPanel, showSandboxPicker, showScheduleModal, showSessionPicker, showSlashMenu, showTelegramPairModal, showTelegramTokenModal, showUpdateModal, showWalletPicker, slashInputIsMatched, slashMenuIndex, slashSearchQuery, streamContent, streamReasoning, subagentInstructionRef, subagentNameRef, submitApiKey, submitMcpEditor, submitSubagentEditor, submitTelegramPair, submitTelegramToken, t, taskListSnapshot, telegramPairError, telegramPairInputRef, telegramTokenError, telegramTokenInputRef, typeahead, updateInfo, updateOutput, walletDisplayInfo, walletFocusIndex, walletSettings, width, } = useAppLogic({ agent, startupConfig, initialMessage, onExit, onRelaunch });
380
+ const { activeHaltCard, activeSubagent, activeToast, activeToolCalls, agentRows, agentRuntime, agentsEditorDraft, agentsEditorError, agentsEditorField, agentsEditorModelIndex, agentsEditorSyncKey, agentsModalIndex, agentsSearchQuery, apiKeyError, apiKeyInputRef, apiKeyPrompt, blockPrompt, btwState, oauthLogin, oauthProviders, configuredProviders, connectModalIndex, contextStats, copyFlashId, councilCardState, councilInfoCards, councilMeta, councilRounds, selectedRound, setSelectedRound, councilMessages, councilTranscriptExpanded, councilPhases, councilPlaceholders, councilProgress, councilStatuses, councilTodoExpanded, compactRun, defaultProvider, disabledModels, disabledProviders, dismissToast, editingMcpId, editingSubagent, expandedMessages, filteredModels, filteredSlashItems, getPartnerLast, getSide, haltSelectedIndex, handlePaste, handleRootMouseDown, handleRootMouseUp, handleSubmit, hasMessages, height, initNewForm, inputRef, isProcessing, isUpdating, lastReasoningElapsedMs, liveTurnSourceLabel, mcpArgsRef, mcpCommandRef, mcpCwdRef, mcpEditorDraft, mcpEditorError, mcpEditorField, mcpEditorSyncKey, mcpEnvRef, mcpHeadersRef, mcpLabelRef, mcpModalIndex, mcpRows, mcpSearchQuery, mcpUrlRef, messages, modeInfo, model, modelInfo, modelPickerFocus, modelPickerIndex, modelSearchQuery, pasteBlocks, pendingCouncilPreflight, pendingCouncilQuestion, pendingPaymentApproval, planQuestions, pointToExistingForm, pqs, preflightCardState, productStatus, providerChipIndex, providersWithKey, queuedMessages, reasoningActive, reasoningEffortByModel, resolveStyle, sandboxMode, sandboxSettings, sandboxSettingsEditBuffer, sandboxSettingsEditing, sandboxSettingsFocusIndex, scheduleModalIndex, scheduleRows, scheduleSearchQuery, scrollRef, newSinceLock, scrollLockedAway, railVisible, sessionId, sessionPickerIndex, sessionPickerList, sessionTitle, sessionTree, showAgentsEditor, showAgentsModal, showApiKeyModal, showConnectModal, showCopyBanner, showMcpEditor, showMcpModal, showModelPicker, showPlanPanel, showSandboxPicker, showScheduleModal, showSessionPicker, showSlashMenu, showTelegramPairModal, showTelegramTokenModal, showUpdateModal, showWalletPicker, slashInputIsMatched, slashMenuIndex, slashSearchQuery, streamContent, streamReasoning, subagentInstructionRef, subagentNameRef, submitApiKey, submitMcpEditor, submitSubagentEditor, submitTelegramPair, submitTelegramToken, t, taskListSnapshot, telegramPairError, telegramPairInputRef, telegramTokenError, telegramTokenInputRef, typeahead, updateInfo, updateOutput, walletDisplayInfo, walletFocusIndex, walletSettings, width, } = useAppLogic({ agent, startupConfig, initialMessage, onExit, onRelaunch });
381
+ // True while a tool group is streaming its items. Gates the legacy inline
382
+ // activeToolCalls list so a running tool isn't printed twice (see below).
383
+ const hasActiveToolGroup = useMemo(() => messages.some((m) => m.type === "tool_group" && m.toolGroup?.state === "active"), [messages]);
384
+ // Roster of every sub-agent / background job spawned this session, for the
385
+ // rail. Derived from the transcript + in-flight calls, so it cannot drift from
386
+ // what actually ran.
387
+ const agentActivities = useMemo(() => collectAgentActivities(messages, activeToolCalls), [messages, activeToolCalls]);
388
+ const [selectedActivity, setSelectedActivity] = useState(null);
377
389
  // Agent-mode input bridge: translate harness {op:"type"/"press"} commands into
378
390
  // synthetic OpenTUI keypresses (via the shared useAppContext().keyHandler) so
379
391
  // an external driver (MCP harness / vitest spec) can actually type into the
@@ -381,6 +393,40 @@ export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }
381
393
  // had no consumer, so type/press silently no-opped (frames flowed out, input
382
394
  // was dead). No-op when agentRuntime is undefined (normal interactive mode).
383
395
  useAgentInputBridge(agentRuntime);
396
+ // Publish the transcript scrollbox's geometry so a spec can tell WHY a scroll
397
+ // assertion failed. Without it, "the lock did not engage" and "the transcript
398
+ // never overflowed, so there was nothing to scroll away from" are the same
399
+ // observation — which is what made scroll-lock.spec unfixable by inspection
400
+ // (measured: scrollHeight 27 == viewportH 27 at the instant `idle` fired).
401
+ //
402
+ // Opt-in (MUONROI_HARNESS_SCROLL_GEOM=1) on top of agent-mode, so a spec that
403
+ // does not ask for it is bit-for-bit unaffected: this adds a timer and extra
404
+ // frames, and the harness shares a process boundary with a native renderer
405
+ // that segfaults under load. Observability must not perturb the thing it
406
+ // observes.
407
+ //
408
+ // Sampled on an interval, never read during render: scrollHeight/viewport are
409
+ // layout getters, and sampling them outside the render pass keeps them from
410
+ // feeding back into it. setState only on a real change, so a settled
411
+ // transcript stops producing frames.
412
+ const [scrollGeom, setScrollGeom] = useState(null);
413
+ useEffect(() => {
414
+ if (!agentRuntime || process.env.MUONROI_HARNESS_SCROLL_GEOM !== "1")
415
+ return;
416
+ const read = () => {
417
+ const sb = scrollRef.current;
418
+ if (!sb)
419
+ return;
420
+ const next = {
421
+ vpH: sb.viewport?.height ?? -1,
422
+ scrollH: sb.scrollHeight ?? -1,
423
+ manual: Boolean(sb._hasManualScroll),
424
+ };
425
+ setScrollGeom((prev) => prev && prev.vpH === next.vpH && prev.scrollH === next.scrollH && prev.manual === next.manual ? prev : next);
426
+ };
427
+ const id = setInterval(read, 200);
428
+ return () => clearInterval(id);
429
+ }, [agentRuntime, scrollRef]);
384
430
  // Context rail (MUONROI_CONTEXT_RAIL): only render when enabled, the user
385
431
  // hasn't hidden it (Ctrl+B), and the terminal is wide enough that a fixed
386
432
  // side panel doesn't starve the transcript. Below 100 cols it stays inline.
@@ -466,7 +512,22 @@ export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }
466
512
  const renderCouncilMeta = (cols) => (_jsxs(_Fragment, { children: [councilPhases.length > 0 && (_jsx(Semantic, { id: "council-phases", role: "listbox", name: "Council Phases", children: _jsx(CouncilPhaseTimeline, { phases: councilPhases, theme: t, expanded: councilTranscriptExpanded }) })), productStatus && _jsx(ProductStatusCard, { data: productStatus, theme: t }), councilStatuses.length > 0 && (_jsx(Semantic, { id: "council-status", role: "listbox", name: "Council Status", children: _jsx(CouncilStatusList, { statuses: councilStatuses, theme: t, sessionId: sessionId ?? undefined }) })), councilInfoCards.map((card, idx) => (_jsx(Semantic, { id: `council-card-${idx}`, role: "listitem", name: card.title || `Council card ${idx}`, children: _jsx(CouncilInfoCardView, { card: card, terminalCols: cols, theme: t }, `info-card-${idx}-${card.title}`) }, `sem-info-${idx}-${card.title}`))), isRoundGroupsEnabled() && councilRounds.length > 0 && (_jsx(CouncilRailRounds, { rounds: councilRounds, selected: selectedRound, onSelect: setSelectedRound, width: cols, theme: t }))] }));
467
513
  return (_jsx(SemanticProvider, { registry: agentRuntime
468
514
  ? agentRuntime.registry
469
- : { register: () => () => { }, update: () => { }, snapshot: () => ({ nodes: [] }), clear: () => { } }, children: _jsxs("box", { width: width, height: height, backgroundColor: t.background, flexDirection: "column", onMouseUp: handleRootMouseUp, onMouseDown: handleRootMouseDown, children: [copyFlashId > 0 ? _jsx(CopyFlashBanner, { t: t, width: width }) : null, hasMessages ? (_jsxs("box", { flexGrow: 1, flexDirection: "column", children: [_jsx(SessionHeader, { t: t, modeInfo: modeInfo, sessionTitle: sessionTitle, sessionId: sessionId, onCopySessionId: showCopyBanner }), _jsxs("box", { flexGrow: 1, paddingBottom: 1, paddingTop: 1, paddingLeft: 2, paddingRight: 2, gap: 1, flexDirection: "row", children: [_jsxs("box", { flexDirection: "column", flexGrow: 1, gap: 1, children: [_jsx(Semantic, { id: "log", role: "log", props: { scrollTop: scrollRef.current?.scrollTop ?? 0, locked: scrollLockedAway, newSinceLock }, children: _jsxs("scrollbox", { ref: scrollRef, flexGrow: 1, stickyScroll: true, stickyStart: "bottom", children: [(() => {
515
+ : { register: () => () => { }, update: () => { }, snapshot: () => ({ nodes: [] }), clear: () => { } }, children: _jsxs("box", { width: width, height: height, backgroundColor: t.background, flexDirection: "column", onMouseUp: handleRootMouseUp, onMouseDown: handleRootMouseDown, children: [copyFlashId > 0 ? _jsx(CopyFlashBanner, { t: t, width: width }) : null, hasMessages ? (_jsxs("box", { flexGrow: 1, flexDirection: "column", children: [_jsx(SessionHeader, { t: t, modeInfo: modeInfo, sessionTitle: sessionTitle, sessionId: sessionId, onCopySessionId: showCopyBanner }), _jsxs("box", { flexGrow: 1, paddingBottom: 1, paddingTop: 1, paddingLeft: 2, paddingRight: 2, gap: 1, flexDirection: "row", children: [_jsxs("box", { flexDirection: "column", flexGrow: 1, gap: 1, children: [_jsx(Semantic, { id: "log", role: "log", props: {
516
+ scrollTop: scrollRef.current?.scrollTop ?? 0,
517
+ locked: scrollLockedAway,
518
+ newSinceLock,
519
+ // Present only in agent-mode (see the sampler above).
520
+ ...(scrollGeom
521
+ ? {
522
+ viewportH: scrollGeom.vpH,
523
+ scrollHeight: scrollGeom.scrollH,
524
+ manualScroll: scrollGeom.manual,
525
+ // The one derived answer every scroll spec actually
526
+ // wants: is there anything to scroll away from?
527
+ overflows: scrollGeom.scrollH > scrollGeom.vpH,
528
+ }
529
+ : {}),
530
+ }, children: _jsxs("scrollbox", { ref: scrollRef, flexGrow: 1, stickyScroll: true, stickyStart: "bottom", children: [(() => {
470
531
  const mcpRuns = computeMcpRunInfo(messages);
471
532
  // Phase 5 F7 — index of the last assistant message so
472
533
  // MessageView can skip auto-collapse on it (final answer
@@ -482,7 +543,9 @@ export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }
482
543
  return messages.map((msg, i) => (
483
544
  // biome-ignore lint/suspicious/noArrayIndexKey: append-only message log; index is part of the stable semantic id
484
545
  _jsx(Semantic, { id: `msg-${i}`, role: "listitem", name: `${msg.type ?? "msg"}:${String(msg.content ?? "").slice(0, 40)}`, children: _jsx(MessageView, { entry: msg, index: i, t: t, modeColor: modeInfo.color, expandedMessages: expandedMessages, mcpRun: mcpRuns[i], isFinalAssistant: i === _lastAssistantIdx }, `${msg.timestamp?.getTime?.() ?? i}-${msg.type}-${msg.remoteKey ?? ""}-${String(msg.content ?? "").slice(0, 24)}`) }, `sem-${msg.timestamp?.getTime?.() ?? i}-${i}`)));
485
- })(), liveTurnSourceLabel && (activeToolCalls.length > 0 || streamContent || isProcessing) && (_jsx("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, children: _jsx("text", { fg: t.textMuted, children: liveTurnSourceLabel }) })), activeToolCalls.map((tc) => tc.function.name === "task" ? (_jsx(SubagentTaskLine, { t: t, agent: tryParseArg(tc, "agent") || "sub-agent", label: toolArgs(tc) || "Working", pending: true }, tc.id)) : tc.function.name === "delegate" ? (_jsx(DelegationTaskLine, { t: t, label: toolArgs(tc) || "Background research", pending: true, id: undefined }, tc.id)) : (_jsx(InlineTool, { t: t, pending: true, children: toolLabel(tc) }, tc.id))), activeSubagent && _jsx(SubagentActivity, { t: t, status: activeSubagent }), !railActive && renderCouncilMeta(width), (() => {
546
+ })(), liveTurnSourceLabel && (activeToolCalls.length > 0 || streamContent || isProcessing) && (_jsx("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, children: _jsx("text", { fg: t.textMuted, children: liveTurnSourceLabel }) })), activeToolCalls
547
+ .filter((tc) => !hasActiveToolGroup || tc.function.name === "task" || tc.function.name === "delegate")
548
+ .map((tc) => tc.function.name === "task" ? (_jsx(SubagentTaskLine, { t: t, agent: tryParseArg(tc, "agent") || "sub-agent", label: toolArgs(tc) || "Working", pending: true }, tc.id)) : tc.function.name === "delegate" ? (_jsx(DelegationTaskLine, { t: t, label: toolArgs(tc) || "Background research", pending: true, id: undefined }, tc.id)) : (_jsx(InlineTool, { t: t, pending: true, children: toolLabel(tc) }, tc.id))), activeSubagent && _jsx(SubagentActivity, { t: t, status: activeSubagent }), compactRun && (_jsx(CompactProgressCard, { t: t, progress: compactRun.progress, startedAt: compactRun.startedAt })), !railActive && renderCouncilMeta(width), (() => {
486
549
  const turns = councilMessages.map((cm, idx) => ({ cm, idx }));
487
550
  const debateTurns = turns.filter(({ cm }) => cm.kind !== "synthesis");
488
551
  const synthesisTurns = turns.filter(({ cm }) => cm.kind === "synthesis");
@@ -556,7 +619,7 @@ export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }
556
619
  !pendingCouncilQuestion &&
557
620
  !pendingCouncilPreflight && _jsx(ShimmerText, { t: t, text: "Planning next moves" }), showPlanPanel && _jsx(PlanQuestionsPanel, { t: t, questions: planQuestions, state: pqs }), pendingPaymentApproval && _jsx(PaymentApprovalPanel, { t: t, payment: pendingPaymentApproval }), activeHaltCard && (_jsx(HaltRecoveryCard, { halt: activeHaltCard, selectedIndex: haltSelectedIndex, terminalCols: width, theme: t })), initNewForm && _jsx(InitNewFormCard, { state: initNewForm, terminalCols: width, theme: t }), pointToExistingForm && (_jsx(PointToExistingFormCard, { state: pointToExistingForm, terminalCols: width, theme: t })), councilProgress && (_jsx(Semantic, { id: "continue-as-council-progress", role: "log", name: "Council brainstorm", children: _jsx("box", { flexDirection: "column", borderStyle: "single", borderColor: councilProgress.status === "error" ? t.initFormError : t.text, padding: 1, marginTop: 1, children: _jsxs("text", { fg: t.text, children: [councilProgress.status === "running" && "Council brainstorming — writing spec.md...", councilProgress.status === "done" &&
558
621
  `Council brainstorm complete: ${councilProgress.specPath}${councilProgress.hasContent ? "" : " (no content — production council wiring deferred)"}`, councilProgress.status === "error" &&
559
- `Council brainstorm failed: ${councilProgress.error}`] }) }) })), pendingCouncilQuestion && councilCardState && (_jsx(CouncilQuestionCard, { question: pendingCouncilQuestion, theme: t, state: councilCardState })), pendingCouncilPreflight && preflightCardState && (_jsx(CouncilQuestionCard, { question: buildPreflightQuestion(pendingCouncilPreflight), theme: t, state: preflightCardState }))] }) }), scrollLockedAway && (_jsx("box", { flexShrink: 0, alignItems: "center", marginTop: 0, children: _jsx(JumpToLatestPill, { newSinceLock: newSinceLock }) })), btwState && _jsx(BtwOverlay, { state: btwState, theme: t }), taskListSnapshot && (_jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, marginBottom: 1, children: _jsx(TaskListPanel, { snapshot: taskListSnapshot, t: t, expanded: false, collapsed: councilStatuses.length > 0 && !councilTodoExpanded }) })), activeToast ? (_jsx("box", { flexShrink: 0, marginBottom: 1, children: _jsx(Toast, { level: activeToast.level, text: activeToast.text, theme: t, onDismiss: dismissToast }, activeToast.id) })) : null, _jsx("box", { flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) })] }), railActive && (_jsxs(ContextRail, { width: railWidth, rows: railRows, children: [_jsx(SessionTreeCard, { nodes: sessionTree }), renderCouncilMeta(railWidth)] }))] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsxs("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: [activeToast ? (_jsx("box", { marginBottom: 1, flexShrink: 0, children: _jsx(Toast, { level: activeToast.level, text: activeToast.text, theme: t, onDismiss: dismissToast }, activeToast.id) })) : null, _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined })] }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, bwSync: bwSync })), showSessionPicker && (_jsx(SessionPickerModal, { t: t, sessions: sessionPickerList, focusIndex: sessionPickerIndex, width: width, height: height })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
622
+ `Council brainstorm failed: ${councilProgress.error}`] }) }) })), pendingCouncilQuestion && councilCardState && (_jsx(CouncilQuestionCard, { question: pendingCouncilQuestion, theme: t, state: councilCardState })), pendingCouncilPreflight && preflightCardState && (_jsx(CouncilQuestionCard, { question: buildPreflightQuestion(pendingCouncilPreflight), theme: t, state: preflightCardState }))] }) }), scrollLockedAway && (_jsx("box", { flexShrink: 0, alignItems: "center", marginTop: 0, children: _jsx(JumpToLatestPill, { newSinceLock: newSinceLock }) })), btwState && _jsx(BtwOverlay, { state: btwState, theme: t }), taskListSnapshot && (_jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, marginBottom: 1, children: _jsx(TaskListPanel, { snapshot: taskListSnapshot, t: t, expanded: false, collapsed: councilStatuses.length > 0 && !councilTodoExpanded }) })), activeToast ? (_jsx("box", { flexShrink: 0, marginBottom: 1, children: _jsx(Toast, { level: activeToast.level, text: activeToast.text, theme: t, onDismiss: dismissToast }, activeToast.id) })) : null, _jsx("box", { flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) })] }), railActive && (_jsxs(ContextRail, { width: railWidth, rows: railRows, children: [_jsx(SessionTreeCard, { nodes: sessionTree }), _jsx(AgentRailActivities, { activities: agentActivities, selected: selectedActivity, onSelect: setSelectedActivity, width: railWidth, t: t }), renderCouncilMeta(railWidth)] }))] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsxs("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: [activeToast ? (_jsx("box", { marginBottom: 1, flexShrink: 0, children: _jsx(Toast, { level: activeToast.level, text: activeToast.text, theme: t, onDismiss: dismissToast }, activeToast.id) })) : null, _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined })] }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, oauthProviders: oauthProviders, oauthLogin: oauthLogin })), showSessionPicker && (_jsx(SessionPickerModal, { t: t, sessions: sessionPickerList, focusIndex: sessionPickerIndex, width: width, height: height })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
560
623
  }
561
624
  export { computeMcpRunInfo } from "./components/message-view.js";
562
625
  function shouldOpenApiKeyModalForKey(key) {
@@ -0,0 +1,26 @@
1
+ /**
2
+ * src/ui/components/agent-rail-activities.tsx
3
+ *
4
+ * Rail block listing every sub-agent / background job this session spawned.
5
+ *
6
+ * Before this, a spawned agent was visible only as a transient line in the
7
+ * transcript: it scrolled away, and once done there was nowhere to see WHAT it
8
+ * was asked or what it returned. The rail keeps the roster on screen, and a row
9
+ * opens to show the prompt and the result.
10
+ *
11
+ * Selection is by click (OpenTUI onMouseDown per row, same as
12
+ * council-rail-rounds.tsx) — clicking the open row closes it again.
13
+ */
14
+ import type { AgentActivity } from "../utils/agent-activities.js";
15
+ import type { Theme } from "../theme.js";
16
+ /** Row prefix naming what was spawned: the agent type, or "shell" for a job. */
17
+ export declare function activityPrefix(a: AgentActivity): string;
18
+ export interface AgentRailActivitiesProps {
19
+ activities: AgentActivity[];
20
+ /** Tool-call id of the open row, or null when all are collapsed. */
21
+ selected: string | null;
22
+ onSelect: (id: string | null) => void;
23
+ width: number;
24
+ t: Theme;
25
+ }
26
+ export declare function AgentRailActivities({ activities, selected, onSelect, width, t }: AgentRailActivitiesProps): import("react").ReactNode;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ /**
3
+ * src/ui/components/agent-rail-activities.tsx
4
+ *
5
+ * Rail block listing every sub-agent / background job this session spawned.
6
+ *
7
+ * Before this, a spawned agent was visible only as a transient line in the
8
+ * transcript: it scrolled away, and once done there was nowhere to see WHAT it
9
+ * was asked or what it returned. The rail keeps the roster on screen, and a row
10
+ * opens to show the prompt and the result.
11
+ *
12
+ * Selection is by click (OpenTUI onMouseDown per row, same as
13
+ * council-rail-rounds.tsx) — clicking the open row closes it again.
14
+ */
15
+ import { ListItem, Region } from "../primitives/semantic-primitives.js";
16
+ import { trunc } from "../utils/text.js";
17
+ /** Characters of detail shown when a row is open. Keeps the rail scrollable. */
18
+ const DETAIL_LIMIT = 600;
19
+ const GLYPH = { running: "▸", done: "✓", failed: "✗" };
20
+ function statusColor(status, t) {
21
+ if (status === "failed")
22
+ return t.diffRemovedFg;
23
+ if (status === "done")
24
+ return t.diffAddedFg;
25
+ return t.accent;
26
+ }
27
+ /** Row prefix naming what was spawned: the agent type, or "shell" for a job. */
28
+ export function activityPrefix(a) {
29
+ if (a.kind === "background")
30
+ return "shell";
31
+ return a.agent || (a.kind === "delegate" ? "delegate" : "sub-agent");
32
+ }
33
+ export function AgentRailActivities({ activities, selected, onSelect, width, t }) {
34
+ if (activities.length === 0)
35
+ return null;
36
+ const running = activities.filter((a) => a.status === "running").length;
37
+ return (_jsx(Region, { id: "agent-rail-activities", name: "Agents", value: `${activities.length}`, props: {
38
+ count: activities.length,
39
+ running,
40
+ selected: selected ?? "",
41
+ ids: activities.map((a) => a.id).join(","),
42
+ }, children: _jsxs("box", { flexDirection: "column", flexShrink: 0, children: [_jsx("text", { fg: t.textMuted, attributes: 1, children: running > 0 ? `Agents (${running} running)` : "Agents" }), activities.map((a) => {
43
+ const isOpen = selected === a.id;
44
+ return (_jsx(ListItem, { id: `rail-agent-${a.id}`, name: `${activityPrefix(a)} ${a.label}`, state: a.status, selected: isOpen || undefined, children: _jsxs("box", { flexDirection: "column", flexShrink: 0, children: [_jsx("box", { flexDirection: "row", onMouseDown: () => onSelect(isOpen ? null : a.id), children: _jsxs("text", { children: [_jsx("span", { style: { fg: statusColor(a.status, t) }, children: `${GLYPH[a.status]} ` }), _jsx("span", { style: { fg: t.textMuted }, children: `${activityPrefix(a)} ` }), _jsx("span", { style: { fg: t.text }, children: trunc(a.label, Math.max(8, width - 12)) })] }) }), isOpen && (_jsx("box", { paddingLeft: 2, flexDirection: "column", children: _jsx("text", { fg: t.textDim, children: a.detail.trim() ? trunc(a.detail, DETAIL_LIMIT) : "(no output yet)" }) }))] }) }, a.id));
45
+ })] }) }));
46
+ }
47
+ //# sourceMappingURL=agent-rail-activities.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * src/ui/components/compact-progress-card.tsx
3
+ *
4
+ * Live progress for a /compact run. Before this, `/compact` awaited two LLM
5
+ * passes with zero UI — a minute of a frozen screen that reads as a hang.
6
+ *
7
+ * The percentage comes from the compaction pipeline itself
8
+ * (flow/compaction/progress.ts), not from a timer: it advances on real stage
9
+ * boundaries, and the dominant compress pass streams so it moves continuously.
10
+ * The elapsed clock is what proves liveness while a stage is mid-flight.
11
+ */
12
+ import type { CompactProgress } from "../../flow/compaction/progress.js";
13
+ import type { Theme } from "../theme.js";
14
+ /** Elapsed as the status line shows it: "42s", "1m 19s". */
15
+ export declare function fmtCompactElapsed(ms: number): string;
16
+ /** Filled/unfilled bar of exactly `width` cells for a 0..100 percent. */
17
+ export declare function compactBar(percent: number, width?: number): string;
18
+ export interface CompactProgressCardProps {
19
+ progress: CompactProgress;
20
+ /** Epoch ms the run started. */
21
+ startedAt: number;
22
+ t: Theme;
23
+ }
24
+ export declare function CompactProgressCard({ progress, startedAt, t }: CompactProgressCardProps): import("react").ReactNode;
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
+ /**
3
+ * src/ui/components/compact-progress-card.tsx
4
+ *
5
+ * Live progress for a /compact run. Before this, `/compact` awaited two LLM
6
+ * passes with zero UI — a minute of a frozen screen that reads as a hang.
7
+ *
8
+ * The percentage comes from the compaction pipeline itself
9
+ * (flow/compaction/progress.ts), not from a timer: it advances on real stage
10
+ * boundaries, and the dominant compress pass streams so it moves continuously.
11
+ * The elapsed clock is what proves liveness while a stage is mid-flight.
12
+ */
13
+ import { useEffect, useState } from "react";
14
+ import { Region } from "../primitives/semantic-primitives.js";
15
+ const BAR_WIDTH = 34;
16
+ /** Elapsed as the status line shows it: "42s", "1m 19s". */
17
+ export function fmtCompactElapsed(ms) {
18
+ const totalSec = Math.max(0, Math.floor(ms / 1000));
19
+ if (totalSec < 60)
20
+ return `${totalSec}s`;
21
+ return `${Math.floor(totalSec / 60)}m ${totalSec % 60}s`;
22
+ }
23
+ /** Filled/unfilled bar of exactly `width` cells for a 0..100 percent. */
24
+ export function compactBar(percent, width = BAR_WIDTH) {
25
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(percent) ? percent : 0));
26
+ const filled = Math.round((clamped / 100) * width);
27
+ return `${"█".repeat(filled)}${"░".repeat(Math.max(0, width - filled))}`;
28
+ }
29
+ export function CompactProgressCard({ progress, startedAt, t }) {
30
+ // The card owns its clock: a compaction can sit inside one LLM pass for a
31
+ // minute, and re-rendering the whole transcript once a second just to advance
32
+ // a timer is a cost the rest of the app should not pay.
33
+ const [now, setNow] = useState(() => Date.now());
34
+ useEffect(() => {
35
+ const id = setInterval(() => setNow(Date.now()), 1000);
36
+ return () => clearInterval(id);
37
+ }, []);
38
+ const elapsed = fmtCompactElapsed(now - startedAt);
39
+ const pct = Math.max(0, Math.min(100, Math.round(progress.percent)));
40
+ return (_jsx(Region, { id: "compact-progress", name: progress.label, value: `${pct}%`, state: progress.stage, props: { percent: pct, elapsedMs: now - startedAt }, children: _jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: t.accent }, children: "✦ " }), _jsx("span", { style: { fg: t.text }, children: progress.label }), _jsx("span", { style: { fg: t.textMuted }, children: ` (${elapsed})` })] }), _jsx("box", { paddingLeft: 2, children: _jsxs("text", { children: [_jsx("span", { style: { fg: t.accent }, children: compactBar(pct) }), _jsx("span", { style: { fg: t.textMuted }, children: ` ${pct}%` })] }) })] }) }));
41
+ }
42
+ //# sourceMappingURL=compact-progress-card.js.map
@@ -1,10 +1,14 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
+ import { heartbeatDebug } from "../heartbeat-debug.js";
3
4
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
5
  function Spinner() {
5
6
  const [frame, setFrame] = useState(0);
6
7
  useEffect(() => {
7
- const id = setInterval(() => setFrame((n) => (n + 1) % SPINNER_FRAMES.length), 100);
8
+ const id = setInterval(() => {
9
+ heartbeatDebug("phase-timeline-spinner", "timer");
10
+ setFrame((n) => (n + 1) % SPINNER_FRAMES.length);
11
+ }, 100);
8
12
  return () => clearInterval(id);
9
13
  }, []);
10
14
  return _jsx(_Fragment, { children: SPINNER_FRAMES[frame] });
@@ -31,7 +35,10 @@ function formatElapsed(ms) {
31
35
  function useHeartbeat(tickMs = 1000) {
32
36
  const [, force] = useState(0);
33
37
  useEffect(() => {
34
- const id = setInterval(() => force((n) => n + 1), tickMs);
38
+ const id = setInterval(() => {
39
+ heartbeatDebug("phase-timeline-heartbeat", "timer");
40
+ force((n) => n + 1);
41
+ }, tickMs);
35
42
  return () => clearInterval(id);
36
43
  }, [tickMs]);
37
44
  return Date.now();
@@ -74,6 +81,14 @@ export function CouncilPhaseTimeline({ phases, theme: t, expanded = false }) {
74
81
  const markerColor = isDone ? t.planOptionCheck : t.accent;
75
82
  const labelColor = isDone ? t.textMuted : t.text;
76
83
  const liveElapsedMs = p.state === "active" && typeof p.startedAt === "number" ? Math.max(0, now - p.startedAt) : p.elapsedMs;
84
+ if (p.state === "active") {
85
+ heartbeatDebug("phase-timeline", "render", {
86
+ label: p.label,
87
+ liveElapsedMs,
88
+ startedAt: p.startedAt,
89
+ now,
90
+ });
91
+ }
77
92
  const elapsed = formatElapsed(liveElapsedMs);
78
93
  const meta = elapsed ? ` (${elapsed})` : isDone ? "" : " …";
79
94
  return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: markerColor, children: marker }), _jsx("text", { fg: labelColor, children: ` ${p.label}` }), meta && _jsx("text", { fg: t.textDim, children: meta })] }), p.detail && (_jsx("box", { paddingLeft: 2, children: _jsx("text", { fg: t.textMuted, children: `└ ${truncate(p.detail, 100)}` }) }))] }, p.phaseId));
@@ -9,6 +9,7 @@ const PHASE_LABEL = {
9
9
  "pil-acceptance": "Confirm the goal",
10
10
  "tool-loop-cap": "Tool-loop checkpoint",
11
11
  "safety-override": "Safety check",
12
+ "ask-user": "The agent is asking",
12
13
  };
13
14
  /**
14
15
  * AskUserQuestion-style card. Controlled component — parent owns the state
@@ -1,5 +1,20 @@
1
1
  import type { ChatEntry } from "../../types/index.js";
2
2
  import type { Theme } from "../theme.js";
3
+ /**
4
+ * Index of the newest entry that renders a `ctrl+e` affordance, or -1.
5
+ *
6
+ * The TUI has ONE global ctrl+e, so it must resolve to a single target. Every
7
+ * component that prints "ctrl+e expand" must be represented here, otherwise the
8
+ * affordance lies: it tells the user a key works while nothing is listening.
9
+ * Mirrors the collapse conditions in AssistantMessageContent / UserMessageContent
10
+ * / ReasoningContent / ToolGroupView — keep the two in sync.
11
+ */
12
+ export declare function findLastCollapsibleIndex(messages: ChatEntry[]): number;
13
+ export declare function ReasoningContent({ content, t, expanded }: {
14
+ content: string;
15
+ t: Theme;
16
+ expanded: boolean;
17
+ }): import("react").ReactNode;
3
18
  export declare function AssistantMessageContent({ content, t, expanded, isFinal, }: {
4
19
  content: string;
5
20
  t: Theme;
@@ -28,6 +28,55 @@ const USER_MSG_COLLAPSED_LINES = 5;
28
28
  // to this many lines to stop the chat scroll wall and cut markdown re-render
29
29
  // cost. 8 fits comfortably on a short terminal and conveys the gist.
30
30
  const ASSISTANT_MSG_COLLAPSED_LINES = 8;
31
+ // Chain-of-thought is never the answer — it is the model talking to itself.
32
+ // Unlike the final assistant message (which must stay fully visible), reasoning
33
+ // collapses even on the final entry: once the turn is answered, a wall of
34
+ // "[Thought]" above the reply is pure noise. 3 lines keeps a gist visible.
35
+ const REASONING_COLLAPSED_LINES = 3;
36
+ /**
37
+ * Index of the newest entry that renders a `ctrl+e` affordance, or -1.
38
+ *
39
+ * The TUI has ONE global ctrl+e, so it must resolve to a single target. Every
40
+ * component that prints "ctrl+e expand" must be represented here, otherwise the
41
+ * affordance lies: it tells the user a key works while nothing is listening.
42
+ * Mirrors the collapse conditions in AssistantMessageContent / UserMessageContent
43
+ * / ReasoningContent / ToolGroupView — keep the two in sync.
44
+ */
45
+ export function findLastCollapsibleIndex(messages) {
46
+ let lastAssistantIdx = -1;
47
+ for (let i = messages.length - 1; i >= 0; i--) {
48
+ if (messages[i]?.type === "assistant") {
49
+ lastAssistantIdx = i;
50
+ break;
51
+ }
52
+ }
53
+ const lineCount = (s) => (s ? s.split("\n").length : 0);
54
+ for (let i = messages.length - 1; i >= 0; i--) {
55
+ const m = messages[i];
56
+ if (!m)
57
+ continue;
58
+ if (m.type === "user" && lineCount(m.content) > USER_MSG_COLLAPSED_LINES)
59
+ return i;
60
+ if (m.type === "assistant") {
61
+ if (lineCount(m.reasoning) > REASONING_COLLAPSED_LINES)
62
+ return i;
63
+ // The final assistant message never auto-collapses its body (F7), so it
64
+ // offers no body affordance to toggle.
65
+ if (i !== lastAssistantIdx && lineCount(m.content) > ASSISTANT_MSG_COLLAPSED_LINES)
66
+ return i;
67
+ }
68
+ if (m.type === "tool_group" && m.toolGroup?.state === "done" && (m.toolGroup?.items.length ?? 0) > 0)
69
+ return i;
70
+ }
71
+ return -1;
72
+ }
73
+ export function ReasoningContent({ content, t, expanded }) {
74
+ const lines = content.split("\n");
75
+ const isLong = lines.length > REASONING_COLLAPSED_LINES;
76
+ const body = !isLong || expanded ? content : lines.slice(0, REASONING_COLLAPSED_LINES).join("\n");
77
+ const hidden = lines.length - REASONING_COLLAPSED_LINES;
78
+ return (_jsxs("box", { paddingLeft: 0, marginBottom: 1, flexShrink: 0, flexDirection: "column", children: [_jsx("text", { fg: t.textMuted, children: "[Thought] Thought" }), _jsxs("box", { border: ["left"], borderColor: t.textMuted, paddingLeft: 2, marginTop: 1, flexDirection: "column", children: [_jsx(Markdown, { content: body, t: t }), isLong && (_jsxs("text", { fg: t.textDim, children: ["ctrl+e ", _jsx("span", { style: { fg: t.textMuted }, children: expanded ? "collapse" : `expand (${hidden} more lines)` })] }))] })] }));
79
+ }
31
80
  export function AssistantMessageContent({ content, t, expanded, isFinal, }) {
32
81
  // Strip stray model self-annotation macros (e.g. grok's trailing
33
82
  // `\confidence{85}`) that leak into the answer — not instructed in the prompt.
@@ -142,7 +191,7 @@ function MessageViewImpl({ entry, index, t, modeColor, expandedMessages, mcpRun,
142
191
  // ignore
143
192
  }
144
193
  }
145
- return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, entry.reasoning ? (_jsxs("box", { paddingLeft: 0, marginBottom: 1, flexShrink: 0, flexDirection: "column", children: [_jsx("text", { fg: t.textMuted, children: "[Thought] Thought" }), _jsx("box", { border: ["left"], borderColor: t.textMuted, paddingLeft: 2, marginTop: 1, flexDirection: "column", children: _jsx(Markdown, { content: entry.reasoning, t: t }) })] })) : null, fallbackSr ? (_jsx(StructuredResponseView, { t: t, sr: fallbackSr, modeColor: entry.modeColor || modeColor })) : (_jsx(AssistantMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false, isFinal: isFinalAssistant === true }))] }));
194
+ return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, entry.reasoning ? (_jsx(ReasoningContent, { content: entry.reasoning, t: t, expanded: expandedMessages?.has(index) ?? false })) : null, fallbackSr ? (_jsx(StructuredResponseView, { t: t, sr: fallbackSr, modeColor: entry.modeColor || modeColor })) : (_jsx(AssistantMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false, isFinal: isFinalAssistant === true }))] }));
146
195
  }
147
196
  case "tool_call":
148
197
  return (_jsx("box", { paddingLeft: 3, marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: entry.modeColor || modeColor }, children: "▣ " }), _jsx("span", { style: { fg: t.textMuted }, children: entry.content.replace("▣ ", "") })] }) }));
@@ -3,16 +3,28 @@
3
3
  *
4
4
  * Claude-Code-style "tool streak" panel — replaces the previous one-line-per-
5
5
  * tool render. Lifecycle:
6
- * - active → header "● <Verb>… (N tools · Ms)" + indented item list
7
- * - done → collapsed "● Done (N tool uses · Ms) ctrl+e to expand"
6
+ * - active → header "● Reading 2 files… (N tools · Ms)" + indented item list
7
+ * - done → collapsed "● Read 2 files, ran 1 shell command ctrl+e to expand"
8
8
  * - failed → always-expanded header + items, error item flagged red
9
9
  *
10
+ * The header names the work per CATEGORY (see utils/tool-summary.ts) rather
11
+ * than saying "Done": once the group collapses that line is all the user has,
12
+ * so it has to carry the meaning. Items add the detail a glance needs — the
13
+ * full shell command for bash, "Wrote N lines to <path>" before a write diff.
14
+ *
10
15
  * The active group lives at the tail of the message stream as a regular
11
16
  * ChatEntry of type "tool_group"; closing the streak (assistant text arrives
12
17
  * or stream ends) flips its state to done/failed in place.
13
18
  */
14
- import type { ChatEntry } from "../../types/index.js";
19
+ import type { ChatEntry, FileDiff, ToolCall } from "../../types/index.js";
15
20
  import type { Theme } from "../theme.js";
21
+ /** The command a bash item ran, split for multi-line display. Null when absent. */
22
+ export declare function bashCommandLines(tc: ToolCall): string[] | null;
23
+ /**
24
+ * One-line prose outcome for a write/edit, e.g. "Wrote 161 lines to a.ts" or
25
+ * "Edited a.ts (+12 -3)". Null when the tool isn't a file mutation.
26
+ */
27
+ export declare function writeOutcomeLine(toolName: string, diff: FileDiff): string | null;
16
28
  export interface ToolGroupViewProps {
17
29
  entry: ChatEntry;
18
30
  t: Theme;