zidane 6.6.4 → 7.0.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 (157) hide show
  1. package/dist/{acp-BQqhozmt.js → acp-Wr8l4IDI.js} +7 -180
  2. package/dist/acp-Wr8l4IDI.js.map +1 -0
  3. package/dist/acp-cli.js +6 -6
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.d.ts.map +1 -1
  6. package/dist/acp.js +2 -1
  7. package/dist/{agent-DTxH7RHW.js → agent-C-camKQW.js} +162 -12
  8. package/dist/agent-C-camKQW.js.map +1 -0
  9. package/dist/agent.d.ts +1 -1
  10. package/dist/agent.js +1 -1
  11. package/dist/{anthropic-DRNRnd6W.js → anthropic-BDOGzhtj.js} +3 -3
  12. package/dist/{anthropic-DRNRnd6W.js.map → anthropic-BDOGzhtj.js.map} +1 -1
  13. package/dist/{transcript-anchors-BjH-68k2.js → approvals-CqqxE5Vh.js} +4924 -275
  14. package/dist/approvals-CqqxE5Vh.js.map +1 -0
  15. package/dist/{auth-7rv6ZfWk.js → auth-BdwDROYg.js} +12 -8
  16. package/dist/{auth-7rv6ZfWk.js.map → auth-BdwDROYg.js.map} +1 -1
  17. package/dist/chat/pure.d.ts +221 -4
  18. package/dist/chat/pure.d.ts.map +1 -0
  19. package/dist/chat/pure.js +4 -2
  20. package/dist/chat.d.ts +6 -2911
  21. package/dist/chat.js +8 -261
  22. package/dist/{context-transport-D8yeGdpd.js → context-transport-BqYwzJYE.js} +2 -2
  23. package/dist/{context-transport-D8yeGdpd.js.map → context-transport-BqYwzJYE.js.map} +1 -1
  24. package/dist/contexts/daytona.d.ts +1 -1
  25. package/dist/contexts/daytona.js +1 -1
  26. package/dist/contexts/e2b.d.ts +1 -1
  27. package/dist/contexts/e2b.js +1 -1
  28. package/dist/daemon-cli.d.ts +1 -0
  29. package/dist/daemon-cli.js +90 -0
  30. package/dist/daemon-cli.js.map +1 -0
  31. package/dist/eval.d.ts +1 -1
  32. package/dist/eval.js +1 -1
  33. package/dist/extensions.d.ts +2 -2
  34. package/dist/extensions.js +2 -2
  35. package/dist/headless.d.ts +1 -1
  36. package/dist/headless.js +4 -4
  37. package/dist/{index-D0O-QO7Z.d.ts → index-B-FSaM3_.d.ts} +2863 -2375
  38. package/dist/index-B-FSaM3_.d.ts.map +1 -0
  39. package/dist/index-DgdzXRPH.d.ts +3590 -0
  40. package/dist/index-DgdzXRPH.d.ts.map +1 -0
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +14 -1179
  43. package/dist/ipc-CIv87xuH.js +331 -0
  44. package/dist/ipc-CIv87xuH.js.map +1 -0
  45. package/dist/ipc.d.ts +849 -0
  46. package/dist/ipc.d.ts.map +1 -0
  47. package/dist/ipc.js +4 -0
  48. package/dist/json-rpc-rSWMUh9b.js +192 -0
  49. package/dist/json-rpc-rSWMUh9b.js.map +1 -0
  50. package/dist/{logger-DBX9uYOw.js → logger--C1VQjwJ.js} +3 -13
  51. package/dist/logger--C1VQjwJ.js.map +1 -0
  52. package/dist/logs-view-CWG8aUAG.js +44 -0
  53. package/dist/logs-view-CWG8aUAG.js.map +1 -0
  54. package/dist/{mcp-CVtSQTWg.js → mcp-BIWa57eY.js} +3 -3
  55. package/dist/{mcp-CVtSQTWg.js.map → mcp-BIWa57eY.js.map} +1 -1
  56. package/dist/mcp.d.ts +1 -1
  57. package/dist/mcp.js +1 -1
  58. package/dist/{messages-CZMUuKzo.js → messages-21XfHxyF.js} +3 -3
  59. package/dist/{messages-CZMUuKzo.js.map → messages-21XfHxyF.js.map} +1 -1
  60. package/dist/{openai-compat-BoTi-9fd.js → openai-compat-DY-AKxCg.js} +2 -2
  61. package/dist/{openai-compat-BoTi-9fd.js.map → openai-compat-DY-AKxCg.js.map} +1 -1
  62. package/dist/output/stream-json.d.ts +1 -1
  63. package/dist/output/terminal.d.ts +1 -1
  64. package/dist/{plugins-onDvTHEm.js → plugins-B531Tz5b.js} +2 -2
  65. package/dist/{plugins-onDvTHEm.js.map → plugins-B531Tz5b.js.map} +1 -1
  66. package/dist/plugins.d.ts +1 -1
  67. package/dist/plugins.js +1 -1
  68. package/dist/presets.d.ts +1 -1
  69. package/dist/presets.js +1 -1
  70. package/dist/providers/anthropic.d.ts +1 -1
  71. package/dist/providers/anthropic.js +1 -1
  72. package/dist/providers/arcee.d.ts +1 -1
  73. package/dist/providers/arcee.js +1 -1
  74. package/dist/providers/baseten.d.ts +1 -1
  75. package/dist/providers/baseten.js +1 -1
  76. package/dist/providers/cerebras.d.ts +1 -1
  77. package/dist/providers/cerebras.js +1 -1
  78. package/dist/providers/local.d.ts +1 -1
  79. package/dist/providers/local.js +1 -1
  80. package/dist/providers/openai-compat.d.ts +1 -1
  81. package/dist/providers/openai-compat.js +1 -1
  82. package/dist/providers/openai.d.ts +1 -1
  83. package/dist/providers/openai.js +3 -3
  84. package/dist/providers/openrouter.d.ts +1 -1
  85. package/dist/providers/openrouter.js +1 -1
  86. package/dist/providers/xai.d.ts +1 -1
  87. package/dist/providers/xai.js +1 -1
  88. package/dist/{providers-CmhdtD7w.js → providers-DX91jwir.js} +2 -2
  89. package/dist/{providers-CmhdtD7w.js.map → providers-DX91jwir.js.map} +1 -1
  90. package/dist/providers.d.ts +1 -1
  91. package/dist/providers.js +3 -3
  92. package/dist/restate.d.ts +1 -1
  93. package/dist/restate.js +8 -8
  94. package/dist/restate.js.map +1 -1
  95. package/dist/server-DJqp9dIR.js +1317 -0
  96. package/dist/server-DJqp9dIR.js.map +1 -0
  97. package/dist/session/sqlite.d.ts +1 -1
  98. package/dist/session/sqlite.d.ts.map +1 -1
  99. package/dist/session/sqlite.js +42 -4
  100. package/dist/session/sqlite.js.map +1 -1
  101. package/dist/{session-CyRLSsVa.js → session-K3icbPH9.js} +8 -2
  102. package/dist/session-K3icbPH9.js.map +1 -0
  103. package/dist/session.d.ts +1 -1
  104. package/dist/session.js +2 -2
  105. package/dist/{skills-BMGH_KK1.js → skills-PJyiSRCp.js} +3 -21
  106. package/dist/skills-PJyiSRCp.js.map +1 -0
  107. package/dist/skills.d.ts +1 -1
  108. package/dist/skills.js +1 -1
  109. package/dist/src-DzzhE2YO.js +2461 -0
  110. package/dist/src-DzzhE2YO.js.map +1 -0
  111. package/dist/todo-edits-Dw2oO2ON.js +378 -0
  112. package/dist/todo-edits-Dw2oO2ON.js.map +1 -0
  113. package/dist/{tool-formatters-NrzD6Fkk.d.ts → tool-formatters-DO56YBBw.d.ts} +104 -4
  114. package/dist/tool-formatters-DO56YBBw.d.ts.map +1 -0
  115. package/dist/tools/fetch-url.d.ts +1 -1
  116. package/dist/tools/web-search.d.ts +1 -1
  117. package/dist/tools.d.ts +1 -1
  118. package/dist/tools.js +1 -1
  119. package/dist/{transcript-anchors-UhC58Tmj.d.ts → transcript-anchors-DXqWWGbq.d.ts} +53 -4
  120. package/dist/transcript-anchors-DXqWWGbq.d.ts.map +1 -0
  121. package/dist/tui.d.ts +94 -13
  122. package/dist/tui.d.ts.map +1 -1
  123. package/dist/tui.js +2241 -1009
  124. package/dist/tui.js.map +1 -1
  125. package/dist/{turn-operations-mMxDRgLY.js → turn-operations-CZLsNgw-.js} +896 -11
  126. package/dist/turn-operations-CZLsNgw-.js.map +1 -0
  127. package/dist/{turn-operations-Cuc9pwiB.d.ts → turn-operations-DKFYWP3O.d.ts} +501 -4
  128. package/dist/turn-operations-DKFYWP3O.d.ts.map +1 -0
  129. package/dist/{types-BIarq1qE.js → types-CWgENVLY.js} +24 -2
  130. package/dist/types-CWgENVLY.js.map +1 -0
  131. package/dist/types.d.ts +1 -1
  132. package/dist/types.js +1 -1
  133. package/dist/xml-D5HBhJle.js +22 -0
  134. package/dist/xml-D5HBhJle.js.map +1 -0
  135. package/docs/CHAT.md +39 -0
  136. package/docs/IPC.md +420 -0
  137. package/docs/TUI.md +62 -0
  138. package/package.json +8 -2
  139. package/dist/acp-BQqhozmt.js.map +0 -1
  140. package/dist/agent-DTxH7RHW.js.map +0 -1
  141. package/dist/chat.d.ts.map +0 -1
  142. package/dist/chat.js.map +0 -1
  143. package/dist/index-D0O-QO7Z.d.ts.map +0 -1
  144. package/dist/index.js.map +0 -1
  145. package/dist/inline-autocomplete-D0MPvBaS.js +0 -596
  146. package/dist/inline-autocomplete-D0MPvBaS.js.map +0 -1
  147. package/dist/logger-DBX9uYOw.js.map +0 -1
  148. package/dist/login-CB23LyI1.js +0 -707
  149. package/dist/login-CB23LyI1.js.map +0 -1
  150. package/dist/session-CyRLSsVa.js.map +0 -1
  151. package/dist/skills-BMGH_KK1.js.map +0 -1
  152. package/dist/tool-formatters-NrzD6Fkk.d.ts.map +0 -1
  153. package/dist/transcript-anchors-BjH-68k2.js.map +0 -1
  154. package/dist/transcript-anchors-UhC58Tmj.d.ts.map +0 -1
  155. package/dist/turn-operations-Cuc9pwiB.d.ts.map +0 -1
  156. package/dist/turn-operations-mMxDRgLY.js.map +0 -1
  157. package/dist/types-BIarq1qE.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,34 +1,36 @@
1
- import { C as estimateRebilledCostUsd } from "./openai-compat-BoTi-9fd.js";
1
+ import { C as estimateRebilledCostUsd } from "./openai-compat-DY-AKxCg.js";
2
2
  import { t as buildContextSegments } from "./context-segments-CJCtJ9sU.js";
3
- import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as coerceExtensionConfigInput, D as resolveTasksDir, Dn as resolveExtensionConfigValue, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, Mn as effectiveContextWindow, O as buildPostCompactAttachments, On as sanitizeExtensionConfigActionPatch, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bn as setDebugLogSink, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as OUTPUT_RESERVE_TOKENS, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-DTxH7RHW.js";
3
+ import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as coerceExtensionConfigInput, D as resolveTasksDir, Dn as resolveExtensionConfigValue, E as resolveSessionDir, Et as isSpawnTool, Fn as effectiveContextWindow, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, On as sanitizeExtensionConfigActionPatch, Pn as OUTPUT_RESERVE_TOKENS, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bn as setDebugLogSink, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as createExtensionBus, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-C-camKQW.js";
4
+ import { D as shouldAutoGenerateTitle, E as generateSessionTitle, F as parseCommandInvocation, M as commandBlocksToContent, P as executeCommand, S as resolveInlineSuggestion, a as loginMcpServer, b as createPathAutocompleteProvider, c as SKILL_EDIT_METADATA_KEY, g as readSkillEditMarker, h as extensionScaffold, j as commandBlockToEvent, l as ZIDANE_EXTENSION_SKILL_NAME, m as extensionDirForCwd, p as editSessionMetadata, u as ZIDANE_SKILL_EDIT_SKILL_NAME, v as withZidaneExtensionSkill, w as generateStructuredData, x as patchInlineGhost, y as withZidaneSkillEditSkill } from "./src-DzzhE2YO.js";
4
5
  import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
5
6
  import { n as appendCapped } from "./output-cap-C9zukcCj.js";
6
7
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-BSFQVclW.js";
7
8
  import { u as errorMessage } from "./errors-DKiMrOKq.js";
8
- import { E as buildCatalog, R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-BMGH_KK1.js";
9
- import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-onDvTHEm.js";
10
- import { a as createRingBufferSink, i as createLoggingHooks, l as logFileTimestamp, n as createFileSink, o as fanoutSink, r as createLogger, s as formatLogLine, t as consoleSink } from "./logger-DBX9uYOw.js";
9
+ import { E as buildCatalog, R as findGitRoot, c as SkillInstallError, l as installSkillFromSpec } from "./skills-PJyiSRCp.js";
10
+ import { i as nextLogLevel, n as filterLogRecords, r as formatLogLine } from "./logs-view-CWG8aUAG.js";
11
+ import { a as createRingBufferSink, c as logFileTimestamp, i as createLoggingHooks, n as createFileSink, o as fanoutSink, r as createLogger } from "./logger--C1VQjwJ.js";
11
12
  import { i as sniffImageMediaType } from "./media-sniff-UKijEaiQ.js";
12
- import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-CVtSQTWg.js";
13
+ import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-BIWa57eY.js";
13
14
  import { replaceDynamicSection, splitSystemPrompt } from "./prompt.js";
14
- import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-CyRLSsVa.js";
15
- import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
15
+ import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-K3icbPH9.js";
16
+ import { t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
16
17
  import { s as withTimeout } from "./timeout-CjOG2mea.js";
17
18
  import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
18
- import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as commandBlockToEvent, g as parseCommandInvocation, h as executeCommand, n as patchInlineGhost, p as commandBlocksToContent, r as resolveInlineSuggestion, s as generateSessionTitle, t as createPathAutocompleteProvider } from "./inline-autocomplete-D0MPvBaS.js";
19
19
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
20
- import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-CB23LyI1.js";
21
- import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-7rv6ZfWk.js";
20
+ import { n as JsonRpcRemoteError, t as JsonRpcConnectionClosedError } from "./json-rpc-rSWMUh9b.js";
21
+ import { $t as hasRunReceipt, A as chunkMarkdownForRender, At as SESSION_EVENT_STATUS_STYLE, B as DAEMON_HEALTH_STYLE, Bt as sessionEventExcerpt, C as usageTone, Cn as createChatsCompletionProvider, Ct as selectableTurnIds, E as filterModelCatalog, Et as turnSelectionOwnership, F as stripHtmlCommentsOutsideCode, Ht as sessionEventMeta, I as cacheBreakChip, K as filterChainFallbacks, Kt as fitRunReceiptCells, L as compactingLabel, M as codeTokenHasClosingFence, N as normalizeMarkdownCodeFences, O as parseCustomModelQuery, P as splitMarkdownCodeBlocks, Pt as isMainSession, Q as restoreChainFallbacks, S as resetLabel, St as retagUserPromptEventsFromRefs, T as buildModelCatalog, Tt as streamEventSearchText, U as chainPositionOf, Ut as shortSessionId, V as buildRunModel, W as chainsEqual, Wt as truncateTitle, X as providerKeySetChanged, Xt as formatRunReceiptLine, Yt as formatRunReceiptCells, Z as reorderKeyed, Zt as formatTokenCount, _ as turnContextSize, _n as resolveApprovalForPayload, _t as isVisible, a as TOOL_DISPLAY, b as formatUsagePercent, bn as summarizeOutcomes, cn as filetypeFromPath, d as finalizeStreamingMarkdown, dn as summarizeEditPayload, dt as buildLinearRamp, en as runReceiptFromRun, et as toggleChainEntry, f as finalizeStreamingMarkdownForOwner, ft as BASH_CALL_ID_PREFIX, g as revealEarlierTranscriptEvents, gn as parseEditOutcomesFromResult, gt as isUserBashEvent, h as replaceTranscriptTurnEvents, hn as mergeApprovalAndBodyOutcomes, ht as isTurnHighlighted, i as turnAsText, in as clipDiffForRender, it as uniqueSkillNamesFromReferences, j as closeDanglingCodeFence, ln as previewEditPayload, lt as createCommandsCompletionProvider, mt as isEditErrorResult, n as deleteTurnSafely, nn as buildContextualDiff, o as displayNameFor, ot as createFilesCompletionProvider, p as hiddenEarlierTranscriptCount, pn as buildEditOutcomesAnnotation, pt as EDIT_TOOL_NAMES, q as pickChainPrimary, qt as formatCostUsd, r as truncateTurnsAt, rn as buildUnifiedDiff, rt as createSkillsCompletionProvider, s as formatToolCall, sn as extractEditPayload, u as capTranscriptEvents, ut as blendHsl, v as formatBalance, vn as rewriteMultiEditHeader, vt as isVisibleDuringSearch, w as splitPromptSegments, wt as selectionAfterTurnRemoval, x as mostConstrainedWindow, xt as retagUserPromptEvents, yn as stripEditOutcomesAnnotation, yt as promptHistoryFromEvents, zt as sessionDirLabel } from "./turn-operations-CZLsNgw-.js";
22
+ import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
23
+ import { $ as supportsOAuth, $n as buildUpdateHint, $r as chatReferenceTokensForPrompt, A as useSafeModeQueue, An as createDiscoverySlot, At as makeRequestInteraction, Bn as hasClaudeCodeSetup, Ct as InteractionsProvider, Dt as createInteractionTools, Er as TODO_STATUS_GLYPHS, Et as createAutomaticInteractionResponse, Fn as useConfig, G as usageChecksOptedOut, Gn as buildMcpServers, H as fetchAllProviderUsage, Hn as mcpsWritePath, In as resolveConfig, Ir as selectActiveTodos, J as fetchOAuthRedirect, Jn as discoverProjectMcps, Jt as buildPluginContributions, Kn as dedupMcpServersByName, Ln as resolveStoragePaths, Lr as setTodosForRun, M as addToSafelist, Mn as useDiscovery, N as getSafelist, Nn as useDiscoveryOptional, Nr as pickActiveRunId, Nt as useInteractionsActions, O as SafeModeProvider, On as parseEditorCommand, Or as applyTodosSnapshot, P as isOnSafelist, Pn as ConfigProvider, Pr as pruneTodosByRun, Pt as useInteractionsQueue, Q as runOAuthLogin, Qn as bootTick, Qr as envSection, R as suggestSafelistEntries, Rn as useCompletion, Rr as todoProgressLabel, St as HOST_INTERACTION_ID_PREFIX, Tt as buildResumedToolResultsTurn, U as fetchProviderUsage, Un as removeMcpServer, Ut as createBuiltinCommands, V as clearProviderUsageCache, Vn as importFromClaudeCode, W as providerUsageSupported, Wn as upsertMcpServer, Wt as createMainSession, X as redirectUriFromAuthUrl, Xn as tryOpenBrowser, Xr as buildBuildSystem, Xt as parsePluginScanPathEnv, Yt as discoverProjectPlugins, Z as oauthUsesManualCodePaste, Zr as buildPlanSystem, Zt as attachMcpWarnLog, _ as buildSpawnEndEvent, _i as toolCallPreview, _t as useMcpAuthState, ai as eventsFromTurns, at as subscribeMcpToolsCache, b as defaultSkillScanPaths, bi as usageByModel, bn as resolveTheme, bt as ASK_USER_TOOL, c as useSelectStyle, ci as listSessionMeta, cn as DEFAULT_SETTINGS, d as useTheme, di as noticeTurnContent, dn as SETTINGS_INPUTS, dr as discoverAgentsMd, dt as parentServerName, ei as createChatTurnsReadTool, en as EMPTY_HINTS, er as useUpdateCheck, f as discoverSystemMd, fn as SETTINGS_TOGGLES, ft as createFileMcpCredentialStore, g as useStreamBuffer, gn as useSettings, gt as useMcpAuthDispatch, h as userSystemMdPath, hi as sumRunCosts, ht as McpAuthProvider, ii as deriveSessionTitle, in as buildHints, jn as DiscoveryProvider, jt as pendingInteractionsFromTurns, k as useSafeModeActions, kn as quoteForShell, kt as isModelInteraction, l as useSurfaces, li as loadState, ln as SETTINGS_CATEGORIES, lt as buildVisibleMcpRows, m as userScopeOverridesLoad, mi as stripSpawnTokensLine, mn as clampFps, ni as compactSummaryEvent, nn as hintsLength, nt as mcpToolsCachePath, o as ThemeProvider, oi as getSessionMeta, on as listProjectFiles, p as renderSystemMdSeed, pi as serverToolResultSummary, pn as SettingsProvider, q as formatPathForCwd, qt as runEndNotice, r as computeTurnAnchors, rn as truncateTrailing, rt as refreshMcpToolsCatalog, s as useColors, si as lastContextSizeFromTurns, sn as useEnabledToggleSet, st as useMcpToolToggleMap, tn as clipHintsToWidth, tt as loadMcpToolsCache, ui as marginTopFor, un as SETTINGS_CHOICES, ur as chatAutoCompactBehavior, ut as indexOfServerRow, v as buildSpawnStartEvent, vi as toolResultText, vt as getMcpAuthStatus, w as writeSessionExport, wt as PRESENT_PLAN_TOOL, x as discoverProjectSkills, y as buildSkillsConfig, yi as updateToolEventOutcomes, yn as resolveChipColor, yr as accentColor, zn as countClaudeSessionFiles, zr as useActiveTodos } from "./approvals-CqqxE5Vh.js";
24
+ import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-BdwDROYg.js";
25
+ import { a as connectIpcClient, f as probeSocket, o as connectOrSpawnDaemon, p as selfDaemonSpawnCommand, u as defaultSocketPath } from "./server-DJqp9dIR.js";
22
26
  import { createDockerContext } from "./contexts/docker.js";
23
27
  import { createTuiStore } from "./session/sqlite.js";
24
- import { B as BASH_CALL_ID_PREFIX, D as normalizeMarkdownCodeFences, E as codeTokenHasClosingFence, G as isVisible, H as isEditErrorResult, J as retagUserPromptEvents, K as isVisibleDuringSearch, L as createCommandsCompletionProvider, M as uniqueSkillNamesFromReferences, O as splitMarkdownCodeBlocks, P as createFilesCompletionProvider, Q as turnSelectionOwnership, R as blendHsl, S as parseCustomModelQuery, T as closeDanglingCodeFence, U as isTurnHighlighted, V as EDIT_TOOL_NAMES, W as isUserBashEvent, X as selectionAfterTurnRemoval, Y as selectableTurnIds, Z as streamEventSearchText, _ as turnContextSize, _t as stripEditOutcomesAnnotation, a as TOOL_DISPLAY, at as extractEditPayload, b as filterModelCatalog, d as finalizeStreamingMarkdown, dt as buildEditOutcomesAnnotation, et as buildContextualDiff, f as finalizeStreamingMarkdownForOwner, g as revealEarlierTranscriptEvents, gt as rewriteMultiEditHeader, h as replaceTranscriptTurnEvents, ht as resolveApprovalForPayload, i as turnAsText, j as createSkillsCompletionProvider, k as stripHtmlCommentsOutsideCode, lt as summarizeEditPayload, mt as parseEditOutcomesFromResult, n as deleteTurnSafely, nt as clipDiffForRender, o as displayNameFor, ot as filetypeFromPath, p as hiddenEarlierTranscriptCount, pt as mergeApprovalAndBodyOutcomes, q as promptHistoryFromEvents, r as truncateTurnsAt, s as formatToolCall, st as previewEditPayload, tt as buildUnifiedDiff, u as capTranscriptEvents, v as splitPromptSegments, vt as summarizeOutcomes, w as chunkMarkdownForRender, xt as createChatsCompletionProvider, y as buildModelCatalog, z as buildLinearRamp } from "./turn-operations-mMxDRgLY.js";
25
- import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
26
- import { At as hintsLength, B as formatPathForCwd, Bt as SETTINGS_TOGGLES, C as addToSafelist, Cn as upsertMcpServer, Ct as makeRequestInteraction, Dn as discoverProjectMcps, Dr as envSection, Dt as useInteractionsQueue, Er as buildPlanSystem, Et as useInteractionsActions, F as formatUsagePercent, Fr as lastContextSizeFromTurns, Ft as useEnabledToggleSet, G as runOAuthLogin, Gr as toolCallPreview, Hn as chatAutoCompactBehavior, Hr as stripSpawnTokensLine, Ht as clampFps, I as mostConstrainedWindow, Ir as listSessionMeta, It as DEFAULT_SETTINGS, J as loadMcpToolsCache, Jr as usageByModel, K as supportsOAuth, Kr as toolResultText, Kt as resolveChipColor, L as providerUsageSupported, Lr as loadState, Lt as SETTINGS_CATEGORIES, M as clearProviderUsageCache, Mn as buildUpdateHint, Mt as buildHints, N as fetchAllProviderUsage, Nn as useUpdateCheck, Nr as deriveSessionTitle, Or as chatReferenceTokensForPrompt, Ot as EMPTY_HINTS, P as fetchProviderUsage, Pr as eventsFromTurns, Pt as listProjectFiles, Q as subscribeMcpToolsCache, R as usageChecksOptedOut, Rr as marginTopFor, Rt as SETTINGS_CHOICES, Sn as removeMcpServer, St as isModelInteraction, T as isOnSafelist, Tn as dedupMcpServersByName, Tr as buildBuildSystem, U as redirectUriFromAuthUrl, Un as discoverAgentsMd, Ur as sumRunCosts, Ut as useSettings, V as fetchOAuthRedirect, Vr as serverToolResultSummary, Vt as SettingsProvider, W as oauthUsesManualCodePaste, X as refreshMcpToolsCatalog, Y as mcpToolsCachePath, Zn as accentColor, _n as useCompletion, _t as PRESENT_PLAN_TOOL, a as useSurfaces, an as ConfigProvider, at as createFileMcpCredentialStore, b as useSafeModeActions, bn as importFromClaudeCode, bt as createInteractionTools, c as discoverSystemMd, cn as resolveStoragePaths, ct as McpAuthProvider, d as userSystemMdPath, dn as chainsEqual, dt as getMcpAuthStatus, et as useMcpToolToggleMap, f as useStreamBuffer, fn as filterChainFallbacks, fr as setTodosForRun, gn as toggleChainEntry, gt as InteractionsProvider, h as discoverProjectSkills, ht as HOST_INTERACTION_ID_PREFIX, i as useSelectStyle, in as useDiscoveryOptional, it as parentServerName, jn as bootTick, jr as compactSummaryEvent, jt as truncateTrailing, k as suggestSafelistEntries, kn as tryOpenBrowser, kr as createChatTurnsReadTool, kt as clipHintsToWidth, l as renderSystemMdSeed, ln as buildRunModel, lt as useMcpAuthDispatch, m as defaultSkillScanPaths, mn as restoreChainFallbacks, mr as useActiveTodos, n as ThemeProvider, nn as DiscoveryProvider, nt as buildVisibleMcpRows, on as useConfig, p as buildSkillsConfig, pn as providerKeySetChanged, pr as todoProgressLabel, pt as ASK_USER_TOOL, qr as updateToolEventOutcomes, qt as resolveTheme, r as useColors, rn as useDiscovery, rr as TODO_STATUS_GLYPHS, rt as indexOfServerRow, s as useTheme, sn as resolveConfig, t as computeTurnAnchors, tn as createDiscoverySlot, u as userScopeOverridesLoad, un as chainPositionOf, ur as pruneTodosByRun, ut as useMcpAuthState, v as writeSessionExport, vn as countClaudeSessionFiles, vt as buildResumedToolResultsTurn, w as getSafelist, wn as buildMcpServers, wt as pendingInteractionsFromTurns, x as useSafeModeQueue, xn as mcpsWritePath, y as SafeModeProvider, yn as hasClaudeCodeSetup, yt as createAutomaticInteractionResponse, zr as noticeTurnContent, zt as SETTINGS_INPUTS } from "./transcript-anchors-BjH-68k2.js";
27
- import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
28
+ import { _ as buildVisibleAuthRows, a as editTodo, b as parentProviderKey, c as NOTICE_GLYPH, f as validateMcpForm, g as instructionPreviewLines, h as instructionOriginBadge, i as cycleTodoStatus, l as MCP_TRANSPORTS, m as buildVisibleInstructionRows, n as buildTodoRetryMessage, r as clearTodos, s as removeTodo, t as addTodo, v as indexOfProviderRow, x as selectableSources } from "./todo-edits-Dw2oO2ON.js";
29
+ import { basename, dirname, join, relative, resolve } from "node:path";
28
30
  import { Buffer } from "node:buffer";
29
31
  import * as fs from "node:fs";
30
- import { createWriteStream, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
31
- import { readFile, readdir, stat, unlink } from "node:fs/promises";
32
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
33
+ import { readFile, unlink } from "node:fs/promises";
32
34
  import { spawn, spawnSync } from "node:child_process";
33
35
  import { homedir, tmpdir } from "node:os";
34
36
  import { Fzf, byLengthAsc } from "fzf";
@@ -36,200 +38,15 @@ import * as React from "react";
36
38
  import { Component, createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
37
39
  import * as ReactJsxRuntime from "react/jsx-runtime";
38
40
  import { jsx } from "react/jsx-runtime";
41
+ import process$1 from "node:process";
39
42
  import * as OpenTuiCore from "@opentui/core";
40
- import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, parseColor, stripAnsiSequences } from "@opentui/core";
43
+ import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextAttributes, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, parseColor, stripAnsiSequences } from "@opentui/core";
41
44
  import * as OpenTuiReact from "@opentui/react";
42
45
  import { createRoot, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from "@opentui/react";
43
46
  import * as OpenTuiReactJsxDevRuntime from "@opentui/react/jsx-dev-runtime";
44
47
  import * as OpenTuiReactJsxRuntime from "@opentui/react/jsx-runtime";
45
48
  import { Fragment, jsx as jsx$1, jsxs } from "@opentui/react/jsx-runtime";
46
49
  import * as ReactJsxDevRuntime from "react/jsx-dev-runtime";
47
- //#region src/chat/plugins-discovery.ts
48
- /** Subdirectory each search root is probed for. */
49
- const PLUGINS_SUBPATH = "plugins";
50
- /**
51
- * Resolve the plugin search roots. First-found wins on name collision;
52
- * earlier entries take precedence.
53
- *
54
- * Search order — see {@link projectUserPaths}:
55
- *
56
- * 1. `{project}/.agents/plugins`
57
- * 2. `{project}/.zidane/plugins`
58
- * 3. `~/.agents/plugins`
59
- * 4. `~/.zidane/plugins`
60
- *
61
- * Non-existent paths are returned; the walk skips them.
62
- *
63
- * Unlike skills and MCP servers there is no foreign-agent or Ruler variant:
64
- * Agent Plugins is a vendor-neutral format, so `.agents/plugins` IS the
65
- * cross-tool location. Nothing to import from a competitor's directory.
66
- */
67
- function defaultPluginScanPaths(opts = {}) {
68
- return projectUserPaths({
69
- subPath: PLUGINS_SUBPATH,
70
- ...opts
71
- });
72
- }
73
- /**
74
- * Parse a platform-delimited list of additional plugin scan roots. Mirrors
75
- * `parseExtensionScanPathEnv` in `../extensions/discovery`:
76
- *
77
- * ZIDANE_PLUGIN_DIRS=/repo/plugins bun run tui
78
- *
79
- * Each path is a scan ROOT whose immediate children are plugin directories
80
- * (`<root>/<name>/plugin.json`), not a single plugin directory.
81
- *
82
- * This exists because the four default roots are all dot-directories, and
83
- * `.zidane/` is gitignored — so a repo has nowhere TRACKED to keep example or
84
- * first-party plugins for local dogfooding. Extensions solved the same
85
- * problem the same way.
86
- */
87
- function parsePluginScanPathEnv(value = process.env.ZIDANE_PLUGIN_DIRS) {
88
- if (!value) return [];
89
- return value.split(delimiter).map((p) => p.trim()).filter(Boolean);
90
- }
91
- /**
92
- * Load every plugin reachable from the default search roots.
93
- *
94
- * Each immediate child directory of a root is one plugin — the same
95
- * depth-1 rule the spec applies inside `skills/`. A child that isn't a
96
- * directory, or that holds no `plugin.json`, is silently skipped: dropping a
97
- * README next to your plugins is not an error.
98
- *
99
- * Dedup is by plugin NAME, not directory name, because the manifest owns
100
- * identity — two directories called `tools/` shipping different plugins are
101
- * two plugins, and one plugin vendored at two scopes is one plugin.
102
- */
103
- async function discoverProjectPlugins(opts = {}) {
104
- const roots = [...defaultPluginScanPaths(opts), ...(opts.extraRoots ?? []).map((path) => ({
105
- path,
106
- source: "project"
107
- }))].filter((p) => existsSync(p.path));
108
- const dataRoot = defaultPluginDataRoot({
109
- ...opts.prefix !== void 0 ? { prefix: opts.prefix } : {},
110
- ...opts.home !== void 0 ? { home: opts.home } : {}
111
- });
112
- const plugins = [];
113
- const errors = [];
114
- const seen = /* @__PURE__ */ new Set();
115
- for (const root of roots) {
116
- if (opts.signal?.aborted) break;
117
- let entries;
118
- try {
119
- entries = await readdir(root.path);
120
- } catch (err) {
121
- errors.push({
122
- path: root.path,
123
- source: root.source,
124
- message: `Could not read plugin directory: ${err instanceof Error ? err.message : String(err)}`
125
- });
126
- continue;
127
- }
128
- for (const entry of entries.sort()) {
129
- if (opts.signal?.aborted) break;
130
- const dir = join(root.path, entry);
131
- try {
132
- if (!(await stat(dir)).isDirectory()) continue;
133
- } catch {
134
- continue;
135
- }
136
- const loaded = await loadAgentPlugin(dir, {
137
- source: root.source,
138
- dataRoot
139
- });
140
- if (!loaded.ok && loaded.diagnostics.some((d) => d.code === "manifest-missing")) continue;
141
- const name = loaded.manifest?.name ?? basename(dir);
142
- if (seen.has(name)) continue;
143
- seen.add(name);
144
- plugins.push({
145
- name,
146
- plugin: loaded,
147
- source: root.source,
148
- path: dir
149
- });
150
- }
151
- }
152
- return {
153
- plugins,
154
- errors
155
- };
156
- }
157
- /**
158
- * Map the user's enabled-plugin list onto the contributions a host merges
159
- * into the agent.
160
- *
161
- * Conventions match `enabledSkills` / `enabledMcps`:
162
- * - `undefined` → every discovered plugin is enabled (default)
163
- * - `[]` → all off
164
- * - `[names]` → allowlist
165
- *
166
- * A plugin that failed to load contributes nothing regardless of the
167
- * allowlist — `ok: false` means components were never discovered.
168
- */
169
- function buildPluginContributions(opts) {
170
- const allow = opts.enabled === void 0 ? null : new Set(opts.enabled);
171
- const skills = [];
172
- const mcpServers = [];
173
- for (const entry of opts.discovered) {
174
- if (!entry.plugin.ok) continue;
175
- if (allow !== null && !allow.has(entry.name)) continue;
176
- skills.push(...entry.plugin.skills);
177
- mcpServers.push(...entry.plugin.mcpServers);
178
- }
179
- return {
180
- skills,
181
- mcpServers
182
- };
183
- }
184
- //#endregion
185
- //#region src/mcp/warn-log.ts
186
- /**
187
- * Subscribe to `mcp:warn`
188
- * (stdio child stderr lines, see `attachStderrWarnPump` in ./index.ts)
189
- * and append each to `<dir>/warnings.log`.
190
- *
191
- * The stream opens lazily on the first warning so
192
- * a healthy session never touches disk.
193
- */
194
- function attachMcpWarnLog(hooks, opts) {
195
- let stream = null;
196
- let logger = null;
197
- const ensureLogger = () => {
198
- if (logger) return logger;
199
- try {
200
- mkdirSync(opts.dir, { recursive: true });
201
- const s = createWriteStream(join(opts.dir, "warnings.log"), { flags: "a" });
202
- s.on("error", () => {});
203
- stream = s;
204
- logger = createLogger(consoleSink({
205
- stream: s,
206
- minLevel: "warn"
207
- }));
208
- return logger;
209
- } catch {
210
- return null;
211
- }
212
- };
213
- const unhook = hooks.hook("mcp:warn", ({ name, message }) => {
214
- ensureLogger()?.warn("mcp warn", {
215
- server: name,
216
- message
217
- });
218
- });
219
- return { close: async () => {
220
- unhook();
221
- const s = stream;
222
- stream = null;
223
- logger = null;
224
- if (!s) return;
225
- await new Promise((resolve) => {
226
- s.once("close", () => resolve());
227
- s.once("error", () => resolve());
228
- s.end();
229
- });
230
- } };
231
- }
232
- //#endregion
233
50
  //#region src/tui/clickable.tsx
234
51
  /** Glyph / word aliases → OpenTUI key names (`parse.keypress` conventions). */
235
52
  const KEY_NAME_ALIASES = {
@@ -835,35 +652,6 @@ function runBashStreaming(opts) {
835
652
  });
836
653
  }
837
654
  //#endregion
838
- //#region src/tui/builtin-commands.ts
839
- function createBuiltinCommands(opts) {
840
- const commands = [{
841
- name: "help",
842
- description: "List the available / commands",
843
- run: (ctx) => {
844
- const all = [...opts.getCommands()].sort((a, b) => a.name.localeCompare(b.name));
845
- const body = all.length === 0 ? "_No commands are registered._" : all.map((c) => `- **/${c.name}** — ${c.description}`).join("\n");
846
- ctx.emit({ text: `### Commands\n\n${body}` });
847
- }
848
- }];
849
- if (opts.reloadExtensions) {
850
- const reload = opts.reloadExtensions;
851
- commands.push({
852
- name: "reload",
853
- description: "Reload extensions (re-discover, re-run setup, tear down the old instances)",
854
- run: async (ctx) => {
855
- ctx.emit({
856
- text: "",
857
- detail: { label: "reloading extensions…" }
858
- });
859
- const { active, inactive } = await reload();
860
- ctx.emit({ text: `Reloaded — **${active}** active${inactive > 0 ? `, ${inactive} inactive` : ""}. Agent-side contributions apply on the next run.` });
861
- }
862
- });
863
- }
864
- return commands;
865
- }
866
- //#endregion
867
655
  //#region src/tui/cancel-tool-modal.tsx
868
656
  /** @jsxImportSource @opentui/react */
869
657
  function CancelToolModal({ inFlight, onCancel, onCancelAll, onClose }) {
@@ -1746,6 +1534,68 @@ function RenderCall({ render }) {
1746
1534
  return /* @__PURE__ */ jsx$1(Fragment, { children: render() });
1747
1535
  }
1748
1536
  //#endregion
1537
+ //#region src/tui/session-titles.ts
1538
+ /**
1539
+ * Session id → title/directory lookup for transcript rows that address ANOTHER
1540
+ * session (the main's `sessions` tool calls). Same external-store shape as
1541
+ * `live-settings.ts`: the app publishes what it learns (every sessions
1542
+ * refresh, every session-event), leaf components subscribe.
1543
+ *
1544
+ * Titles the app has not seen yet — a worker the main created in a
1545
+ * directory outside the current project scope — are fetched once through
1546
+ * the resolver the app installs (a by-id header read against the store),
1547
+ * so the row upgrades from short id to title as soon as the read lands.
1548
+ */
1549
+ const refs = /* @__PURE__ */ new Map();
1550
+ const listeners = /* @__PURE__ */ new Set();
1551
+ const inFlight = /* @__PURE__ */ new Set();
1552
+ let resolver = null;
1553
+ function notify() {
1554
+ for (const listener of listeners) listener();
1555
+ }
1556
+ function publishSessionRefs(entries) {
1557
+ let changed = false;
1558
+ for (const { id, title, projectRoot } of entries) {
1559
+ const dir = sessionDirLabel(projectRoot);
1560
+ const prev = refs.get(id);
1561
+ if (prev?.title !== title || prev?.dir !== dir) {
1562
+ refs.set(id, dir ? {
1563
+ title,
1564
+ dir
1565
+ } : { title });
1566
+ changed = true;
1567
+ }
1568
+ }
1569
+ if (changed) notify();
1570
+ }
1571
+ /** Installed by the app once the store is known; `null` disables lazy lookups. */
1572
+ function setSessionRefResolver(next) {
1573
+ resolver = next;
1574
+ }
1575
+ function subscribe(listener) {
1576
+ listeners.add(listener);
1577
+ return () => {
1578
+ listeners.delete(listener);
1579
+ };
1580
+ }
1581
+ function requestRef(id) {
1582
+ if (!resolver || refs.has(id) || inFlight.has(id)) return;
1583
+ inFlight.add(id);
1584
+ resolver(id).then((ref) => {
1585
+ if (!ref) return;
1586
+ refs.set(id, ref);
1587
+ notify();
1588
+ }).catch(() => {}).finally(() => inFlight.delete(id));
1589
+ }
1590
+ /** The session's title + directory, or `undefined` until known. Kicks a lazy lookup on a miss. */
1591
+ function useSessionRef(id) {
1592
+ const ref = useSyncExternalStore(subscribe, () => id ? refs.get(id) : void 0, () => id ? refs.get(id) : void 0);
1593
+ useEffect(() => {
1594
+ if (id && ref === void 0) requestRef(id);
1595
+ }, [id, ref]);
1596
+ return ref;
1597
+ }
1598
+ //#endregion
1749
1599
  //#region src/tui/terminal-sanitize.ts
1750
1600
  /**
1751
1601
  * Terminal-display sanitation.
@@ -1995,7 +1845,7 @@ const BASH_RESULT_MAX_LINES = 10;
1995
1845
  * same-turn events read as one continuous highlighted block instead of a
1996
1846
  * striped list.
1997
1847
  */
1998
- const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) => {
1848
+ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false, onOpenSessionEvent }) => {
1999
1849
  const SURFACE = useSurfaces();
2000
1850
  const gap = marginTopFor(event, previous);
2001
1851
  const highlighted = selected;
@@ -2018,7 +1868,8 @@ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, an
2018
1868
  settleCodeEpoch,
2019
1869
  liveSpawn,
2020
1870
  searchQuery,
2021
- searchTitleMatch
1871
+ searchTitleMatch,
1872
+ onOpenSessionEvent
2022
1873
  })
2023
1874
  });
2024
1875
  });
@@ -2037,7 +1888,7 @@ function onInputSubmit(handler) {
2037
1888
  * Width tiering is driven by plain-text length estimates — close enough
2038
1889
  * since the segments are ASCII-heavy.
2039
1890
  */
2040
- function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null, usage = null, usageKey = null }) {
1891
+ function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null, usage = null, usageKey = null, daemonHealth = null }) {
2041
1892
  const { width } = useTerminalDimensions();
2042
1893
  const SURFACE = useSurfaces();
2043
1894
  const showCost = typeof cost === "number" && cost > 0;
@@ -2054,9 +1905,9 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2054
1905
  const timer = setInterval(() => setCompactTick((t) => t + 1), 1e3);
2055
1906
  return () => clearInterval(timer);
2056
1907
  }, [status]);
2057
- const statusLabel = status === "compacting" ? compactingLabel(compactionTokens, compactionStartedAt) : status === "searching" ? " Searching messages…" : status === "retrying" ? retryingLabel(retry) : "";
1908
+ const statusLabel = status === "compacting" ? ` ${compactingLabel(compactionTokens, compactionStartedAt)}` : status === "searching" ? " Searching messages…" : status === "retrying" ? retryingLabel(retry) : "";
2058
1909
  const sW = status === "compacting" || status === "searching" || status === "retrying" ? 2 + statusLabel.length : status === "asking" ? 3 : status ? 2 : 0;
2059
- if (hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
1910
+ if ((daemonHealth ? 2 : 0) + hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
2060
1911
  style: {
2061
1912
  flexDirection: "row",
2062
1913
  height: 1,
@@ -2065,6 +1916,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2065
1916
  backgroundColor: SURFACE.background
2066
1917
  },
2067
1918
  children: [
1919
+ /* @__PURE__ */ jsx$1(DaemonHealthDot, { health: daemonHealth }),
2068
1920
  /* @__PURE__ */ jsx$1(HintsText, { hints }),
2069
1921
  /* @__PURE__ */ jsx$1(FooterStatusIcon, {
2070
1922
  status,
@@ -2093,10 +1945,14 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2093
1945
  flexDirection: "row",
2094
1946
  height: 1
2095
1947
  },
2096
- children: [/* @__PURE__ */ jsx$1(HintsText, { hints: stackedHints }), /* @__PURE__ */ jsx$1(FooterStatusIcon, {
2097
- status,
2098
- label: statusLabel
2099
- })]
1948
+ children: [
1949
+ /* @__PURE__ */ jsx$1(DaemonHealthDot, { health: daemonHealth }),
1950
+ /* @__PURE__ */ jsx$1(HintsText, { hints: stackedHints }),
1951
+ /* @__PURE__ */ jsx$1(FooterStatusIcon, {
1952
+ status,
1953
+ label: statusLabel
1954
+ })
1955
+ ]
2100
1956
  }), rightW > 0 && /* @__PURE__ */ jsxs("box", {
2101
1957
  style: {
2102
1958
  flexDirection: "row",
@@ -2183,6 +2039,25 @@ function ClickableStatus({ keySpec, render }) {
2183
2039
  * compacting adds a short label because background compaction can otherwise
2184
2040
  * look like an idle spinner.
2185
2041
  */
2042
+ /**
2043
+ * The daemon health dot — bottom-left of the footer, two cells (`● `).
2044
+ *
2045
+ * Minimal on purpose: a single glyph in a theme slot, no label. The three
2046
+ * states are told apart by hue alone (green / amber / red via `accent` /
2047
+ * `warn` / `error`), which is enough for a status light and keeps the hints
2048
+ * beside it undisturbed. Renders nothing when `health` is `null` (feature
2049
+ * off), so the footer's width budget is only spent while the daemon is on.
2050
+ */
2051
+ function DaemonHealthDot({ health }) {
2052
+ const COLOR = useColors();
2053
+ if (!health) return null;
2054
+ const { tone } = DAEMON_HEALTH_STYLE[health];
2055
+ return /* @__PURE__ */ jsx$1("text", {
2056
+ wrapMode: "none",
2057
+ fg: tone === "accent" ? COLOR.accent : tone === "warn" ? COLOR.warn : COLOR.error,
2058
+ children: `● `
2059
+ });
2060
+ }
2186
2061
  function FooterStatusIcon({ status, label = "" }) {
2187
2062
  const COLOR = useColors();
2188
2063
  if (!status) return null;
@@ -2204,19 +2079,6 @@ function FooterStatusIcon({ status, label = "" }) {
2204
2079
  function retryingLabel(retry) {
2205
2080
  return retry ? ` Retrying (${retry.attempt}/${retry.max})...` : " Retrying...";
2206
2081
  }
2207
- /**
2208
- * Footer label for the compacting state — ` Compacting...` plus liveness:
2209
- * a `~2.1k` output-token estimate once the summary streams, and an elapsed
2210
- * suffix while it doesn't (prompt ingestion is minutes of silence on
2211
- * near-window sessions). Kept as a plain string so the Footer's width
2212
- * math and the rendered spans can't drift apart.
2213
- */
2214
- function compactingLabel(estTokens, startedAt) {
2215
- const base = " Compacting...";
2216
- const elapsedMs = typeof startedAt === "number" ? Date.now() - startedAt : 0;
2217
- const elapsed = elapsedMs >= 1e3 ? ` · ${formatDuration(elapsedMs)}` : "";
2218
- return typeof estTokens === "number" && estTokens > 0 ? `${base} ~${fmtTokens(estTokens)}${elapsed}` : `${base}${elapsed}`;
2219
- }
2220
2082
  function HintsText({ hints }) {
2221
2083
  return /* @__PURE__ */ jsx$1(HintRow, { hints });
2222
2084
  }
@@ -2292,7 +2154,8 @@ const USAGE_GLYPH = "▰";
2292
2154
  */
2293
2155
  function UsageIndicator({ usage, accent = false }) {
2294
2156
  const COLOR = useColors();
2295
- const color = accent ? COLOR.accent : usage.usedPercent >= 85 ? COLOR.error : usage.usedPercent >= 60 ? COLOR.warn : COLOR.dim;
2157
+ const tone = usageTone(usage.usedPercent);
2158
+ const color = accent ? COLOR.accent : tone === "error" ? COLOR.error : tone === "warn" ? COLOR.warn : COLOR.dim;
2296
2159
  return /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
2297
2160
  fg: accent ? COLOR.accent : COLOR.mute,
2298
2161
  children: `${USAGE_GLYPH} ${usage.label} `
@@ -2516,7 +2379,7 @@ function costIndicatorLength(cost) {
2516
2379
  function usageIndicatorLength(usage) {
2517
2380
  return 2 + usage.label.length + 1 + footerUsagePercent(usage.usedPercent).length + 1;
2518
2381
  }
2519
- const SPINNER_FRAMES = [
2382
+ const SPINNER_FRAMES$1 = [
2520
2383
  "▰▱▱▱▱",
2521
2384
  "▰▰▱▱▱",
2522
2385
  "▰▰▰▱▱",
@@ -2528,24 +2391,24 @@ const SPINNER_FRAMES = [
2528
2391
  "▱▱▱▱▰",
2529
2392
  "▱▱▱▱▱"
2530
2393
  ];
2531
- const SPINNER_INTERVAL_MS = 80;
2394
+ const SPINNER_INTERVAL_MS$1 = 80;
2532
2395
  /**
2533
2396
  * Tick a frame index every {@link SPINNER_INTERVAL_MS}. Pulled out of the
2534
2397
  * default `Spinner` body so the title-overlay status icon and any other
2535
2398
  * tiny single-glyph spinner can share the same animation cadence without
2536
2399
  * each instance running its own `setInterval`.
2537
2400
  */
2538
- function useSpinnerFrame() {
2401
+ function useSpinnerFrame$1() {
2539
2402
  const [frame, setFrame] = useState(0);
2540
2403
  useEffect(() => {
2541
- const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), SPINNER_INTERVAL_MS);
2404
+ const id = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES$1.length), SPINNER_INTERVAL_MS$1);
2542
2405
  return () => clearInterval(id);
2543
2406
  }, []);
2544
- return SPINNER_FRAMES[frame];
2407
+ return SPINNER_FRAMES$1[frame];
2545
2408
  }
2546
2409
  function Spinner({ label }) {
2547
2410
  const COLOR = useColors();
2548
- const glyph = useSpinnerFrame();
2411
+ const glyph = useSpinnerFrame$1();
2549
2412
  return /* @__PURE__ */ jsxs("text", {
2550
2413
  fg: COLOR.warn,
2551
2414
  children: [glyph, label !== void 0 && /* @__PURE__ */ jsx$1("span", {
@@ -2563,7 +2426,7 @@ function Spinner({ label }) {
2563
2426
  function StatusSpinner({ color }) {
2564
2427
  return /* @__PURE__ */ jsx$1("span", {
2565
2428
  fg: color,
2566
- children: useSpinnerFrame()
2429
+ children: useSpinnerFrame$1()
2567
2430
  });
2568
2431
  }
2569
2432
  /**
@@ -2711,7 +2574,7 @@ const NO_SEARCH_MATCHES = {
2711
2574
  * needs a stable id on `StreamEvent` itself, which is a data-model change
2712
2575
  * rather than a rendering one.
2713
2576
  */
2714
- function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, interactionPending = false, onLoadEarlier, onSelectTurn, onOpenTurn }) {
2577
+ function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, interactionPending = false, onLoadEarlier, onSelectTurn, onOpenTurn, onOpenSessionEvent }) {
2715
2578
  const COLOR = useColors();
2716
2579
  const showThrobber = busy && settings.showThrobber;
2717
2580
  const codeSettle = useRef({
@@ -2936,7 +2799,8 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
2936
2799
  settleCodeEpoch: codeSettle.current.targets.has(item.event) ? codeSettle.current.epoch : 0,
2937
2800
  liveSpawn: live?.[0],
2938
2801
  searchQuery,
2939
- searchTitleMatch: item.event.kind === "tool" && item.event.callId !== void 0 && searchMatches.callIds.has(item.event.callId) || (item.event.kind === "spawn-start" || item.event.kind === "spawn-end") && item.event.childId !== void 0 && searchMatches.childIds.has(item.event.childId)
2802
+ searchTitleMatch: item.event.kind === "tool" && item.event.callId !== void 0 && searchMatches.callIds.has(item.event.callId) || (item.event.kind === "spawn-start" || item.event.kind === "spawn-end") && item.event.childId !== void 0 && searchMatches.childIds.has(item.event.childId),
2803
+ onOpenSessionEvent
2940
2804
  });
2941
2805
  const selectActive = selectedTurnId !== null && (onSelectTurn !== void 0 || onOpenTurn !== void 0);
2942
2806
  const targetTurn = item.event.turnId && !item.event.childId ? ownership.get(item.event.turnId) ?? item.event.turnId : null;
@@ -3221,7 +3085,7 @@ function partitionTranscript(events, settings, searchReveal) {
3221
3085
  function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live, searchMatch = false }) {
3222
3086
  const COLOR = useColors();
3223
3087
  const matchColor = resolveChipColor(useSurfaces().chips, "default");
3224
- const frame = useSpinnerFrame();
3088
+ const frame = useSpinnerFrame$1();
3225
3089
  const terminal = event.kind === "spawn-end";
3226
3090
  const failed = terminal && (event.text.startsWith("error") || event.text.startsWith("aborted"));
3227
3091
  const turns = terminal ? event.spawn?.turns : live?.turns;
@@ -3309,11 +3173,6 @@ const THINKING_GUTTER_WIDTH = 4;
3309
3173
  * the ambiguity an aligned gutter cannot absorb.
3310
3174
  */
3311
3175
  const NOTICE_LABEL_WIDTH = 15;
3312
- const NOTICE_GLYPH = {
3313
- info: "·",
3314
- warn: "!",
3315
- error: "✗"
3316
- };
3317
3176
  function indentFor(depth) {
3318
3177
  return depth && depth > 0 ? depth * INDENT_PER_DEPTH : 0;
3319
3178
  }
@@ -3377,7 +3236,7 @@ function NoticeRow({ row, notice, text }) {
3377
3236
  })
3378
3237
  });
3379
3238
  }
3380
- function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) {
3239
+ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false, onOpenSessionEvent }) {
3381
3240
  const COLOR = useColors();
3382
3241
  const { settings } = useSettings();
3383
3242
  const cleanText = stripTerminalControlChars(event.text);
@@ -3386,7 +3245,7 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
3386
3245
  const row = rowStyle(indentFor(settings.hideSubagentOutput && (event.kind === "spawn-start" || event.kind === "spawn-end") ? 0 : effectiveDepth));
3387
3246
  const child = isChild(event);
3388
3247
  switch (event.kind) {
3389
- case "separator": return /* @__PURE__ */ jsx$1("text", { children: " " });
3248
+ case "separator": return null;
3390
3249
  case "user-prompt": return /* @__PURE__ */ jsx$1(UserPromptBlock, {
3391
3250
  text: safeText,
3392
3251
  refs: event.refs,
@@ -3456,7 +3315,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
3456
3315
  event,
3457
3316
  display: settings.toolCallDisplay === "full" ? "full" : "formatted",
3458
3317
  dim: child,
3459
- searchMatch: searchTitleMatch
3318
+ searchMatch: searchTitleMatch,
3319
+ onOpenSession: onOpenSessionEvent
3460
3320
  }), event.tool === "todowrite" && /* @__PURE__ */ jsx$1(TodoInProgressList, {
3461
3321
  input: event.input,
3462
3322
  dim: child
@@ -3539,6 +3399,15 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
3539
3399
  event,
3540
3400
  indent: row.paddingLeft
3541
3401
  });
3402
+ case "session-event": return /* @__PURE__ */ jsx$1(SessionEventBlock, {
3403
+ event,
3404
+ indent: row.paddingLeft,
3405
+ onOpen: onOpenSessionEvent
3406
+ });
3407
+ case "run-summary": return /* @__PURE__ */ jsx$1(RunSummaryLine, {
3408
+ event,
3409
+ indent: row.paddingLeft
3410
+ });
3542
3411
  default: return /* @__PURE__ */ jsx$1("text", { children: safeText });
3543
3412
  }
3544
3413
  }
@@ -3608,6 +3477,168 @@ function TaskNotificationBlock({ event, indent }) {
3608
3477
  })
3609
3478
  });
3610
3479
  }
3480
+ /** Maps a `SessionEventTone` onto the theme color a `session-event` row paints it in. */
3481
+ function sessionEventToneColor(tone, COLOR) {
3482
+ switch (tone) {
3483
+ case "accent": return COLOR.accent;
3484
+ case "error": return COLOR.error;
3485
+ case "warn": return COLOR.warn;
3486
+ case "dim": return COLOR.dim;
3487
+ }
3488
+ }
3489
+ /** Icon column width — ` ✓ ` (leading space, glyph, trailing space) — so line 2 can indent to the same column line 1's text starts at. */
3490
+ const SESSION_EVENT_GUTTER_WIDTH = 3;
3491
+ /**
3492
+ * Two-line card for a cross-session event delivered to a MAIN
3493
+ * (orchestrating) session: a worker finished, failed, was cancelled, or is
3494
+ * blocked on a decision.
3495
+ *
3496
+ * Line 1 is the receipt: status glyph, title, verb, then `sessionEventMeta`'s
3497
+ * cells (short id, duration, tokens, turns, cost) — the same content both
3498
+ * the TUI and GUI cards show for a run, just laid out for a terminal. Line 2
3499
+ * is the worker's last message (`sessionEventExcerpt`), dimmed and clamped to
3500
+ * the row's own available width so a long summary can't blow out the
3501
+ * transcript; it is omitted entirely when there is nothing to show.
3502
+ *
3503
+ * `needs-attention` reads deliberately louder — both the glyph and the title
3504
+ * ride `warn` (via `SESSION_EVENT_STATUS_STYLE`'s tone) and line 1 gets an
3505
+ * extra nudge — because it is the one status where a human, not the main,
3506
+ * has to act.
3507
+ *
3508
+ * Clickable (mouse only — this is a transcript row, not a focus target):
3509
+ * hovering underlines the title and appends a dim `→ open` hint so the user
3510
+ * can see the row will navigate; activating it opens the worker session
3511
+ * `sessionEvent.sessionId` points at, same destination as the
3512
+ * `openMainSession` chord's round trip. `sessionEvent` is optional on the
3513
+ * wire type, so a host that didn't attach the structured payload still gets
3514
+ * a readable (inert) line — same fallback as before.
3515
+ */
3516
+ /**
3517
+ * The per-run receipt — `model · 2m14s · 3 turns · $0.04 · ↑4.2k ↓812`.
3518
+ *
3519
+ * One muted line that never wraps: fitted to the row's width and ellipsized
3520
+ * by the shared formatter, so a narrow pane drops cells from the right rather
3521
+ * than spilling onto a second line and breaking the transcript's rhythm.
3522
+ * Content comes from `formatRunReceiptLine`, the same function the GUI and
3523
+ * replay use, so the line reads identically everywhere.
3524
+ */
3525
+ function RunSummaryLine({ event, indent }) {
3526
+ const COLOR = useColors();
3527
+ const { width: termWidth } = useTerminalDimensions();
3528
+ const receipt = event.runSummary;
3529
+ if (!receipt) return event.text ? /* @__PURE__ */ jsx$1("box", {
3530
+ style: {
3531
+ paddingLeft: indent,
3532
+ paddingBottom: 1
3533
+ },
3534
+ children: /* @__PURE__ */ jsx$1("text", {
3535
+ wrapMode: "none",
3536
+ fg: COLOR.mute,
3537
+ children: event.text
3538
+ })
3539
+ }) : null;
3540
+ const width = Math.max(0, termWidth - 2 - (indent ?? 0));
3541
+ const cells = fitRunReceiptCells(formatRunReceiptCells(receipt), width, 2);
3542
+ if (cells.length === 0) return null;
3543
+ const tint = (kind) => kind === "model" ? COLOR.model : kind === "turns" ? COLOR.brand : kind === "cost" ? COLOR.accent : kind === "speed" ? COLOR.warn : COLOR.dim;
3544
+ const segmentTint = (kind) => kind === "tokens-in" ? COLOR.brand : COLOR.accent;
3545
+ const renderCell = (cell) => {
3546
+ if (cell.segments) return cell.segments.map((seg, j) => /* @__PURE__ */ jsxs("span", { children: [j > 0 && /* @__PURE__ */ jsx$1("span", { children: " " }), /* @__PURE__ */ jsx$1("span", {
3547
+ fg: segmentTint(seg.kind),
3548
+ children: seg.text
3549
+ })] }, `${seg.kind}-${j}`));
3550
+ return /* @__PURE__ */ jsx$1("span", {
3551
+ fg: tint(cell.kind),
3552
+ children: cell.text
3553
+ });
3554
+ };
3555
+ return /* @__PURE__ */ jsx$1("box", {
3556
+ style: {
3557
+ paddingLeft: indent,
3558
+ paddingBottom: 1
3559
+ },
3560
+ children: /* @__PURE__ */ jsxs("text", {
3561
+ wrapMode: "none",
3562
+ children: [/* @__PURE__ */ jsx$1("span", {
3563
+ fg: COLOR.mute,
3564
+ children: `≡ `
3565
+ }), cells.map((cell, i) => /* @__PURE__ */ jsxs("span", { children: [i > 0 && /* @__PURE__ */ jsx$1("span", {
3566
+ fg: COLOR.mute,
3567
+ children: " · "
3568
+ }), renderCell(cell)] }, `${cell.kind}-${i}`))]
3569
+ })
3570
+ });
3571
+ }
3572
+ function SessionEventBlock({ event, indent, onOpen }) {
3573
+ const COLOR = useColors();
3574
+ const { width: termWidth } = useTerminalDimensions();
3575
+ const [hovered, setHovered] = useState(false);
3576
+ const sessionEvent = event.sessionEvent;
3577
+ if (!sessionEvent) return /* @__PURE__ */ jsx$1("box", {
3578
+ style: { paddingLeft: indent },
3579
+ children: /* @__PURE__ */ jsx$1("text", {
3580
+ fg: COLOR.dim,
3581
+ children: event.text
3582
+ })
3583
+ });
3584
+ const clickable = onOpen !== void 0;
3585
+ const style = SESSION_EVENT_STATUS_STYLE[sessionEvent.status];
3586
+ const tone = sessionEventToneColor(style.tone, COLOR);
3587
+ const meta = sessionEventMeta(sessionEvent);
3588
+ const contentWidth = Math.max(0, termWidth - 2 - (indent ?? 0) - SESSION_EVENT_GUTTER_WIDTH);
3589
+ const excerpt = contentWidth > 0 ? sessionEventExcerpt(sessionEvent, contentWidth) : "";
3590
+ const fixed = ` ${style.verb}`.length + (sessionEvent.status === "needs-attention" ? 18 : 0) + meta.reduce((n, cell) => n + ` · ${cell}`.length, 0) + ` · ${shortSessionId(sessionEvent.sessionId)}`.length + 7;
3591
+ const title = truncateTitle(sessionEvent.title, Math.max(12, contentWidth - fixed));
3592
+ return /* @__PURE__ */ jsxs("box", {
3593
+ style: {
3594
+ paddingLeft: indent,
3595
+ flexDirection: "column"
3596
+ },
3597
+ children: [/* @__PURE__ */ jsxs("text", {
3598
+ wrapMode: "none",
3599
+ selectable: false,
3600
+ onMouseDown: clickable ? (mouseEvent) => {
3601
+ mouseEvent.stopPropagation();
3602
+ mouseEvent.preventDefault();
3603
+ onOpen(sessionEvent.sessionId);
3604
+ } : void 0,
3605
+ onMouseOver: clickable ? () => setHovered(true) : void 0,
3606
+ onMouseOut: clickable ? () => setHovered(false) : void 0,
3607
+ children: [
3608
+ /* @__PURE__ */ jsx$1("span", {
3609
+ fg: tone,
3610
+ children: ` ${style.glyph} `
3611
+ }),
3612
+ /* @__PURE__ */ jsx$1("span", {
3613
+ fg: tone,
3614
+ attributes: hovered ? TextAttributes.BOLD | TextAttributes.UNDERLINE : TextAttributes.BOLD,
3615
+ children: title
3616
+ }),
3617
+ /* @__PURE__ */ jsx$1("span", { children: ` ${style.verb}` }),
3618
+ sessionEvent.status === "needs-attention" && /* @__PURE__ */ jsx$1("span", {
3619
+ fg: COLOR.warn,
3620
+ children: " — click to answer"
3621
+ }),
3622
+ meta.map((cell, i) => /* @__PURE__ */ jsx$1("span", {
3623
+ fg: COLOR.dim,
3624
+ children: ` · ${cell}`
3625
+ }, i)),
3626
+ /* @__PURE__ */ jsx$1("span", {
3627
+ fg: COLOR.mute,
3628
+ children: ` · ${shortSessionId(sessionEvent.sessionId)}`
3629
+ }),
3630
+ hovered && /* @__PURE__ */ jsx$1("span", {
3631
+ fg: COLOR.dim,
3632
+ children: " → open"
3633
+ })
3634
+ ]
3635
+ }), excerpt && /* @__PURE__ */ jsx$1("text", {
3636
+ wrapMode: "none",
3637
+ fg: COLOR.dim,
3638
+ children: `${" ".repeat(SESSION_EVENT_GUTTER_WIDTH)}${excerpt}`
3639
+ })]
3640
+ });
3641
+ }
3611
3642
  /**
3612
3643
  * Boundary row for `compact-summary` events. The persisted summary still
3613
3644
  * drives the provider context, but by default the TUI only shows metadata
@@ -3640,31 +3671,34 @@ function CompactSummaryBlock({ event, indent }) {
3640
3671
  meta && /* @__PURE__ */ jsxs(Fragment, { children: [
3641
3672
  /* @__PURE__ */ jsx$1("span", {
3642
3673
  fg: COLOR.mute,
3643
- children: " · model "
3674
+ children: " · "
3644
3675
  }),
3645
3676
  /* @__PURE__ */ jsx$1("span", {
3646
3677
  fg: COLOR.model,
3647
3678
  children: meta.model
3648
3679
  }),
3680
+ meta.costUsd !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
3681
+ fg: COLOR.mute,
3682
+ children: " · "
3683
+ }), /* @__PURE__ */ jsx$1("span", {
3684
+ fg: COLOR.accent,
3685
+ children: formatCostUsd(meta.costUsd)
3686
+ })] }),
3649
3687
  /* @__PURE__ */ jsx$1("span", {
3650
3688
  fg: COLOR.mute,
3651
3689
  children: " · "
3652
3690
  }),
3653
3691
  /* @__PURE__ */ jsx$1("span", {
3654
- fg: COLOR.dim,
3655
- children: fmtTokens(meta.inputTokens + meta.cacheReadTokens + meta.cacheCreationTokens)
3692
+ fg: COLOR.brand,
3693
+ children: `↑${formatTokenCount(meta.inputTokens + meta.cacheReadTokens + meta.cacheCreationTokens)}`
3656
3694
  }),
3657
3695
  /* @__PURE__ */ jsx$1("span", {
3658
3696
  fg: COLOR.mute,
3659
- children: " in / "
3660
- }),
3661
- /* @__PURE__ */ jsx$1("span", {
3662
- fg: COLOR.dim,
3663
- children: fmtTokens(meta.outputTokens)
3697
+ children: " "
3664
3698
  }),
3665
3699
  /* @__PURE__ */ jsx$1("span", {
3666
- fg: COLOR.mute,
3667
- children: " out"
3700
+ fg: COLOR.accent,
3701
+ children: `↓${formatTokenCount(meta.outputTokens)}`
3668
3702
  })
3669
3703
  ] })
3670
3704
  ]
@@ -4649,8 +4683,9 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
4649
4683
  });
4650
4684
  }
4651
4685
  const FULL_VIEW_MAX_INPUT_BYTES = 32 * 1024;
4652
- function ToolCallBlock({ event, display, dim, searchMatch = false }) {
4686
+ function ToolCallBlock({ event, display, dim, searchMatch = false, onOpenSession }) {
4653
4687
  const COLOR = useColors();
4688
+ const [hovered, setHovered] = useState(false);
4654
4689
  const matchColor = resolveChipColor(useSurfaces().chips, "default");
4655
4690
  const mdStyle = useMdStyle();
4656
4691
  const name = event.tool ?? "";
@@ -4665,6 +4700,8 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
4665
4700
  }
4666
4701
  }, [event.input]);
4667
4702
  const line = useMemo(() => event.input ? formatToolCall(name, event.input) : null, [event.input, name]);
4703
+ const sessionRef = useSessionRef(line?.sessionId);
4704
+ const { width: termWidth } = useTerminalDimensions();
4668
4705
  if (display === "full") return /* @__PURE__ */ jsxs("box", {
4669
4706
  style: {
4670
4707
  flexDirection: "column",
@@ -4703,9 +4740,19 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
4703
4740
  children: " (no structured input)"
4704
4741
  })]
4705
4742
  });
4706
- return /* @__PURE__ */ jsxs("text", {
4743
+ const targetSession = line?.sessionId;
4744
+ const clickable = targetSession !== void 0 && onOpenSession !== void 0;
4745
+ const header = /* @__PURE__ */ jsxs("text", {
4707
4746
  fg: dim ? COLOR.dim : COLOR.model,
4708
4747
  wrapMode: "none",
4748
+ selectable: !clickable,
4749
+ onMouseDown: clickable ? (mouseEvent) => {
4750
+ mouseEvent.stopPropagation();
4751
+ mouseEvent.preventDefault();
4752
+ onOpenSession(targetSession);
4753
+ } : void 0,
4754
+ onMouseOver: clickable ? () => setHovered(true) : void 0,
4755
+ onMouseOut: clickable ? () => setHovered(false) : void 0,
4709
4756
  children: [
4710
4757
  /* @__PURE__ */ jsx$1("span", {
4711
4758
  fg: COLOR.mute,
@@ -4716,19 +4763,49 @@ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
4716
4763
  bg: searchMatch ? matchColor.bg : void 0,
4717
4764
  children: verb
4718
4765
  }),
4719
- line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
4720
- fg: COLOR.mute,
4721
- children: " "
4722
- }), /* @__PURE__ */ jsx$1("span", {
4723
- fg: dim ? COLOR.dim : COLOR.warn,
4724
- children: line.target
4725
- })] }),
4766
+ line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [
4767
+ /* @__PURE__ */ jsx$1("span", {
4768
+ fg: COLOR.mute,
4769
+ children: " "
4770
+ }),
4771
+ /* @__PURE__ */ jsx$1("span", {
4772
+ fg: dim ? COLOR.dim : COLOR.warn,
4773
+ attributes: hovered ? TextAttributes.UNDERLINE : void 0,
4774
+ children: sessionRef?.title ?? line.target
4775
+ }),
4776
+ sessionRef?.dir && /* @__PURE__ */ jsx$1("span", {
4777
+ fg: COLOR.mute,
4778
+ children: ` · ${sessionRef.dir}`
4779
+ })
4780
+ ] }),
4726
4781
  line?.meta && line.meta.length > 0 && /* @__PURE__ */ jsx$1("span", {
4727
4782
  fg: COLOR.mute,
4728
4783
  children: ` · ${line.meta.join(" · ")}`
4784
+ }),
4785
+ hovered && /* @__PURE__ */ jsx$1("span", {
4786
+ fg: COLOR.dim,
4787
+ children: " → open"
4729
4788
  })
4730
4789
  ]
4731
4790
  });
4791
+ if (!line?.detail) return header;
4792
+ return /* @__PURE__ */ jsxs("box", {
4793
+ style: {
4794
+ flexDirection: "column",
4795
+ flexShrink: 0,
4796
+ alignSelf: "stretch"
4797
+ },
4798
+ children: [header, /* @__PURE__ */ jsx$1("text", {
4799
+ wrapMode: "none",
4800
+ fg: COLOR.dim,
4801
+ children: ` ${clipLine(line.detail, Math.max(0, termWidth - 4))}`
4802
+ })]
4803
+ });
4804
+ }
4805
+ /** Flatten whitespace and clip to `max` columns with a trailing ellipsis. */
4806
+ function clipLine(text, max) {
4807
+ const flat = text.replace(/\s+/g, " ").trim();
4808
+ return flat.length <= max ? flat : `${flat.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
4732
4809
  }
4733
4810
  function TodoInProgressList({ input, dim }) {
4734
4811
  const COLOR = useColors();
@@ -6142,94 +6219,6 @@ function readToolsByServer(dataDir) {
6142
6219
  return out;
6143
6220
  }
6144
6221
  //#endregion
6145
- //#region src/tui/editor-command.ts
6146
- /**
6147
- * `$EDITOR` / `$VISUAL` command parsing.
6148
- *
6149
- * These variables conventionally hold a command LINE, not a bare executable —
6150
- * `code --wait`, `cursor --wait`, `subl -w`, `emacsclient -nw` are all normal
6151
- * values. Spawning the whole string as argv[0] looks for a file literally named
6152
- * `cursor --wait` and fails with ENOENT, so the flags have to be split off.
6153
- *
6154
- * Kept separate from `app.tsx` (and free of `node:child_process`) so the
6155
- * tokenizer is unit-testable without pulling in the TUI.
6156
- */
6157
- /**
6158
- * Split an editor command line into `[executable, ...args]`.
6159
- *
6160
- * Whitespace-separated, with single and double quotes honored so an editor
6161
- * living under a path with spaces still works
6162
- * (`"/Applications/My App/bin/ed" -w`). Quotes group and are stripped;
6163
- * backslash escaping is deliberately NOT implemented, because on Windows
6164
- * backslash is the path separator and treating `C:\tools\ed` as escapes would
6165
- * corrupt far more command lines than it would rescue.
6166
- *
6167
- * Returns `[]` for an empty or whitespace-only spec, so callers can fall back
6168
- * to the platform opener rather than spawning `''`.
6169
- */
6170
- function parseEditorCommand(spec) {
6171
- const tokens = [];
6172
- let current = "";
6173
- let quote = null;
6174
- let quoted = false;
6175
- for (const ch of spec) {
6176
- if (quote) {
6177
- if (ch === quote) quote = null;
6178
- else current += ch;
6179
- continue;
6180
- }
6181
- if (ch === "\"" || ch === "'") {
6182
- quote = ch;
6183
- quoted = true;
6184
- continue;
6185
- }
6186
- if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
6187
- if (current !== "" || quoted) {
6188
- tokens.push(current);
6189
- current = "";
6190
- quoted = false;
6191
- }
6192
- continue;
6193
- }
6194
- current += ch;
6195
- }
6196
- if (current !== "" || quoted) tokens.push(current);
6197
- return tokens;
6198
- }
6199
- /**
6200
- * Re-quote a single argv token for a shell that will re-parse the joined command
6201
- * line — i.e. `spawn(..., { shell: true })`, which Node uses on Windows so that
6202
- * `.cmd` / `.bat` wrappers (how `code`, `cursor` and friends ship there) resolve.
6203
- *
6204
- * Node does NOT re-quote when `shell` is set: it joins `[cmd, ...args]` with
6205
- * spaces and hands the string over. Since {@link parseEditorCommand} has already
6206
- * STRIPPED the user's quotes, handing tokens back unquoted would split
6207
- * `C:\Program Files\ed.exe` at the space and try to run `C:\Program`. This puts
6208
- * the quotes back.
6209
- *
6210
- * Only tokens that need it are quoted, so a plain `code` stays `code`. A literal
6211
- * double quote is doubled, which is how `cmd.exe` escapes one inside a quoted
6212
- * run.
6213
- *
6214
- * The test covers `cmd.exe` METACHARACTERS, not just whitespace: `&`, `|`, `^`,
6215
- * `<`, `>` and friends carry none of the obvious "needs quoting" signals, so a
6216
- * repo cloned into `C:\work\a&b` would otherwise hand `C:\work\a&b\AGENTS.md`
6217
- * over bare, and cmd would split it at the `&` and run the tail as a second
6218
- * command.
6219
- *
6220
- * `%` and `!` are in the class so such a token is quoted rather than passed
6221
- * bare — but be clear about the limit: quoting does NOT stop cmd.exe expanding
6222
- * `%VAR%`, nor `!VAR!` where delayed expansion is on. Double quotes suppress
6223
- * splitting and other syntax, not expansion. That is a property of the shell
6224
- * which no amount of quoting can close; the worst case is a wrong path, not an
6225
- * injected command.
6226
- */
6227
- function quoteForShell(token) {
6228
- if (token === "") return "\"\"";
6229
- if (!/[\s"&|^<>%()!]/.test(token)) return token;
6230
- return `"${token.replace(/"/g, "\"\"")}"`;
6231
- }
6232
- //#endregion
6233
6222
  //#region src/tui/extension-modal.ts
6234
6223
  /**
6235
6224
  * Resolve a modal contribution to a renderable node. Eager nodes pass through;
@@ -6735,6 +6724,24 @@ function CountsBadge$1({ count }) {
6735
6724
  }
6736
6725
  //#endregion
6737
6726
  //#region src/tui/live-settings.ts
6727
+ /**
6728
+ * Process-global mirror of the live `Settings`, backing extension
6729
+ * `ctx.config.get`.
6730
+ *
6731
+ * The boot extension registry is built in `runTui` BEFORE React mounts,
6732
+ * so its per-extension `ctx.config.get` closures can't capture a React
6733
+ * ref — yet they must return the user's CURRENT config value when they
6734
+ * fire inside a hook / tool mid-session. This tiny holder bridges the
6735
+ * gap: `runTui` seeds it from the persisted boot snapshot, then the
6736
+ * mounted `App` keeps it in lock-step with the live `Settings`. Any
6737
+ * registry (boot or a cwd-rebuild) reads through {@link readLiveSettings},
6738
+ * so an extension always sees the latest values regardless of which
6739
+ * registry built its context.
6740
+ *
6741
+ * Singleton is acceptable: the TUI is one app per process. Core tests
6742
+ * never touch this — they pass their own `readSettings` getter to
6743
+ * `buildExtensionRegistry`.
6744
+ */
6738
6745
  /** Mutable holder — written by `runTui` (boot seed) + `App` (live updates). */
6739
6746
  const liveSettingsHolder = { current: void 0 };
6740
6747
  /** Stable getter handed to `discoverAndBuildExtensionRegistry` as `readSettings`. */
@@ -6745,6 +6752,58 @@ function readLiveSettings() {
6745
6752
  function setLiveSettings(settings) {
6746
6753
  liveSettingsHolder.current = settings;
6747
6754
  }
6755
+ /**
6756
+ * Whether a daemon controller is currently connected (daemon runtime).
6757
+ *
6758
+ * Bridges the same App↔AppShell scope gap as the settings mirror:
6759
+ * `onSettingsChange` lives in the top-level `App` (next to
6760
+ * `SettingsProvider`), but the daemon controller lives in `AppShell`. The
6761
+ * settings writer must persist LOCALLY whenever no daemon is connected
6762
+ * (auth/picker screens, before first activation, disconnect→reconnect
6763
+ * grace) — otherwise the change is only `publishDraft`-style broadcast and
6764
+ * silently lost on restart. AppShell keeps this flag current; `App` reads
6765
+ * it to decide whether the daemon owns the settings file right now.
6766
+ *
6767
+ * Singleton per process, same as the settings mirror.
6768
+ */
6769
+ /**
6770
+ * Emergency kill-switch for the daemon runtime — wins over the `useDaemon`
6771
+ * setting. Set by `zidane --no-daemon` (the CLI exports the env var so it
6772
+ * works identically in dev and compiled binaries) or directly via
6773
+ * `ZIDANE_NO_DAEMON=1`. Exists so a daemon-path bug can never lock the
6774
+ * user out: the in-process runtime is always one flag away, even when the
6775
+ * settings UI itself is unreachable.
6776
+ */
6777
+ function isDaemonDisabledByEnv() {
6778
+ return process$1.env.ZIDANE_NO_DAEMON === "1";
6779
+ }
6780
+ let daemonHealth = "disconnected";
6781
+ const daemonHealthListeners = /* @__PURE__ */ new Set();
6782
+ function setDaemonHealth(next) {
6783
+ if (daemonHealth === next) return;
6784
+ daemonHealth = next;
6785
+ for (const listener of daemonHealthListeners) listener();
6786
+ }
6787
+ /** Boolean shorthand kept for the existing call sites. */
6788
+ function setDaemonConnected(connected) {
6789
+ setDaemonHealth(connected ? "connected" : "disconnected");
6790
+ }
6791
+ function isDaemonConnected() {
6792
+ return daemonHealth === "connected";
6793
+ }
6794
+ function getDaemonHealth() {
6795
+ return daemonHealth;
6796
+ }
6797
+ function subscribeDaemonHealth(listener) {
6798
+ daemonHealthListeners.add(listener);
6799
+ return () => {
6800
+ daemonHealthListeners.delete(listener);
6801
+ };
6802
+ }
6803
+ /** React binding over the store above. */
6804
+ function useDaemonHealth() {
6805
+ return useSyncExternalStore(subscribeDaemonHealth, getDaemonHealth, getDaemonHealth);
6806
+ }
6748
6807
  //#endregion
6749
6808
  //#region src/tui/model-options-picker.tsx
6750
6809
  const BASE_LEVELS$1 = [
@@ -7045,16 +7104,7 @@ function selectedModelsFirst(entries, selectedKeys, keyOf) {
7045
7104
  function providerModelKey(providerKey, modelId) {
7046
7105
  return `${providerKey}\u0000${modelId}`;
7047
7106
  }
7048
- /** Move one selected entry by one priority slot; boundary/missing = identity. */
7049
- function reorderSelectedModel(selected, key, delta, keyOf) {
7050
- const from = selected.findIndex((entry) => keyOf(entry) === key);
7051
- const to = from + delta;
7052
- if (from < 0 || to < 0 || to >= selected.length) return selected;
7053
- const next = [...selected];
7054
- const [entry] = next.splice(from, 1);
7055
- next.splice(to, 0, entry);
7056
- return next;
7057
- }
7107
+ const reorderSelectedModel = reorderKeyed;
7058
7108
  function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onApplyChain }) {
7059
7109
  const COLOR = useColors();
7060
7110
  const SURFACE = useSurfaces();
@@ -7124,7 +7174,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7124
7174
  offCatalog: true,
7125
7175
  searchCorpus: ""
7126
7176
  } : null;
7127
- const commit = () => {
7177
+ const commit = (toggleOnly = false) => {
7128
7178
  const row = filtered[safeIndex];
7129
7179
  const target = row ? {
7130
7180
  provider: row.providerKey,
@@ -7136,7 +7186,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7136
7186
  if (!target) return;
7137
7187
  if (chainMode) {
7138
7188
  if (!row) setTypedEntries((prev) => prev.some((e) => e.provider === target.provider && e.model === target.model) ? prev : [...prev, target]);
7139
- setChainState((prev) => toggleChainEntry(prev, target));
7189
+ setChainState((prev) => toggleOnly ? toggleChainEntry(prev, target) : pickChainPrimary(prev, target));
7140
7190
  return;
7141
7191
  }
7142
7192
  onPick?.({
@@ -7198,6 +7248,10 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7198
7248
  });
7199
7249
  return;
7200
7250
  }
7251
+ if (key.name === "tab") {
7252
+ commit(true);
7253
+ return;
7254
+ }
7201
7255
  if (key.name === "return") commit();
7202
7256
  });
7203
7257
  if (providers.length === 0) return /* @__PURE__ */ jsx$1(EmptyProvidersState, {});
@@ -7260,7 +7314,12 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7260
7314
  },
7261
7315
  {
7262
7316
  key: "↵",
7263
- label: customEntry ? "use anyway" : chainMode ? "add/remove" : "select",
7317
+ label: customEntry ? "use anyway" : chainMode ? "use as primary" : "select",
7318
+ labelColor: COLOR.dim
7319
+ },
7320
+ chainMode && {
7321
+ key: "⇥",
7322
+ label: "add/remove fallback",
7264
7323
  labelColor: COLOR.dim
7265
7324
  },
7266
7325
  chainMode && {
@@ -7564,6 +7623,425 @@ function describeModel(m) {
7564
7623
  return parts.join(" · ");
7565
7624
  }
7566
7625
  //#endregion
7626
+ //#region src/tui/runtime/daemon.ts
7627
+ /** JSON-RPC "method not found" — the daemon predates an optional feature. */
7628
+ const METHOD_NOT_FOUND = -32601;
7629
+ /**
7630
+ * Run an RPC for an OPTIONAL feature: an older daemon that doesn't know
7631
+ * the method degrades the feature silently instead of failing the caller.
7632
+ * Version-skewed daemons exist by construction (a daemon process is frozen
7633
+ * at spawn time while windows come from newer builds mid-development).
7634
+ */
7635
+ async function optional(call) {
7636
+ try {
7637
+ return await call();
7638
+ } catch (err) {
7639
+ if (err instanceof JsonRpcRemoteError && err.code === METHOD_NOT_FOUND) return null;
7640
+ if (err instanceof JsonRpcConnectionClosedError) return null;
7641
+ throw err;
7642
+ }
7643
+ }
7644
+ async function connectDaemonSession(options) {
7645
+ const { sessionId, callbacks } = options;
7646
+ let disposed = false;
7647
+ const client = await connectOrSpawnDaemon({
7648
+ ...options.socketPath ? { socketPath: options.socketPath } : {},
7649
+ spawnCommand: options.spawnCommand ?? selfDaemonSpawnCommand(),
7650
+ ...options.appVersion ? { appVersion: options.appVersion } : {},
7651
+ onClose: () => {
7652
+ if (!disposed) callbacks.onDisconnect?.();
7653
+ }
7654
+ });
7655
+ let running = false;
7656
+ /** A `run:start` observed via signal wins over the initial RPC probe. */
7657
+ let sawRunStart = false;
7658
+ /** Sequence for `suggest.resolve` — see `resolveSuggestion`. */
7659
+ let suggestSeq = 0;
7660
+ /**
7661
+ * Live `event` signals that arrive between subscribing and delivering
7662
+ * the authoritative `sessions.loadEvents` snapshot are buffered, then
7663
+ * flushed in order once the snapshot lands — so nothing is missed in the
7664
+ * gap and the snapshot never clobbers a live event.
7665
+ */
7666
+ let transcriptReady = false;
7667
+ const bufferedEvents = [];
7668
+ /**
7669
+ * De-dup HITL surfacing across the backfill + live-signal overlap, and
7670
+ * skip requests another window already resolved.
7671
+ */
7672
+ const surfacedHitl = /* @__PURE__ */ new Set();
7673
+ const resolvedHitl = /* @__PURE__ */ new Set();
7674
+ const surfaceApproval = (request) => {
7675
+ if (request.sessionId !== sessionId || surfacedHitl.has(request.id) || resolvedHitl.has(request.id)) return;
7676
+ surfacedHitl.add(request.id);
7677
+ callbacks.onApproval(request, (decision) => {
7678
+ if (disposed || resolvedHitl.has(request.id)) return;
7679
+ client.request("approvals.resolve", {
7680
+ requestId: request.id,
7681
+ decision
7682
+ }).catch(() => void 0);
7683
+ });
7684
+ };
7685
+ const surfaceInteraction = (request) => {
7686
+ if (surfacedHitl.has(request.id) || resolvedHitl.has(request.id)) return;
7687
+ surfacedHitl.add(request.id);
7688
+ callbacks.onInteraction(request, {
7689
+ respond: (response) => {
7690
+ if (disposed || resolvedHitl.has(request.id)) return;
7691
+ client.request("interactions.respond", {
7692
+ requestId: request.id,
7693
+ response
7694
+ }).catch(() => void 0);
7695
+ },
7696
+ cancel: () => void 0
7697
+ });
7698
+ };
7699
+ const unsubscribe = client.onSignal((signal) => {
7700
+ switch (signal.kind) {
7701
+ case "approval:request":
7702
+ surfaceApproval(signal.request);
7703
+ return;
7704
+ case "settings:changed":
7705
+ callbacks.onSettingsChanged?.(signal.settings);
7706
+ return;
7707
+ case "sessions:changed":
7708
+ callbacks.onSessionsChanged?.(signal.op, signal.sessionId);
7709
+ return;
7710
+ case "presence:changed":
7711
+ callbacks.onPresenceChanged?.(signal.sessions);
7712
+ return;
7713
+ case "hud:state":
7714
+ callbacks.onHudState?.({
7715
+ statuses: signal.statuses,
7716
+ banners: signal.banners
7717
+ });
7718
+ return;
7719
+ case "hud:notify":
7720
+ callbacks.onHudNotify?.(signal);
7721
+ return;
7722
+ case "extension:signal":
7723
+ callbacks.onExtensionSignal?.({
7724
+ extensionId: signal.extensionId,
7725
+ ...signal.sessionId !== void 0 ? { sessionId: signal.sessionId } : {},
7726
+ payload: signal.payload,
7727
+ seq: signal.seq,
7728
+ ...signal.nonce !== void 0 ? { nonce: signal.nonce } : {}
7729
+ });
7730
+ return;
7731
+ case "approval:resolved":
7732
+ if (signal.sessionId === sessionId) {
7733
+ resolvedHitl.add(signal.requestId);
7734
+ if (surfacedHitl.has(signal.requestId)) callbacks.onApprovalResolved?.(signal.requestId);
7735
+ }
7736
+ return;
7737
+ case "interaction:request":
7738
+ if (signal.sessionId === sessionId) surfaceInteraction(signal.request);
7739
+ return;
7740
+ case "interaction:resolved":
7741
+ if (signal.sessionId === sessionId) {
7742
+ resolvedHitl.add(signal.requestId);
7743
+ if (surfacedHitl.has(signal.requestId)) callbacks.onInteractionResolved?.(signal.requestId);
7744
+ }
7745
+ return;
7746
+ default: break;
7747
+ }
7748
+ if (!("sessionId" in signal) || signal.sessionId !== sessionId) return;
7749
+ switch (signal.kind) {
7750
+ case "stream:retry":
7751
+ callbacks.onRetry?.({
7752
+ attempt: signal.attempt,
7753
+ max: signal.max,
7754
+ source: signal.source
7755
+ });
7756
+ break;
7757
+ case "turn:end":
7758
+ callbacks.onTurnEnd?.({
7759
+ turnId: signal.turnId,
7760
+ ...signal.usage ? { usage: signal.usage } : {},
7761
+ ...signal.childId ? { childId: signal.childId } : {}
7762
+ });
7763
+ break;
7764
+ case "run:start":
7765
+ running = true;
7766
+ sawRunStart = true;
7767
+ callbacks.onRunState(true);
7768
+ break;
7769
+ case "run:end":
7770
+ running = false;
7771
+ callbacks.onRunState(false, {
7772
+ ok: signal.ok,
7773
+ ...signal.error ? { error: signal.error } : {}
7774
+ });
7775
+ break;
7776
+ case "event":
7777
+ if (transcriptReady) callbacks.onStreamEvent(signal.event);
7778
+ else bufferedEvents.push(signal.event);
7779
+ break;
7780
+ case "compacting:progress":
7781
+ callbacks.onCompactionProgress?.(signal.estOutputTokens);
7782
+ break;
7783
+ case "prompt-turns":
7784
+ callbacks.onPromptTurns(signal.refs);
7785
+ break;
7786
+ case "stream:retry:cleared":
7787
+ callbacks.onRetryCleared(signal.source);
7788
+ break;
7789
+ case "compacting:start":
7790
+ callbacks.onCompacting?.(true);
7791
+ break;
7792
+ case "compacting:end":
7793
+ callbacks.onCompacting?.(false, {
7794
+ ok: signal.ok,
7795
+ ...signal.effectiveTokens !== void 0 ? { effectiveTokens: signal.effectiveTokens } : {}
7796
+ });
7797
+ break;
7798
+ case "model:fallback":
7799
+ callbacks.onModelFallback?.(signal.from, signal.to);
7800
+ break;
7801
+ case "draft:changed":
7802
+ callbacks.onDraftChanged?.(signal.text, signal.shell === true);
7803
+ break;
7804
+ case "draft:attachments":
7805
+ callbacks.onDraftAttachmentsChanged?.(signal.attachments);
7806
+ break;
7807
+ case "search:changed":
7808
+ callbacks.onSearchChanged?.({
7809
+ active: signal.active,
7810
+ query: signal.query
7811
+ });
7812
+ break;
7813
+ case "suggest:changed":
7814
+ callbacks.onSuggestChanged?.({
7815
+ seq: signal.seq,
7816
+ basis: signal.basis,
7817
+ caret: signal.caret,
7818
+ suggestion: signal.suggestion,
7819
+ ...signal.candidates ? { candidates: signal.candidates } : {}
7820
+ });
7821
+ break;
7822
+ case "skills:snapshot":
7823
+ callbacks.onSkillsChanged?.(signal.active);
7824
+ break;
7825
+ case "mcp:auth":
7826
+ callbacks.onMcpAuth?.({
7827
+ name: signal.name,
7828
+ reason: signal.reason
7829
+ });
7830
+ break;
7831
+ case "queue:changed":
7832
+ callbacks.onQueueChanged?.(signal.items);
7833
+ break;
7834
+ case "work:snapshot":
7835
+ callbacks.onWorkChanged?.(signal.items);
7836
+ break;
7837
+ case "todos:snapshot":
7838
+ callbacks.onTodosChanged?.(signal.snapshot);
7839
+ break;
7840
+ case "cache:break":
7841
+ callbacks.onCacheBreak(signal.info);
7842
+ break;
7843
+ case "system-md":
7844
+ callbacks.onSystemMd(signal.file, signal.shadowed);
7845
+ break;
7846
+ default: break;
7847
+ }
7848
+ });
7849
+ try {
7850
+ const initialEvents = await client.request("sessions.loadEvents", { sessionId });
7851
+ callbacks.onInitialTranscript(initialEvents);
7852
+ transcriptReady = true;
7853
+ for (const event of bufferedEvents.splice(0)) callbacks.onStreamEvent(event);
7854
+ if (!sawRunStart) {
7855
+ running = await client.request("chat.isRunning", { sessionId });
7856
+ if (running) callbacks.onRunState(true);
7857
+ }
7858
+ for (const request of await client.request("approvals.listPending", {})) surfaceApproval(request);
7859
+ await client.request("interactions.rehydrate", { sessionId });
7860
+ for (const pending of await client.request("interactions.list", { sessionId })) surfaceInteraction(pending.request);
7861
+ client.notify("presence.watch", { sessionId });
7862
+ const [presence, queued, draft, systemMd, todos] = await Promise.all([
7863
+ optional(() => client.request("presence.list", {})),
7864
+ optional(() => client.request("queue.list", { sessionId })),
7865
+ optional(() => client.request("drafts.get", { sessionId })),
7866
+ optional(() => client.request("sessions.systemMd", { sessionId })),
7867
+ optional(() => client.request("todos.get", { sessionId }))
7868
+ ]);
7869
+ if (presence) callbacks.onPresenceChanged?.(presence);
7870
+ if (queued && queued.length > 0) callbacks.onQueueChanged?.(queued);
7871
+ if (draft && (draft.text.length > 0 || draft.shell)) callbacks.onDraftChanged?.(draft.text, draft.shell === true);
7872
+ if (draft?.attachments?.length) callbacks.onDraftAttachmentsChanged?.(draft.attachments);
7873
+ callbacks.onSearchChanged?.(draft?.search ?? {
7874
+ active: false,
7875
+ query: ""
7876
+ });
7877
+ callbacks.onSystemMd(systemMd?.file ?? null, systemMd?.shadowed ?? []);
7878
+ if (todos) callbacks.onTodosChanged?.(todos);
7879
+ } catch (err) {
7880
+ unsubscribe();
7881
+ client.close();
7882
+ throw err;
7883
+ }
7884
+ return {
7885
+ sessionId,
7886
+ get isRunning() {
7887
+ return running;
7888
+ },
7889
+ async send(prompt, sendOptions) {
7890
+ if (sendOptions?.attachments?.length) {
7891
+ await client.request("chat.send", {
7892
+ sessionId,
7893
+ prompt,
7894
+ attachments: sendOptions.attachments,
7895
+ ...sendOptions.activatedSkills?.length ? { activatedSkills: sendOptions.activatedSkills } : {}
7896
+ });
7897
+ return "send";
7898
+ }
7899
+ const { delivered } = await client.request("chat.pushQueued", {
7900
+ sessionId,
7901
+ message: prompt,
7902
+ ...sendOptions?.activatedSkills?.length ? { activatedSkills: sendOptions.activatedSkills } : {}
7903
+ });
7904
+ return delivered;
7905
+ },
7906
+ interrupt() {
7907
+ client.request("chat.interrupt", { sessionId }).catch(() => void 0);
7908
+ },
7909
+ publishSettings(patch) {
7910
+ client.request("settings.set", { patch }).catch(() => void 0);
7911
+ },
7912
+ touchSession() {
7913
+ client.notify("sessions.touch", { sessionId });
7914
+ },
7915
+ publishDraftAttachments(attachments) {
7916
+ client.notify("drafts.setAttachments", {
7917
+ sessionId,
7918
+ attachments
7919
+ });
7920
+ },
7921
+ publishSearch(active, query) {
7922
+ client.notify("search.set", {
7923
+ sessionId,
7924
+ active,
7925
+ query
7926
+ });
7927
+ },
7928
+ emitExtension(envelope) {
7929
+ client.notify("extension.emit", envelope);
7930
+ },
7931
+ async resolveSuggestion(text, caret) {
7932
+ suggestSeq += 1;
7933
+ return optional(() => client.request("suggest.resolve", {
7934
+ sessionId,
7935
+ text,
7936
+ caret,
7937
+ seq: suggestSeq
7938
+ }));
7939
+ },
7940
+ publishDraft(text, shell) {
7941
+ client.notify("drafts.set", {
7942
+ sessionId,
7943
+ text,
7944
+ ...shell ? { shell: true } : {}
7945
+ });
7946
+ },
7947
+ async queueAdd(text, options) {
7948
+ const result = await optional(() => client.request("queue.add", {
7949
+ sessionId,
7950
+ text,
7951
+ ...options?.activatedSkills?.length ? { activatedSkills: options.activatedSkills } : {},
7952
+ ...options?.attachments?.length ? { attachments: options.attachments } : {}
7953
+ }));
7954
+ if (result) return result.delivered;
7955
+ const { delivered } = await client.request("chat.pushQueued", {
7956
+ sessionId,
7957
+ message: text,
7958
+ ...options?.activatedSkills?.length ? { activatedSkills: options.activatedSkills } : {}
7959
+ });
7960
+ return delivered === "steer" ? "queued" : "sent";
7961
+ },
7962
+ async queueRemove(id) {
7963
+ return await optional(() => client.request("queue.remove", {
7964
+ sessionId,
7965
+ id
7966
+ })) ?? false;
7967
+ },
7968
+ async setTodos(items) {
7969
+ return await optional(() => client.request("todos.set", {
7970
+ sessionId,
7971
+ items
7972
+ })) ?? null;
7973
+ },
7974
+ async queuePushNow(id) {
7975
+ return (await optional(() => client.request("queue.pushNow", {
7976
+ sessionId,
7977
+ id
7978
+ })))?.delivered ?? "missing";
7979
+ },
7980
+ async fetchTranscript() {
7981
+ return optional(() => client.request("sessions.loadEvents", { sessionId }));
7982
+ },
7983
+ async applyModel(options) {
7984
+ await optional(() => client.request("sessions.setModel", {
7985
+ sessionId,
7986
+ providerKey: options.providerKey,
7987
+ modelId: options.modelId
7988
+ }));
7989
+ if (options.effort !== void 0) await optional(() => client.request("sessions.setEffort", {
7990
+ sessionId,
7991
+ effort: options.effort
7992
+ }));
7993
+ if (options.modelOptions !== void 0) await optional(() => client.request("sessions.setModelOptions", {
7994
+ sessionId,
7995
+ options: options.modelOptions
7996
+ }));
7997
+ },
7998
+ async setAgentId(agentId) {
7999
+ await optional(() => client.request("sessions.setAgentId", {
8000
+ sessionId,
8001
+ agentId
8002
+ }));
8003
+ },
8004
+ async fetchContextBreakdown() {
8005
+ return optional(() => client.request("sessions.contextBreakdown", { sessionId }));
8006
+ },
8007
+ async mainSession() {
8008
+ return optional(() => client.request("orchestration.main", {}));
8009
+ },
8010
+ async searchTranscript(query, limit) {
8011
+ return optional(() => client.request("history.searchSession", {
8012
+ sessionId,
8013
+ query,
8014
+ ...limit !== void 0 ? { limit } : {}
8015
+ }));
8016
+ },
8017
+ async searchHistory(query, sessionIds, excerptLimit) {
8018
+ return optional(() => client.request("history.searchAll", {
8019
+ query,
8020
+ sessionIds,
8021
+ ...excerptLimit !== void 0 ? { excerptLimit } : {}
8022
+ }));
8023
+ },
8024
+ async rebuildAgent() {
8025
+ await optional(() => client.request("chat.rebuildAgent", { sessionId }));
8026
+ },
8027
+ async fetchPresence() {
8028
+ return optional(() => client.request("presence.list", {}));
8029
+ },
8030
+ async cancelWork(id, reason) {
8031
+ return await optional(() => client.request("chat.cancelWork", {
8032
+ sessionId,
8033
+ id,
8034
+ ...reason ? { reason } : {}
8035
+ })) ?? false;
8036
+ },
8037
+ dispose() {
8038
+ disposed = true;
8039
+ unsubscribe();
8040
+ client.close();
8041
+ }
8042
+ };
8043
+ }
8044
+ //#endregion
7567
8045
  //#region src/chat/external-editor.ts
7568
8046
  /**
7569
8047
  * External-editor handoff — open a piece of text in the user's `$EDITOR`,
@@ -8256,16 +8734,69 @@ function CompletionPopup({ state, visibleRows = 6 }) {
8256
8734
  });
8257
8735
  }
8258
8736
  //#endregion
8737
+ //#region src/tui/spinner.ts
8738
+ /**
8739
+ * Shared braille spinner language — one frame set + cadence for every
8740
+ * inline working glyph in the TUI (extension HUD loader, session-list
8741
+ * presence spinner). Kept out of any component module so consumers on
8742
+ * different screens don't drag each other's imports in.
8743
+ */
8744
+ const SPINNER_FRAMES = [
8745
+ "⠋",
8746
+ "⠙",
8747
+ "⠹",
8748
+ "⠸",
8749
+ "⠼",
8750
+ "⠴",
8751
+ "⠦",
8752
+ "⠧",
8753
+ "⠇",
8754
+ "⠏"
8755
+ ];
8756
+ /**
8757
+ * Frame index ticking at {@link SPINNER_INTERVAL_MS} — the interval only
8758
+ * runs while `active`; an idle consumer re-renders zero times. Returns
8759
+ * the raw index (not the glyph) so callers can also derive slower
8760
+ * secondary rhythms (e.g. a pulse) from the same clock.
8761
+ *
8762
+ * The index wraps at `SPINNER_FRAMES.length` (even), so `frame % 2`
8763
+ * parity stays continuous across the wrap.
8764
+ */
8765
+ function useSpinnerFrame(active) {
8766
+ const [frame, setFrame] = useState(0);
8767
+ useEffect(() => {
8768
+ if (!active) return;
8769
+ const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 120);
8770
+ return () => clearInterval(timer);
8771
+ }, [active]);
8772
+ return frame;
8773
+ }
8774
+ //#endregion
8259
8775
  //#region src/tui/tui-hud.ts
8260
- let holder$3 = null;
8776
+ let holder$4 = null;
8261
8777
  /** Initialize (or return) the process-wide HUD. Idempotent. */
8262
8778
  function initTuiHud() {
8263
- holder$3 ??= createExtensionHud();
8264
- return holder$3;
8779
+ holder$4 ??= createExtensionHud();
8780
+ return holder$4;
8265
8781
  }
8266
8782
  /** The HUD, or null when `runTui` hasn't wired it (bare embedders). */
8267
8783
  function getTuiHud() {
8268
- return holder$3;
8784
+ return holder$4;
8785
+ }
8786
+ let remoteState = null;
8787
+ const remoteListeners = /* @__PURE__ */ new Set();
8788
+ function setRemoteHudState(state) {
8789
+ remoteState = state;
8790
+ for (const listener of remoteListeners) listener();
8791
+ }
8792
+ function getRemoteHudState() {
8793
+ return remoteState;
8794
+ }
8795
+ function subscribeRemoteHudState(listener) {
8796
+ remoteListeners.add(listener);
8797
+ return () => {
8798
+ remoteListeners.delete(listener);
8799
+ };
8269
8800
  }
8270
8801
  //#endregion
8271
8802
  //#region src/tui/extension-hud.tsx
@@ -8273,10 +8804,27 @@ const EMPTY_STATE = {
8273
8804
  statuses: {},
8274
8805
  banners: {}
8275
8806
  };
8276
- /** Subscribe to the HUD (inert empty state when unwired). */
8807
+ /**
8808
+ * Subscribe to the HUD (inert empty state when unwired), merged with the
8809
+ * remote overlay forwarded from the daemon (whose extension hooks run
8810
+ * out-of-process). Local entries win per extension name — a local write
8811
+ * is this window's own action.
8812
+ */
8277
8813
  function useHudState() {
8278
8814
  const hud = getTuiHud();
8279
- return useSyncExternalStore((listener) => hud ? hud.subscribe(listener) : () => {}, () => hud ? hud.getState() : EMPTY_STATE);
8815
+ const local = useSyncExternalStore((listener) => hud ? hud.subscribe(listener) : () => {}, () => hud ? hud.getState() : EMPTY_STATE);
8816
+ const remote = useSyncExternalStore(subscribeRemoteHudState, getRemoteHudState);
8817
+ if (!remote) return local;
8818
+ return {
8819
+ statuses: {
8820
+ ...remote.statuses,
8821
+ ...local.statuses
8822
+ },
8823
+ banners: {
8824
+ ...remote.banners,
8825
+ ...local.banners
8826
+ }
8827
+ };
8280
8828
  }
8281
8829
  /**
8282
8830
  * Live enabled-set filter — a Settings toggle-off hides the extension's
@@ -8291,34 +8839,11 @@ function useVisibleEntries(record) {
8291
8839
  const entries = Object.entries(record);
8292
8840
  return allow === void 0 ? entries : entries.filter(([name]) => allow.includes(name));
8293
8841
  }
8294
- const LOADER_FRAMES = [
8295
- "⠋",
8296
- "⠙",
8297
- "⠹",
8298
- "⠸",
8299
- "⠼",
8300
- "⠴",
8301
- "⠦",
8302
- "⠧",
8303
- "⠇",
8304
- "⠏"
8305
- ];
8306
- const LOADER_INTERVAL_MS = 120;
8307
- /** Animated loader frame — the interval only runs while a loader is visible. */
8308
- function useLoaderFrame(active) {
8309
- const [frame, setFrame] = useState(0);
8310
- useEffect(() => {
8311
- if (!active) return;
8312
- const timer = setInterval(() => setFrame((f) => (f + 1) % LOADER_FRAMES.length), LOADER_INTERVAL_MS);
8313
- return () => clearInterval(timer);
8314
- }, [active]);
8315
- return LOADER_FRAMES[frame % LOADER_FRAMES.length];
8316
- }
8317
8842
  function ExtensionStatusLine() {
8318
8843
  const COLOR = useColors();
8319
8844
  const { statuses } = useHudState();
8320
8845
  const entries = useVisibleEntries(statuses);
8321
- const glyph = useLoaderFrame(entries.some(([, status]) => status.loader));
8846
+ const glyph = SPINNER_FRAMES[useSpinnerFrame(entries.some(([, status]) => status.loader)) % SPINNER_FRAMES.length];
8322
8847
  if (entries.length === 0) return null;
8323
8848
  return /* @__PURE__ */ jsx$1("text", {
8324
8849
  fg: COLOR.dim,
@@ -11496,7 +12021,7 @@ const PAGE_JUMP = 6;
11496
12021
  function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange, contentMatches = /* @__PURE__ */ new Map(), contentSearching = false, contentSearchProgress = {
11497
12022
  done: 0,
11498
12023
  total: 0
11499
- }, onSearchQueryChange, showAllProjects = false, onToggleScope, currentProjectRoot }) {
12024
+ }, onSearchQueryChange, showAllProjects = false, scopeLabel, presence, onToggleScope, currentProjectRoot, mainSessionId = null }) {
11500
12025
  const focused = useModalAwareFocus();
11501
12026
  const COLOR = useColors();
11502
12027
  const inputRef = useRef(null);
@@ -11519,26 +12044,50 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
11519
12044
  query,
11520
12045
  contentMatches
11521
12046
  ]);
11522
- const rows = useMemo(() => [{
11523
- kind: "new",
11524
- rowId: NEW_SESSION_ROW_ID,
11525
- meta: null
11526
- }, ...filteredSessions.map((meta) => ({
11527
- kind: "session",
11528
- rowId: meta.id,
11529
- meta,
11530
- ...query.trim() && contentMatches.get(meta.id) ? { contentMatch: contentMatches.get(meta.id) } : {}
11531
- }))], [filteredSessions, contentMatches]);
12047
+ const rows = useMemo(() => {
12048
+ const isMain = (meta) => meta.isMain === true || meta.id === mainSessionId;
12049
+ const sessionRows = filteredSessions.map((meta) => ({
12050
+ kind: "session",
12051
+ rowId: meta.id,
12052
+ meta,
12053
+ ...isMain(meta) ? { isMain: true } : {},
12054
+ ...query.trim() && contentMatches.get(meta.id) ? { contentMatch: contentMatches.get(meta.id) } : {}
12055
+ }));
12056
+ const mainIndex = sessionRows.findIndex((row) => row.isMain);
12057
+ return [
12058
+ ...mainIndex >= 0 ? sessionRows.splice(mainIndex, 1) : [],
12059
+ {
12060
+ kind: "new",
12061
+ rowId: NEW_SESSION_ROW_ID,
12062
+ meta: null
12063
+ },
12064
+ ...sessionRows
12065
+ ];
12066
+ }, [
12067
+ filteredSessions,
12068
+ contentMatches,
12069
+ mainSessionId
12070
+ ]);
12071
+ const spinnerFrame = useSpinnerFrame(useMemo(() => {
12072
+ if (!presence) return false;
12073
+ return rows.some((row) => {
12074
+ if (row.kind !== "session") return false;
12075
+ const info = presence.get(row.rowId);
12076
+ return info !== void 0 && (info.attention || info.running);
12077
+ });
12078
+ }, [rows, presence]));
11532
12079
  useEffect(() => {
11533
12080
  if (focused) inputRef.current?.focus();
11534
12081
  }, [focused]);
11535
12082
  const cursorIndex = useMemo(() => {
11536
- if (focusedSessionId === "__new__") return 0;
12083
+ const newIdx = Math.max(0, rows.findIndex((r) => r.kind === "new"));
12084
+ if (focusedSessionId === "__new__") return newIdx;
11537
12085
  if (focusedSessionId) {
11538
12086
  const idx = rows.findIndex((r) => r.kind === "session" && r.rowId === focusedSessionId);
11539
12087
  if (idx !== -1) return idx;
11540
12088
  }
11541
- return rows.length > 1 ? 1 : 0;
12089
+ const firstOrdinary = rows.findIndex((r) => r.kind === "session" && !r.isMain);
12090
+ return firstOrdinary !== -1 ? firstOrdinary : newIdx;
11542
12091
  }, [rows, focusedSessionId]);
11543
12092
  const lastScrolledCursorRef = useRef(-1);
11544
12093
  useEffect(() => {
@@ -11645,7 +12194,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
11645
12194
  const OVERLAY_RESERVED = 6;
11646
12195
  const TITLE_LEN = 8;
11647
12196
  const SEP_LEN = 3;
11648
- const allProjectsLen = showAllProjects ? 15 : 0;
12197
+ const allProjectsLen = showAllProjects ? (scopeLabel ?? "all projects").length + SEP_LEN : 0;
11649
12198
  const countLen = countSegs.reduce((sum, s) => sum + s.text.length, 0);
11650
12199
  const cwdBudget = Math.max(0, termWidth - 4 - TITLE_LEN - OVERLAY_RESERVED) - countLen - SEP_LEN - allProjectsLen;
11651
12200
  if (cwdBudget < 6) return countSegs;
@@ -11657,7 +12206,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
11657
12206
  text: " · ",
11658
12207
  color: COLOR.mute
11659
12208
  }, {
11660
- text: "all projects",
12209
+ text: scopeLabel ?? "all projects",
11661
12210
  color: COLOR.accent
11662
12211
  });
11663
12212
  segs.push({
@@ -11722,6 +12271,8 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
11722
12271
  isCurrent: row.kind === "session" && row.rowId === currentId,
11723
12272
  showProject: showAllProjects,
11724
12273
  currentProjectRoot,
12274
+ presence: row.kind === "session" ? presence?.get(row.rowId) : void 0,
12275
+ spinnerFrame,
11725
12276
  onHover: focused ? () => moveCursor(idx) : void 0,
11726
12277
  onClick: focused ? () => {
11727
12278
  moveCursor(idx);
@@ -11769,7 +12320,7 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
11769
12320
  * exactly the width of the focus + current markers above — so the first
11770
12321
  * stat (the turn count) sits flush under the title's first letter.
11771
12322
  */
11772
- function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot, onClick, onHover }) {
12323
+ function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot, presence, spinnerFrame = 0, onClick, onHover }) {
11773
12324
  const COLOR = useColors();
11774
12325
  const SURFACE = useSurfaces();
11775
12326
  const STATS_INDENT = " ";
@@ -11843,10 +12394,21 @@ function SessionRow({ row, anchorId, focused, isCurrent, separator = false, show
11843
12394
  fg: currentColor,
11844
12395
  children: currentMark
11845
12396
  }),
12397
+ row.isMain && /* @__PURE__ */ jsx$1("span", {
12398
+ fg: COLOR.brand,
12399
+ children: "◆ "
12400
+ }),
11846
12401
  /* @__PURE__ */ jsx$1("span", {
11847
- fg: titleColor,
12402
+ fg: row.isMain ? COLOR.brand : titleColor,
11848
12403
  children: meta.title
11849
- })
12404
+ }),
12405
+ presence && (() => {
12406
+ const glyph = presence.attention ? spinnerFrame % 2 === 0 ? "◉" : "◎" : presence.running ? SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length] : presence.windows > 0 ? "○" : " ";
12407
+ return /* @__PURE__ */ jsx$1("span", {
12408
+ fg: presence.attention ? COLOR.warn : presence.running ? COLOR.brand : COLOR.accent,
12409
+ children: ` ${glyph}`
12410
+ });
12411
+ })()
11850
12412
  ]
11851
12413
  }),
11852
12414
  /* @__PURE__ */ jsxs("text", {
@@ -12024,7 +12586,7 @@ const EMPTY_QUEUED_MESSAGES = [];
12024
12586
  function shouldRenderTranscriptSearch(searchMode, query) {
12025
12587
  return searchMode && query.trim().length > 0;
12026
12588
  }
12027
- function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectedTurnId, onSelectTurn, onOpenTurn, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding }) {
12589
+ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectedTurnId, onSelectTurn, onOpenTurn, onOpenSessionEvent, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, onDraftChange, remoteDraft, onDraftAttachmentsChange, remoteDraftAttachments, remoteSearchQuery, remoteSuggest, resolveGhostRemote }) {
12028
12590
  const COLOR = useColors();
12029
12591
  const SURFACE = useSurfaces();
12030
12592
  const titleText = session?.title ?? "New session";
@@ -12119,6 +12681,7 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
12119
12681
  selectedTurnId: selectedTurnId ?? null,
12120
12682
  onSelectTurn,
12121
12683
  onOpenTurn,
12684
+ onOpenSessionEvent,
12122
12685
  searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
12123
12686
  searchPending: transcriptSearchSearching,
12124
12687
  busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
@@ -12195,7 +12758,14 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
12195
12758
  onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
12196
12759
  attachImageBinding,
12197
12760
  editorBinding,
12198
- suspended: approvalPending != null || pendingInteraction != null
12761
+ suspended: approvalPending != null || pendingInteraction != null,
12762
+ onDraftChange,
12763
+ remoteDraft,
12764
+ onDraftAttachmentsChange,
12765
+ remoteDraftAttachments,
12766
+ remoteSearchQuery,
12767
+ remoteSuggest,
12768
+ resolveGhostRemote
12199
12769
  })
12200
12770
  ]
12201
12771
  })
@@ -12542,7 +13112,7 @@ const INLINE_GHOST_DEBOUNCE_MS = 120;
12542
13112
  function isTranscriptSearchExitKey(event, cursorOffset) {
12543
13113
  return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
12544
13114
  }
12545
- function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding, clipboardReader = readClipboardContent, suspended = false }) {
13115
+ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding, onDraftChange, remoteDraft, onDraftAttachmentsChange, remoteDraftAttachments, remoteSearchQuery, remoteSuggest, resolveGhostRemote, clipboardReader = readClipboardContent, suspended = false }) {
12546
13116
  const focused = useModalAwareFocus();
12547
13117
  const renderer = useRenderer();
12548
13118
  const COLOR = useColors();
@@ -12614,15 +13184,37 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12614
13184
  }, [popupOpen, onPopupOpenChange]);
12615
13185
  const chipStyle = useChipStyle();
12616
13186
  useChipHighlights(textareaRef, completion.references, chipStyle);
13187
+ const onDraftChangeRef = useRef(onDraftChange);
13188
+ onDraftChangeRef.current = onDraftChange;
13189
+ const applyingRemoteDraftRef = useRef(false);
12617
13190
  const [shellMode, setShellMode] = useState(false);
12618
13191
  const shellModeRef = useRef(false);
12619
13192
  const searchModeRef = useRef(searchMode);
12620
13193
  const previousSearchModeRef = useRef(searchMode);
12621
13194
  const preSearchTextRef = useRef(null);
13195
+ /**
13196
+ * Shell flag parked alongside `preSearchTextRef`: a remote draft that
13197
+ * arrives WHILE searching carries its mode out-of-band — dropping it
13198
+ * would restore the text as a normal prompt after search exit while
13199
+ * every other window shows shell mode. `null` = keep the current mode
13200
+ * (the local park path, where shell mode was never touched).
13201
+ */
13202
+ const preSearchShellRef = useRef(null);
12622
13203
  /** Last text pushed into bufferState — distinguishes typed sentinels from bulk sets. */
12623
13204
  const lastSyncedTextRef = useRef("");
12624
13205
  searchModeRef.current = searchMode;
12625
13206
  shellModeRef.current = shellMode;
13207
+ /**
13208
+ * Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
13209
+ * that mutate the buffer and re-run `syncBuffer` in the same tick
13210
+ * (history cycling) must not race the render-time ref assignment.
13211
+ */
13212
+ const setShellModeNow = useCallback((on) => {
13213
+ const changed = shellModeRef.current !== on;
13214
+ shellModeRef.current = on;
13215
+ setShellMode(on);
13216
+ if (changed && !applyingRemoteDraftRef.current && !searchModeRef.current) onDraftChangeRef.current?.(textareaRef.current?.plainText ?? "", on);
13217
+ }, []);
12626
13218
  useEffect(() => {
12627
13219
  const textarea = textareaRef.current;
12628
13220
  if (!previousSearchModeRef.current && searchMode) {
@@ -12638,6 +13230,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12638
13230
  }
12639
13231
  } else if (previousSearchModeRef.current && !searchMode) {
12640
13232
  const restored = preSearchTextRef.current ?? "";
13233
+ if (preSearchShellRef.current !== null && preSearchShellRef.current !== shellModeRef.current) setShellModeNow(preSearchShellRef.current);
12641
13234
  textarea?.setText(restored);
12642
13235
  lastSyncedTextRef.current = restored;
12643
13236
  setBufferState({
@@ -12645,18 +13238,10 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12645
13238
  cursor: restored.length
12646
13239
  });
12647
13240
  preSearchTextRef.current = null;
13241
+ preSearchShellRef.current = null;
12648
13242
  }
12649
13243
  previousSearchModeRef.current = searchMode;
12650
13244
  }, [searchMode, onSearchQueryChange]);
12651
- /**
12652
- * Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
12653
- * that mutate the buffer and re-run `syncBuffer` in the same tick
12654
- * (history cycling) must not race the render-time ref assignment.
12655
- */
12656
- const setShellModeNow = useCallback((on) => {
12657
- shellModeRef.current = on;
12658
- setShellMode(on);
12659
- }, []);
12660
13245
  const exitSearchMode = useCallback(() => {
12661
13246
  searchModeRef.current = false;
12662
13247
  onSearchQueryChange?.("");
@@ -12694,6 +13279,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12694
13279
  text: ta.plainText,
12695
13280
  cursor: ta.cursorOffset
12696
13281
  });
13282
+ if (!applyingRemoteDraftRef.current && !searchModeRef.current) onDraftChangeRef.current?.(ta.plainText, shellModeRef.current);
12697
13283
  setGhost((prev) => patchInlineGhost(prev, ta.plainText, ta.cursorOffset));
12698
13284
  const total = ta.editorView.getTotalVirtualLineCount();
12699
13285
  setContentLines(Math.max(MIN_CONTENT_LINES, total));
@@ -12709,14 +13295,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12709
13295
  const controller = new AbortController();
12710
13296
  const { text, cursor } = bufferState;
12711
13297
  const timer = setTimeout(() => {
12712
- resolveInlineSuggestion(inlineProviders, {
13298
+ const local = () => resolveInlineSuggestion(inlineProviders, {
12713
13299
  text,
12714
13300
  cursor,
12715
13301
  cwd: process.cwd(),
12716
13302
  signal: controller.signal
12717
- }).then((res) => {
13303
+ }).then((res) => res?.suffixes ?? []);
13304
+ (resolveGhostRemote ? resolveGhostRemote(text, cursor).then((res) => res === null ? local() : res.candidates?.length ? res.candidates : res.suggestion !== null ? [res.suggestion] : []).catch(() => local()) : local()).then((raw) => {
12718
13305
  if (controller.signal.aborted) return;
12719
- const suffixes = (res?.suffixes ?? []).map((s) => s.split("\n")[0]).filter((s) => s.length > 0);
13306
+ const suffixes = raw.map((s) => s.split("\n")[0]).filter((s) => s.length > 0);
12720
13307
  setGhost(suffixes.length > 0 ? {
12721
13308
  suffixes,
12722
13309
  index: 0,
@@ -12736,7 +13323,8 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12736
13323
  selectMode,
12737
13324
  shellMode,
12738
13325
  searchMode,
12739
- inlineProviders
13326
+ inlineProviders,
13327
+ resolveGhostRemote
12740
13328
  ]);
12741
13329
  /**
12742
13330
  * Open the current prompt in `$EDITOR` (git-style round-trip). The
@@ -12805,6 +13393,77 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12805
13393
  syncBuffer();
12806
13394
  return true;
12807
13395
  }, [completion, syncBuffer]);
13396
+ useEffect(() => {
13397
+ if (!remoteDraft) return;
13398
+ if (searchModeRef.current) {
13399
+ preSearchTextRef.current = remoteDraft.text;
13400
+ preSearchShellRef.current = remoteDraft.shell === true;
13401
+ return;
13402
+ }
13403
+ const ta = textareaRef.current;
13404
+ if (!ta) return;
13405
+ const remoteShell = remoteDraft.shell === true;
13406
+ if (ta.plainText === remoteDraft.text && shellModeRef.current === remoteShell) return;
13407
+ applyingRemoteDraftRef.current = true;
13408
+ try {
13409
+ if (shellModeRef.current !== remoteShell) setShellModeNow(remoteShell);
13410
+ ta.setText(remoteDraft.text);
13411
+ ta.cursorOffset = remoteDraft.text.length;
13412
+ syncBuffer();
13413
+ } finally {
13414
+ applyingRemoteDraftRef.current = false;
13415
+ }
13416
+ }, [remoteDraft?.seq]);
13417
+ useEffect(() => {
13418
+ if (!remoteSearchQuery || !searchModeRef.current) return;
13419
+ const ta = textareaRef.current;
13420
+ if (!ta || ta.plainText === remoteSearchQuery.query) return;
13421
+ applyingRemoteDraftRef.current = true;
13422
+ try {
13423
+ ta.setText(remoteSearchQuery.query);
13424
+ ta.cursorOffset = remoteSearchQuery.query.length;
13425
+ syncBuffer();
13426
+ } finally {
13427
+ applyingRemoteDraftRef.current = false;
13428
+ }
13429
+ }, [remoteSearchQuery?.seq]);
13430
+ useEffect(() => {
13431
+ if (!remoteSuggest) return;
13432
+ if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || searchMode) return;
13433
+ const ta = textareaRef.current;
13434
+ if (!ta || ta.plainText !== remoteSuggest.basis) return;
13435
+ if (remoteSuggest.suggestion === null) {
13436
+ setGhost(null);
13437
+ return;
13438
+ }
13439
+ const suffixes = (remoteSuggest.candidates?.length ? remoteSuggest.candidates : [remoteSuggest.suggestion]).map((s) => s.split("\n")[0]).filter((s) => s.length > 0);
13440
+ setGhost(suffixes.length > 0 ? {
13441
+ suffixes,
13442
+ index: 0,
13443
+ text: remoteSuggest.basis,
13444
+ cursor: remoteSuggest.caret
13445
+ } : null);
13446
+ }, [remoteSuggest?.seq]);
13447
+ const onDraftAttachmentsChangeRef = useRef(onDraftAttachmentsChange);
13448
+ onDraftAttachmentsChangeRef.current = onDraftAttachmentsChange;
13449
+ const applyingRemoteAttachmentsRef = useRef(false);
13450
+ const attachmentsInitRef = useRef(true);
13451
+ useEffect(() => {
13452
+ if (attachmentsInitRef.current) {
13453
+ attachmentsInitRef.current = false;
13454
+ return;
13455
+ }
13456
+ if (applyingRemoteAttachmentsRef.current) {
13457
+ applyingRemoteAttachmentsRef.current = false;
13458
+ return;
13459
+ }
13460
+ onDraftAttachmentsChangeRef.current?.(attachments);
13461
+ }, [attachments]);
13462
+ useEffect(() => {
13463
+ if (!remoteDraftAttachments) return;
13464
+ applyingRemoteAttachmentsRef.current = true;
13465
+ setAttachments(remoteDraftAttachments.attachments);
13466
+ }, [remoteDraftAttachments?.seq]);
12808
13467
  /** Apply decoded clipboard text through the shared file/attachment/inline policy. */
12809
13468
  const applyTextPaste = useCallback((text) => {
12810
13469
  const ta = textareaRef.current;
@@ -14043,102 +14702,30 @@ function statusColor(status, COLOR) {
14043
14702
  }
14044
14703
  }
14045
14704
  //#endregion
14046
- //#region src/chat/auth-rows.ts
14705
+ //#region src/chat/contribution-origin.ts
14047
14706
  /**
14048
- * Stored credentials, i.e. the ones a user can actually choose between.
14707
+ * Union the three skill sources into the list the Skills tab shows, in the
14708
+ * precedence the AGENT applies — so the tab can never advertise a skill the
14709
+ * agent dropped, or attribute a name to the copy that lost.
14049
14710
  *
14050
- * `env` is excluded: it is observed rather than stored, and is usually just the
14051
- * mirror `applyApiKeyEnv` wrote from the stored API key, so counting it would
14052
- * advertise a choice that does not exist.
14711
+ * Precedence is extension > plugin > disk, and it is not arbitrary:
14712
+ * `resolveSkills` seeds its map in catalog order with later entries
14713
+ * overwriting earlier, `app.tsx` concatenates plugin skills BEFORE extension
14714
+ * ones, and catalog entries shadow anything discovered on disk. The reasoning
14715
+ * for extension-over-plugin lives at that concat: extension code is something
14716
+ * the user installed deliberately, whereas a project-scope plugin arrives with
14717
+ * a repo clone, so a cloned repo must not shadow a user's extension skill by
14718
+ * reusing its name.
14053
14719
  */
14054
- function selectableSources(provider) {
14055
- return provider.methods.filter((m) => m.source !== "env");
14056
- }
14057
- /** A provider earns a chevron only when there is a real choice to reveal. */
14058
- function isExpandableProvider(provider) {
14059
- return selectableSources(provider).length > 1;
14720
+ function mergeSkillCatalog(opts) {
14721
+ const byName = /* @__PURE__ */ new Map();
14722
+ for (const skill of opts.disk ?? []) byName.set(skill.name, skill);
14723
+ for (const skill of opts.plugin ?? []) byName.set(skill.name, skill);
14724
+ for (const skill of opts.extension ?? []) byName.set(skill.name, skill);
14725
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
14060
14726
  }
14061
14727
  /**
14062
- * Flatten the provider list plus the expanded set into the visible-row list.
14063
- *
14064
- * - `providers` — filtered / ordered provider list (post-search).
14065
- * - `expanded` — keys of providers whose sources are open. Keys for
14066
- * providers that are no longer expandable are ignored rather
14067
- * than cleaned up, so stale expansion state (a credential
14068
- * removed while the modal is open) can't desync the cursor.
14069
- */
14070
- function buildVisibleAuthRows(providers, expanded) {
14071
- const out = [];
14072
- for (const provider of providers) {
14073
- const expandable = isExpandableProvider(provider);
14074
- out.push({
14075
- kind: "provider",
14076
- provider,
14077
- expandable
14078
- });
14079
- if (!expandable || !expanded.has(provider.key)) continue;
14080
- for (const method of selectableSources(provider)) out.push({
14081
- kind: "source",
14082
- providerKey: provider.key,
14083
- method
14084
- });
14085
- if (provider.envKey) {
14086
- const envMethod = provider.methods.find((m) => m.source === "env");
14087
- out.push({
14088
- kind: "source",
14089
- providerKey: provider.key,
14090
- method: envMethod ?? {
14091
- source: "env",
14092
- detail: provider.envKey,
14093
- active: false
14094
- }
14095
- });
14096
- }
14097
- }
14098
- return out;
14099
- }
14100
- /**
14101
- * Provider key a row "belongs to" — its own for a provider header, the parent's
14102
- * for a source row.
14103
- */
14104
- function parentProviderKey(row) {
14105
- return row.kind === "provider" ? row.provider.key : row.providerKey;
14106
- }
14107
- /**
14108
- * Index in `rows` of the header for the given provider key, or `-1`.
14109
- *
14110
- * Collapse handlers snap the cursor up to the parent BEFORE the row list
14111
- * shrinks; without that the cursor keeps its flat index and silently lands on
14112
- * whichever sibling row slid into that slot.
14113
- */
14114
- function indexOfProviderRow(rows, providerKey) {
14115
- return rows.findIndex((r) => r.kind === "provider" && r.provider.key === providerKey);
14116
- }
14117
- //#endregion
14118
- //#region src/chat/contribution-origin.ts
14119
- /**
14120
- * Union the three skill sources into the list the Skills tab shows, in the
14121
- * precedence the AGENT applies — so the tab can never advertise a skill the
14122
- * agent dropped, or attribute a name to the copy that lost.
14123
- *
14124
- * Precedence is extension > plugin > disk, and it is not arbitrary:
14125
- * `resolveSkills` seeds its map in catalog order with later entries
14126
- * overwriting earlier, `app.tsx` concatenates plugin skills BEFORE extension
14127
- * ones, and catalog entries shadow anything discovered on disk. The reasoning
14128
- * for extension-over-plugin lives at that concat: extension code is something
14129
- * the user installed deliberately, whereas a project-scope plugin arrives with
14130
- * a repo clone, so a cloned repo must not shadow a user's extension skill by
14131
- * reusing its name.
14132
- */
14133
- function mergeSkillCatalog(opts) {
14134
- const byName = /* @__PURE__ */ new Map();
14135
- for (const skill of opts.disk ?? []) byName.set(skill.name, skill);
14136
- for (const skill of opts.plugin ?? []) byName.set(skill.name, skill);
14137
- for (const skill of opts.extension ?? []) byName.set(skill.name, skill);
14138
- return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
14139
- }
14140
- /**
14141
- * Badge for a skill row, read straight off the skill's own metadata.
14728
+ * Badge for a skill row, read straight off the skill's own metadata.
14142
14729
  *
14143
14730
  * `undefined` for a skill discovered on disk — no badge means "this came from a
14144
14731
  * directory you control", which is what makes a badge mean "something
@@ -14418,88 +15005,6 @@ function parentExtensionName(row) {
14418
15005
  function indexOfExtensionRow(rows, extName) {
14419
15006
  return rows.findIndex((r) => r.kind === "extension" && r.ext.name === extName);
14420
15007
  }
14421
- /**
14422
- * Filter segments by the active query and resolve each one's expanded state.
14423
- *
14424
- * Deliberately does NOT auto-expand matches the way the Extensions tab does:
14425
- * the search corpus includes each segment's full text, so a query typically
14426
- * matches deep inside a long document — auto-expanding every hit would dump
14427
- * thousands of lines into the viewport on a keystroke. The user expands the row
14428
- * they care about.
14429
- */
14430
- function buildVisibleInstructionRows(opts) {
14431
- const { segments, expanded, queryActive, matchSegment } = opts;
14432
- const out = [];
14433
- for (const segment of segments) {
14434
- if (queryActive && !matchSegment(segment)) continue;
14435
- out.push({
14436
- segment,
14437
- expanded: expanded.has(segment.id)
14438
- });
14439
- }
14440
- return out;
14441
- }
14442
- /**
14443
- * Split a segment's text into renderable preview lines, clamped to
14444
- * {@link INSTRUCTION_PREVIEW_LINE_CAP}.
14445
- *
14446
- * `hidden` is how many lines were dropped off the tail, so the view can render
14447
- * an honest "+N more lines" marker instead of silently truncating. Tabs are
14448
- * expanded to two spaces because the terminal text renderable doesn't
14449
- * interpret `\t`, and `\r` is stripped so CRLF files don't leave artefacts at
14450
- * every line end.
14451
- */
14452
- function instructionPreviewLines(text) {
14453
- const all = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
14454
- while (all.length > 0 && all[all.length - 1].trim() === "") all.pop();
14455
- if (all.length <= 400) return {
14456
- lines: all,
14457
- hidden: 0
14458
- };
14459
- return {
14460
- lines: all.slice(0, 400),
14461
- hidden: all.length - 400
14462
- };
14463
- }
14464
- /**
14465
- * Short badge for a segment's origin — the "where does this come from" column.
14466
- * `'file'` is intentionally the only one that reads like a location, since it's
14467
- * the only origin the user can act on.
14468
- */
14469
- function instructionOriginBadge(segment) {
14470
- switch (segment.origin) {
14471
- case "file": return "file";
14472
- case "generated": return "generated";
14473
- case "mcp": return "mcp server";
14474
- case "built-in": return "built-in";
14475
- }
14476
- }
14477
- //#endregion
14478
- //#region src/tui/logs-view.ts
14479
- /** Level order for the tab's min-level filter (matches the logger's ordering). */
14480
- const LEVEL_ORDER = {
14481
- debug: 0,
14482
- info: 1,
14483
- warn: 2,
14484
- error: 3
14485
- };
14486
- /** The cycle order the `l` key walks through — coarsest last so it wraps back to "everything". */
14487
- const LOG_LEVEL_CYCLE = [
14488
- "debug",
14489
- "info",
14490
- "warn",
14491
- "error"
14492
- ];
14493
- /** Next min-level in {@link LOG_LEVEL_CYCLE}, wrapping around. */
14494
- function nextLogLevel(level) {
14495
- const i = LOG_LEVEL_CYCLE.indexOf(level);
14496
- return LOG_LEVEL_CYCLE[(i + 1) % LOG_LEVEL_CYCLE.length];
14497
- }
14498
- /** Retain only records at or above `minLevel`. Preserves order. */
14499
- function filterLogRecords(records, minLevel) {
14500
- const min = LEVEL_ORDER[minLevel];
14501
- return records.filter((r) => LEVEL_ORDER[r.level] >= min);
14502
- }
14503
15008
  //#endregion
14504
15009
  //#region src/tui/settings-form-dialog.tsx
14505
15010
  /** Shared dialog width — pickers use it to size their own ellipsed rows. */
@@ -14943,132 +15448,6 @@ function clipEnd(text, max, nameLen) {
14943
15448
  rest: clipped.slice(Math.min(nameLen, clipped.length))
14944
15449
  };
14945
15450
  }
14946
- const MCP_TRANSPORTS = [
14947
- "stdio",
14948
- "streamable-http",
14949
- "sse"
14950
- ];
14951
- /**
14952
- * Validate + normalize the raw field values into a config entry.
14953
- * Returns the first problem in row order so the dialog can surface it.
14954
- */
14955
- function validateMcpForm(values) {
14956
- const name = values.name.trim();
14957
- if (name.length === 0) return {
14958
- ok: false,
14959
- error: "name is required"
14960
- };
14961
- if (!/^[\w.-]+$/.test(name)) return {
14962
- ok: false,
14963
- error: "name must be alphanumerics, dots, hyphens, underscores"
14964
- };
14965
- if (values.transport === "stdio") {
14966
- const tokens = splitCommandLine(values.commandLine);
14967
- if (tokens.length === 0) return {
14968
- ok: false,
14969
- error: "command is required for stdio servers"
14970
- };
14971
- const env = parseKeyValueLines(values.env, {
14972
- label: "env",
14973
- separators: ["="]
14974
- });
14975
- if (!env.ok) return {
14976
- ok: false,
14977
- error: env.error
14978
- };
14979
- return {
14980
- ok: true,
14981
- result: {
14982
- entry: {
14983
- name,
14984
- transport: "stdio",
14985
- command: tokens[0],
14986
- ...tokens.length > 1 ? { args: tokens.slice(1) } : {},
14987
- ...Object.keys(env.pairs).length > 0 ? { env: env.pairs } : {}
14988
- },
14989
- scope: values.scope
14990
- }
14991
- };
14992
- }
14993
- const url = values.url.trim();
14994
- if (url.length === 0) return {
14995
- ok: false,
14996
- error: "url is required for http/sse servers"
14997
- };
14998
- if (!/^https?:\/\//.test(url)) return {
14999
- ok: false,
15000
- error: "url must start with http:// or https://"
15001
- };
15002
- const headers = parseKeyValueLines(values.headers, {
15003
- label: "headers",
15004
- separators: [":", "="]
15005
- });
15006
- if (!headers.ok) return {
15007
- ok: false,
15008
- error: headers.error
15009
- };
15010
- return {
15011
- ok: true,
15012
- result: {
15013
- entry: {
15014
- name,
15015
- transport: values.transport,
15016
- url,
15017
- ...Object.keys(headers.pairs).length > 0 ? { headers: headers.pairs } : {}
15018
- },
15019
- scope: values.scope
15020
- }
15021
- };
15022
- }
15023
- /**
15024
- * Split a command line into tokens, honoring single/double quotes so
15025
- * `npx -y my-server --root "/path with spaces"` round-trips. No escape
15026
- * processing beyond quote grouping — this is a form field, not a shell.
15027
- */
15028
- function splitCommandLine(line) {
15029
- const out = [];
15030
- const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
15031
- for (let m = re.exec(line); m !== null; m = re.exec(line)) out.push(m[1] ?? m[2] ?? m[3]);
15032
- return out;
15033
- }
15034
- /**
15035
- * Parse one key/value pair PER LINE. Line-based (not whitespace-split)
15036
- * because values legitimately contain spaces — `Authorization: Bearer
15037
- * abc123` is the canonical headers case, and env values like
15038
- * `GREETING=hello world` are equally valid. The first separator found
15039
- * (in `separators` priority order, `:` before `=` for headers) splits;
15040
- * the rest of the line is the value, trimmed. Blank lines skipped.
15041
- */
15042
- function parseKeyValueLines(raw, opts) {
15043
- const pairs = {};
15044
- for (const line of raw.split("\n")) {
15045
- const trimmed = line.trim();
15046
- if (trimmed.length === 0) continue;
15047
- let splitAt = -1;
15048
- for (const sep of opts.separators) {
15049
- const idx = trimmed.indexOf(sep);
15050
- if (idx > 0 && (splitAt === -1 || idx < splitAt)) splitAt = idx;
15051
- }
15052
- if (splitAt <= 0) {
15053
- const forms = opts.separators.map((s) => `KEY${s}VALUE`).join(" or ");
15054
- return {
15055
- ok: false,
15056
- error: `${opts.label} entries must be ${forms}, one per line (got "${trimmed}")`
15057
- };
15058
- }
15059
- const key = trimmed.slice(0, splitAt).trim();
15060
- const value = trimmed.slice(splitAt + 1).trim();
15061
- if (key.length === 0) return {
15062
- ok: false,
15063
- error: `${opts.label} entry has an empty key ("${trimmed}")`
15064
- };
15065
- pairs[key] = value;
15066
- }
15067
- return {
15068
- ok: true,
15069
- pairs
15070
- };
15071
- }
15072
15451
  function McpAddDialog({ active, existingNames, onSubmit, onCancel }) {
15073
15452
  const COLOR = useColors();
15074
15453
  const [transport, setTransport] = useState("stdio");
@@ -16210,8 +16589,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16210
16589
  if (isCategoryTab(activeTab)) {
16211
16590
  const it = filteredByCategory[activeTab][index];
16212
16591
  if (!it) return;
16213
- if (it.kind === "toggle") toggleBoolean(it.key);
16214
- else if (it.kind === "choice") {
16592
+ if (it.kind === "toggle") {
16593
+ const requirement = it.requires ? SETTINGS_TOGGLES.find((t) => t.key === it.requires) : void 0;
16594
+ if (!requirement || settings[requirement.key]) toggleBoolean(it.key);
16595
+ } else if (it.kind === "choice") {
16215
16596
  const current = settings[it.key];
16216
16597
  const idx = it.options.findIndex((o) => o.value === current);
16217
16598
  const next = it.options[(idx + 1) % it.options.length];
@@ -17088,15 +17469,20 @@ function GeneralList({ items, cursor, highlightBg, query, contentCols, editingKe
17088
17469
  const focused = i === cursor;
17089
17470
  const bg = focused ? highlightBg : void 0;
17090
17471
  const id = anchorIdFor(i);
17091
- if (item.kind === "toggle") return /* @__PURE__ */ jsx$1(ToggleRow, {
17092
- onClick: (zone) => onRowClick?.(i, zone),
17093
- id,
17094
- label: item.label,
17095
- description: truncateToCols(item.description, descCols),
17096
- enabled: settings[item.key],
17097
- focused,
17098
- bg
17099
- }, item.key);
17472
+ if (item.kind === "toggle") {
17473
+ const requirement = item.requires ? SETTINGS_TOGGLES.find((t) => t.key === item.requires) : void 0;
17474
+ const blockedBy = requirement && !settings[requirement.key] ? requirement.label : void 0;
17475
+ return /* @__PURE__ */ jsx$1(ToggleRow, {
17476
+ onClick: (zone) => onRowClick?.(i, zone),
17477
+ id,
17478
+ label: item.label,
17479
+ description: truncateToCols(item.description, descCols),
17480
+ enabled: settings[item.key],
17481
+ focused,
17482
+ bg,
17483
+ ...blockedBy !== void 0 ? { blockedBy } : {}
17484
+ }, item.key);
17485
+ }
17100
17486
  if (item.kind === "choice") {
17101
17487
  const current = settings[item.key];
17102
17488
  const opt = item.options.find((o) => o.value === current);
@@ -18411,8 +18797,9 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
18411
18797
  ]
18412
18798
  });
18413
18799
  }
18414
- function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, onClick }) {
18800
+ function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, onClick, blockedBy }) {
18415
18801
  const COLOR = useColors();
18802
+ const blocked = blockedBy !== void 0;
18416
18803
  return /* @__PURE__ */ jsxs("box", {
18417
18804
  onMouseDown: onClick && ((event) => {
18418
18805
  event.stopPropagation();
@@ -18432,7 +18819,7 @@ function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, o
18432
18819
  onMouseDown: onClick && ((event) => {
18433
18820
  event.stopPropagation();
18434
18821
  event.preventDefault();
18435
- onClick(inCheckboxZone(event, 2) ? "checkbox" : "row");
18822
+ onClick(!blocked && inCheckboxZone(event, 2) ? "checkbox" : "row");
18436
18823
  }),
18437
18824
  children: [
18438
18825
  /* @__PURE__ */ jsx$1("span", {
@@ -18440,18 +18827,18 @@ function ToggleRow({ id, label, description, enabled, focused, bg, indent = 0, o
18440
18827
  children: focused ? "▶ " : " "
18441
18828
  }),
18442
18829
  /* @__PURE__ */ jsx$1("span", {
18443
- fg: enabled ? COLOR.accent : COLOR.mute,
18444
- children: enabled ? "[✓] " : "[ ] "
18830
+ fg: blocked ? COLOR.mute : enabled ? COLOR.accent : COLOR.mute,
18831
+ children: blocked ? "[–] " : enabled ? "[✓] " : "[ ] "
18445
18832
  }),
18446
18833
  /* @__PURE__ */ jsx$1("span", {
18447
- fg: focused ? COLOR.brand : COLOR.dim,
18834
+ fg: blocked ? COLOR.mute : focused ? COLOR.brand : COLOR.dim,
18448
18835
  children: label
18449
18836
  })
18450
18837
  ]
18451
18838
  }), /* @__PURE__ */ jsx$1("text", {
18452
18839
  wrapMode: "none",
18453
18840
  fg: COLOR.mute,
18454
- children: `${COL_TITLE}${description}`
18841
+ children: `${COL_TITLE}${blocked ? `requires “${blockedBy}” — turn that on first` : description}`
18455
18842
  })]
18456
18843
  });
18457
18844
  }
@@ -19020,109 +19407,7 @@ function restoreTerminalTitle(out = process.stdout) {
19020
19407
  } catch {}
19021
19408
  }
19022
19409
  //#endregion
19023
- //#region src/chat/todo-edits.ts
19024
- /**
19025
- * Prefix for ids minted here, distinguishing user-authored rows from the
19026
- * model's own ids in a session file. Purely diagnostic — nothing branches on
19027
- * it — but it makes "who added this" answerable after the fact.
19028
- */
19029
- const USER_TODO_ID_PREFIX = "user-";
19030
- /**
19031
- * Mint an id that cannot collide with an existing row.
19032
- *
19033
- * Counter-based rather than random: a stable, readable id keeps session diffs
19034
- * legible, and the collision check makes it safe against ids the model already
19035
- * used (including previous `user-*` ones after a reload).
19036
- */
19037
- function nextTodoId(items) {
19038
- const taken = new Set(items.map((i) => i.id));
19039
- for (let n = 1;; n++) {
19040
- const id = `${USER_TODO_ID_PREFIX}${n}`;
19041
- if (!taken.has(id)) return id;
19042
- }
19043
- }
19044
- /**
19045
- * Append a todo. Returns the list unchanged when `content` is blank — an empty
19046
- * row is never what the user meant, and rejecting it here means every caller
19047
- * (keyboard, click, future GUI) gets the same guard.
19048
- */
19049
- function addTodo(items, content) {
19050
- const trimmed = content.trim();
19051
- if (!trimmed) return [...items];
19052
- return [...items, {
19053
- id: nextTodoId(items),
19054
- content: trimmed,
19055
- status: "pending"
19056
- }];
19057
- }
19058
- /**
19059
- * Replace one todo's text. Blank content is rejected (see {@link addTodo}); an
19060
- * unknown id is a no-op rather than an error, because the list can be replaced
19061
- * by a `todowrite` between render and keypress.
19062
- */
19063
- function editTodo(items, id, content) {
19064
- const trimmed = content.trim();
19065
- if (!trimmed) return [...items];
19066
- return items.map((item) => item.id === id ? {
19067
- ...item,
19068
- content: trimmed
19069
- } : item);
19070
- }
19071
- /** Drop one todo. Unknown id is a no-op, same rationale as {@link editTodo}. */
19072
- function removeTodo(items, id) {
19073
- return items.filter((item) => item.id !== id);
19074
- }
19075
- /** Drop everything. Separate from `removeTodo` so the caller's intent is explicit at the call site. */
19076
- function clearTodos() {
19077
- return [];
19078
- }
19079
- /**
19080
- * Advance a todo's status one step: pending → in_progress → completed →
19081
- * pending. `cancelled` also lands on `pending`, so a dropped item can be
19082
- * revived without a dedicated key.
19083
- *
19084
- * Cycling INTO `in_progress` demotes any other in-progress row to `pending`:
19085
- * the list's whole purpose is "what is being worked on right now", and two
19086
- * live rows make that unanswerable — `TodoIndicator`'s successor
19087
- * (`todoProgressLabel`) reads the first one and would silently ignore the rest.
19088
- */
19089
- function cycleTodoStatus(items, id) {
19090
- const target = items.find((item) => item.id === id);
19091
- if (!target) return [...items];
19092
- const next = nextStatus(target.status);
19093
- return items.map((item) => {
19094
- if (item.id === id) return {
19095
- ...item,
19096
- status: next
19097
- };
19098
- if (next === "in_progress" && item.status === "in_progress") return {
19099
- ...item,
19100
- status: "pending"
19101
- };
19102
- return item;
19103
- });
19104
- }
19105
- function nextStatus(status) {
19106
- switch (status) {
19107
- case "pending": return "in_progress";
19108
- case "in_progress": return "completed";
19109
- default: return "pending";
19110
- }
19111
- }
19112
- //#endregion
19113
19410
  //#region src/tui/todos-modal.tsx
19114
- /**
19115
- * Steering text for a "retry this todo" request.
19116
- *
19117
- * Quotes the content so a model reading it can match the row verbatim against
19118
- * its own list, and names the status because "retry" means different things
19119
- * for a row it believes is `completed` versus one it abandoned as `cancelled`.
19120
- * Pure + exported so the wording is pinned by a test rather than living only
19121
- * inside a JSX callback.
19122
- */
19123
- function buildTodoRetryMessage(item) {
19124
- return `Retry this todo (currently ${item.status}): "${item.content}"`;
19125
- }
19126
19411
  /** Floor on the modal height — keeps the header + at least a few rows visible on tiny terminals. */
19127
19412
  const MIN_MODAL_HEIGHT = 12;
19128
19413
  /** Ceiling on the modal height — prevents a giant list from blanket-filling a tall window. */
@@ -19197,7 +19482,7 @@ function TodoRow({ item, rowId, focused, onClick }) {
19197
19482
  })
19198
19483
  });
19199
19484
  }
19200
- function TodosModal({ session, agent, onSteer }) {
19485
+ function TodosModal({ getSession, subscribe, onCommit, onSteer }) {
19201
19486
  const COLOR = useColors();
19202
19487
  const modal = useModal();
19203
19488
  const { height: termHeight } = useTerminalDimensions();
@@ -19205,12 +19490,8 @@ function TodosModal({ session, agent, onSteer }) {
19205
19490
  const [cursorRaw, setCursorRaw] = useState(0);
19206
19491
  const [mode, setMode] = useState("list");
19207
19492
  const [draft, setDraft] = useState("");
19208
- useEffect(() => {
19209
- if (!agent) return;
19210
- return agent.hooks.hook("tool:after", (ctx) => {
19211
- if (ctx.name === "todowrite") setTick((t) => t + 1);
19212
- });
19213
- }, [agent]);
19493
+ useEffect(() => subscribe(() => setTick((t) => t + 1)), [subscribe]);
19494
+ const session = getSession();
19214
19495
  const state = useActiveTodos(session);
19215
19496
  const scrollRef = useRef(null);
19216
19497
  const inProgressId = state.inProgress?.id ?? null;
@@ -19233,14 +19514,13 @@ function TodosModal({ session, agent, onSteer }) {
19233
19514
  * Write a transformed list back to the run.
19234
19515
  *
19235
19516
  * Editing while the ARCHIVE is on screen promotes it back to the live slot,
19236
- * which is the only coherent option: `setTodosForRun` owns the live slot, and
19237
- * an edit the user just made must be the list that's displayed next.
19517
+ * which is the only coherent option: the live slot is what the model reads,
19518
+ * and an edit the user just made must be the list that's displayed next.
19238
19519
  */
19239
19520
  const commit = useCallback((next) => {
19240
- if (!session || !state.runId) return;
19241
- setTodosForRun(session, state.runId, next, { clearArchive: true });
19521
+ onCommit(next);
19242
19522
  setTick((t) => t + 1);
19243
- }, [session, state.runId]);
19523
+ }, [onCommit]);
19244
19524
  const editable = session !== null && state.runId !== null;
19245
19525
  /** Enter add / edit mode, seeding the draft from the focused row for edits. */
19246
19526
  const beginEdit = useCallback((next) => {
@@ -19503,6 +19783,18 @@ function CountsBadge({ items }) {
19503
19783
  });
19504
19784
  }
19505
19785
  //#endregion
19786
+ //#region src/tui/tui-bus.ts
19787
+ let holder$3 = null;
19788
+ /** Initialize (or return) the process-wide extension bus. Idempotent. */
19789
+ function initTuiBus() {
19790
+ holder$3 ??= createExtensionBus();
19791
+ return holder$3;
19792
+ }
19793
+ /** The bus, or null when `runTui` hasn't wired it (bare embedders). */
19794
+ function getTuiBus() {
19795
+ return holder$3;
19796
+ }
19797
+ //#endregion
19506
19798
  //#region src/tui/tui-host-runtime.ts
19507
19799
  let holder$2 = null;
19508
19800
  /** Initialize (or return) the process-wide bridge. Idempotent. */
@@ -20130,32 +20422,10 @@ const BAR_WIDTH = 12;
20130
20422
  /** Right-hand columns, padded so percents and resets align down the panel. */
20131
20423
  const PERCENT_COL = 6;
20132
20424
  const TRAILING_COL = 16;
20133
- /** Percent thresholds, shared with the footer indicator. */
20425
+ /** Percent thresholds → palette. The thresholds themselves are shared with the GUI via `usageTone`. */
20134
20426
  function usageColor(usedPercent, COLOR) {
20135
- if (usedPercent >= 85) return COLOR.error;
20136
- if (usedPercent >= 60) return COLOR.warn;
20137
- return COLOR.brand;
20138
- }
20139
- /**
20140
- * Time to reset, coarsened to whole days past 24h. `null` when the provider
20141
- * reported no reset, `resetting` once it is due.
20142
- *
20143
- * Every step rounds DOWN, like the footer's percent: 36h reads as `1d`, not
20144
- * `2d`. Overstating the headroom left on a window is the wrong direction to
20145
- * be wrong in.
20146
- */
20147
- function resetLabel(resetsAt, now = Date.now()) {
20148
- if (resetsAt === null) return null;
20149
- const remaining = resetsAt - now;
20150
- if (remaining <= 0) return "resetting";
20151
- const minutes = Math.floor(remaining / 6e4);
20152
- if (minutes < 60) return `resets in ${Math.max(1, minutes)}m`;
20153
- const hours = Math.floor(minutes / 60);
20154
- if (hours < 24) {
20155
- const remMinutes = minutes % 60;
20156
- return `resets in ${hours}h${remMinutes > 0 ? `${remMinutes}m` : ""}`;
20157
- }
20158
- return `resets in ${Math.max(1, Math.floor(hours / 24))}d`;
20427
+ const tone = usageTone(usedPercent);
20428
+ return tone === "error" ? COLOR.error : tone === "warn" ? COLOR.warn : COLOR.brand;
20159
20429
  }
20160
20430
  function UsagePanelModal({ dataDir, registry, activeProviderKey, autoLoad = true }) {
20161
20431
  const COLOR = useColors();
@@ -20342,15 +20612,6 @@ function emptyStateLine({ loading, loaded, envOptedOut, autoLoad }) {
20342
20612
  if (!autoLoad && !loaded) return "usage checks are off · ctrl+r fetches once";
20343
20613
  return "no signed-in provider reports usage";
20344
20614
  }
20345
- /** `20.00 / 25.00`, or just the remainder when no total is reported. */
20346
- function formatBalance(credits) {
20347
- const remaining = formatCredits(credits.remaining);
20348
- return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
20349
- }
20350
- /** Balances are dollars for OpenRouter and opaque units elsewhere. */
20351
- function formatCredits(value) {
20352
- return value >= 100 ? value.toFixed(0) : value.toFixed(2);
20353
- }
20354
20615
  //#endregion
20355
20616
  //#region src/tui/app.tsx
20356
20617
  const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
@@ -20557,10 +20818,24 @@ function App({ config }) {
20557
20818
  ...DEFAULT_SETTINGS,
20558
20819
  ...config.initialSettings
20559
20820
  }), [config.initialSettings]),
20560
- onChange: useCallback((settings) => config.stateStore.save({
20561
- ...config.stateStore.load(),
20562
- settings
20563
- }), [config.stateStore]),
20821
+ onChange: useCallback((settings) => {
20822
+ if (settings.useDaemon && isDaemonConnected()) {
20823
+ const state = config.stateStore.load();
20824
+ const merged = {
20825
+ ...state.settings ?? {},
20826
+ useDaemon: settings.useDaemon
20827
+ };
20828
+ config.stateStore.save({
20829
+ ...state,
20830
+ settings: merged
20831
+ });
20832
+ return;
20833
+ }
20834
+ config.stateStore.save({
20835
+ ...config.stateStore.load(),
20836
+ settings
20837
+ });
20838
+ }, [config.stateStore]),
20564
20839
  children: /* @__PURE__ */ jsx$1(ThemedShell, {})
20565
20840
  })
20566
20841
  });
@@ -20614,7 +20889,7 @@ function AppShell() {
20614
20889
  bootTick("AppShell:first-effect (post-first-paint)");
20615
20890
  }, []);
20616
20891
  const queue = useSafeModeQueue();
20617
- const { requestApproval, resolveHead, denyAll } = useSafeModeActions();
20892
+ const { requestApproval, resolveHead, denyAll, dropByCorrelation: dropApprovalByCorrelation } = useSafeModeActions();
20618
20893
  const pendingApproval = queue[0] ?? null;
20619
20894
  const interactionsQueue = useInteractionsQueue();
20620
20895
  const interactions = useInteractionsActions();
@@ -20759,9 +21034,16 @@ function AppShell() {
20759
21034
  ]);
20760
21035
  const runCapsRef = useRef(runCaps);
20761
21036
  runCapsRef.current = runCaps;
21037
+ /**
21038
+ * When THIS window's last local run settled, per session — exempts the
21039
+ * cross-process run guard from refusing our own settle-lagged
21040
+ * `'running'` marker (mirror of the host's `lastRunEndedAt`).
21041
+ */
21042
+ const lastLocalRunEndRef = useRef(null);
20762
21043
  const lastFinishReasonRef = useRef(void 0);
20763
21044
  const providerRegistryRef = useRef(providerRegistry);
20764
21045
  providerRegistryRef.current = providerRegistry;
21046
+ const availableProvidersRef = useRef([]);
20765
21047
  /**
20766
21048
  * Loop-native `behavior.autoCompact` for a run — honors the user's
20767
21049
  * enable/threshold settings and injects the picked model's context window.
@@ -20938,6 +21220,7 @@ function AppShell() {
20938
21220
  ...getTuiHud() ? { hud: getTuiHud() } : {},
20939
21221
  ...getTuiPromptBridge() ? { promptBridge: getTuiPromptBridge() } : {},
20940
21222
  ...getTuiHostRuntime() ? { hostRuntime: getTuiHostRuntime() } : {},
21223
+ ...getTuiBus() ? { bus: getTuiBus() } : {},
20941
21224
  readSettings: readLiveSettings
20942
21225
  }), [
20943
21226
  config.prefix,
@@ -21036,6 +21319,8 @@ function AppShell() {
21036
21319
  extensionRegistry,
21037
21320
  enabledExtensionsSet
21038
21321
  ]);
21322
+ const liveAgentRegistryRef = useRef(liveAgentRegistry);
21323
+ liveAgentRegistryRef.current = liveAgentRegistry;
21039
21324
  /**
21040
21325
  * Single source of truth for "should this call execute?". Three short-circuits:
21041
21326
  *
@@ -21449,7 +21734,270 @@ function AppShell() {
21449
21734
  const queueSelectionIndexRef = useRef(null);
21450
21735
  queueSelectionIndexRef.current = queueSelectionIndex;
21451
21736
  const agentRef = useRef(null);
21737
+ /**
21738
+ * Daemon runtime controller (experimental, `useDaemon` setting). When
21739
+ * set, execution lives in the shared daemon and `agentRef` stays null —
21740
+ * every `agentRef.current?.` consumer degrades soft by construction.
21741
+ * Mutually exclusive with `agentRef` per activation.
21742
+ */
21743
+ const daemonRef = useRef(null);
21744
+ useEffect(() => getTuiBus()?.onEmit((envelope) => {
21745
+ daemonRef.current?.emitExtension(envelope);
21746
+ }), []);
21747
+ /**
21748
+ * Suppresses the settings-publish effect for exactly one commit — set
21749
+ * when a `settings:changed` broadcast is being applied locally, so the
21750
+ * apply doesn't echo straight back to the daemon.
21751
+ */
21752
+ const suppressSettingsPublishRef = useRef(false);
21753
+ const settingsPublishInitRef = useRef(true);
21754
+ const [remoteDraft, setRemoteDraft] = useState(null);
21755
+ const draftSeqRef = useRef(0);
21756
+ const [remoteDraftAttachments, setRemoteDraftAttachments] = useState(null);
21757
+ const draftAttachmentsSeqRef = useRef(0);
21758
+ /**
21759
+ * Remote inline-suggest mirror (daemon mode) — the daemon resolved
21760
+ * ANOTHER window's ghost query on this session (origin-excluded like
21761
+ * drafts). Local `seq` keys the PromptBlock apply; the basis guard
21762
+ * lives in PromptBlock.
21763
+ */
21764
+ const [remoteSuggest, setRemoteSuggest] = useState(null);
21765
+ const remoteSuggestSeqRef = useRef(0);
21766
+ /**
21767
+ * Daemon-side ghost resolver for PromptBlock — the typing window's
21768
+ * request is the single shared computation. Resolves `null` in local
21769
+ * mode and on older daemons (method not found), which tells
21770
+ * PromptBlock to run today's local compute path unchanged.
21771
+ */
21772
+ const resolveGhostRemote = useCallback(async (text, cursor) => {
21773
+ const daemon = daemonRef.current;
21774
+ if (!daemon) return null;
21775
+ return daemon.resolveSuggestion(text, cursor);
21776
+ }, []);
21777
+ /**
21778
+ * Composing-attachment publish (daemon mode) — payloads ride the wire so
21779
+ * a remote window can submit the shared draft attachments-and-all.
21780
+ * Change-driven (paste/drop/remove), not per-keystroke, so no debounce
21781
+ * concerns. No-op in local mode.
21782
+ */
21783
+ const onPromptDraftAttachmentsChange = useCallback((attachments) => {
21784
+ daemonRef.current?.publishDraftAttachments(attachments.map((att) => ({
21785
+ mediaType: att.mediaType,
21786
+ data: att.content.toString("base64"),
21787
+ name: att.name
21788
+ })));
21789
+ }, []);
21790
+ /** Parallel id list for the daemon-mirrored queue (index-aligned with `messageQueue`). */
21791
+ const daemonQueueIdsRef = useRef([]);
21792
+ /**
21793
+ * Live presence from the daemon — sessions with attached windows and
21794
+ * their run / awaiting-input state. Feeds the session list's "active"
21795
+ * scope and per-row status icons. Empty in local mode.
21796
+ */
21797
+ const [sessionPresence, setSessionPresence] = useState(/* @__PURE__ */ new Map());
21798
+ useEffect(() => {
21799
+ if (!settings.useDaemon || isDaemonDisabledByEnv()) {
21800
+ setSessionPresence(/* @__PURE__ */ new Map());
21801
+ return;
21802
+ }
21803
+ let cancelled = false;
21804
+ let client = null;
21805
+ let timer = null;
21806
+ setDaemonHealth("connecting");
21807
+ const applyRows = (rows) => {
21808
+ if (!cancelled) setSessionPresence(new Map(rows.map((row) => [row.sessionId, row])));
21809
+ };
21810
+ const connect = async () => {
21811
+ if (cancelled) return;
21812
+ try {
21813
+ const socketPath = defaultSocketPath();
21814
+ if (!await probeSocket(socketPath)) throw new Error("daemon not listening");
21815
+ const connected = await connectIpcClient({
21816
+ socketPath,
21817
+ appVersion: "tui-presence",
21818
+ drainOnMismatch: false,
21819
+ onClose: () => {
21820
+ if (cancelled) return;
21821
+ client = null;
21822
+ setSessionPresence(/* @__PURE__ */ new Map());
21823
+ timer = setTimeout(() => void connect(), 3e3);
21824
+ }
21825
+ });
21826
+ if (cancelled) {
21827
+ connected.close();
21828
+ return;
21829
+ }
21830
+ client = connected;
21831
+ connected.onSignal((signal) => {
21832
+ if (signal.kind === "presence:changed") applyRows(signal.sessions);
21833
+ });
21834
+ setDaemonHealth("connected");
21835
+ try {
21836
+ applyRows(await connected.request("presence.list", {}));
21837
+ } catch {}
21838
+ } catch {
21839
+ if (!cancelled) setDaemonHealth("disconnected");
21840
+ timer = setTimeout(() => void connect(), 3e3);
21841
+ }
21842
+ };
21843
+ connect();
21844
+ return () => {
21845
+ cancelled = true;
21846
+ if (timer) clearTimeout(timer);
21847
+ client?.close();
21848
+ setDaemonHealth("disconnected");
21849
+ };
21850
+ }, [settings.useDaemon]);
21851
+ /**
21852
+ * Model-pick → session-metadata cabling (daemon mode). Seeded at
21853
+ * activation with the CURRENT pick so the push effect only fires on a
21854
+ * genuine user change — pushing at mount would dispose a possibly-live
21855
+ * daemon agent for nothing.
21856
+ */
21857
+ const lastPushedModelRef = useRef(null);
21858
+ /**
21859
+ * Forward ref to {@link activateSession} — declared early so daemon
21860
+ * callbacks (reconnect-on-disconnect) can reference it lexically before
21861
+ * the callback itself is defined below.
21862
+ */
21863
+ const activateSessionRef = useRef(async () => {});
21864
+ const activationGenerationRef = useRef(0);
21865
+ useEffect(() => {
21866
+ const daemon = daemonRef.current;
21867
+ if (!daemon || !picked) return;
21868
+ const tuple = JSON.stringify([
21869
+ picked.provider.key,
21870
+ picked.model,
21871
+ picked.effort ?? null,
21872
+ picked.modelOptions ?? null
21873
+ ]);
21874
+ if (lastPushedModelRef.current === tuple) return;
21875
+ lastPushedModelRef.current = tuple;
21876
+ daemon.applyModel({
21877
+ providerKey: picked.provider.key,
21878
+ modelId: picked.model,
21879
+ ...picked.effort ? { effort: picked.effort } : {},
21880
+ ...picked.modelOptions ? { modelOptions: picked.modelOptions } : {}
21881
+ });
21882
+ }, [picked]);
21883
+ /** Remote search-query mirror for the PromptBlock input (seq-keyed apply). */
21884
+ const [remoteSearchQuery, setRemoteSearchQuery] = useState(null);
21885
+ const remoteSearchSeqRef = useRef(0);
21886
+ const searchPublishInitRef = useRef(true);
21887
+ useEffect(() => {
21888
+ if (searchPublishInitRef.current) {
21889
+ searchPublishInitRef.current = false;
21890
+ return;
21891
+ }
21892
+ daemonRef.current?.publishSearch(transcriptSearchMode, transcriptSearchQuery);
21893
+ }, [transcriptSearchMode, transcriptSearchQuery]);
21894
+ /**
21895
+ * Local-edit → daemon draft publish, on EVERY keystroke. No debounce:
21896
+ * `publishDraft` is a fire-and-forget JSON-RPC notification on a local
21897
+ * unix socket (no ids, no response), so per-key cost is negligible and
21898
+ * other windows mirror the draft with keystroke latency. Synchronous
21899
+ * dispatch also removes the timer/session-switch race the debounce
21900
+ * needed guards for. No-op in local mode.
21901
+ */
21902
+ const onPromptDraftChange = useCallback((text, shell) => {
21903
+ daemonRef.current?.publishDraft(text, shell);
21904
+ }, []);
21905
+ const prevSettingsRef = useRef(settings);
21906
+ useEffect(() => {
21907
+ const prev = prevSettingsRef.current;
21908
+ prevSettingsRef.current = settings;
21909
+ if (settingsPublishInitRef.current) {
21910
+ settingsPublishInitRef.current = false;
21911
+ return;
21912
+ }
21913
+ if (suppressSettingsPublishRef.current) {
21914
+ suppressSettingsPublishRef.current = false;
21915
+ return;
21916
+ }
21917
+ const patch = {};
21918
+ for (const key of Object.keys(settings)) if (JSON.stringify(prev[key]) !== JSON.stringify(settings[key])) patch[key] = settings[key];
21919
+ if (Object.keys(patch).length > 0) daemonRef.current?.publishSettings(patch);
21920
+ }, [settings]);
21452
21921
  const sessionRef = useRef(null);
21922
+ /**
21923
+ * Todo-change listeners, notified by BOTH runtimes.
21924
+ *
21925
+ * Runtime-agnostic on purpose. Binding local-vs-daemon at subscribe time
21926
+ * looked equivalent but wasn't: `agentRef` is null between teardown and
21927
+ * rebuild, and the `ctrl+t` guard only requires a chat screen with a session
21928
+ * — so opening the modal in that window registered against whichever runtime
21929
+ * happened to exist at mount. A local open with `agentRef` momentarily null
21930
+ * bound to nothing, and an agent rebuild under an open modal left the
21931
+ * subscription on the dead agent. Both reproduce the frozen-modal bug this
21932
+ * plumbing exists to fix.
21933
+ */
21934
+ const todosListenersRef = useRef(/* @__PURE__ */ new Set());
21935
+ /**
21936
+ * Monotonic token for todo-snapshot application order.
21937
+ *
21938
+ * `todowrite` typically fires several times per run, and each signal starts
21939
+ * its own `loadSession`. Those loads can overlap and resolve out of order, in
21940
+ * which case the OLDER snapshot is applied last and sticks — the window keeps
21941
+ * rendering a stale list until an unrelated `todowrite` happens along, so it
21942
+ * does not self-heal. Same shape as the `suggestSeq` guard in
21943
+ * `runtime/daemon.ts`: stamp before the await, bail after it if a newer
21944
+ * application has started.
21945
+ */
21946
+ const todosApplySeqRef = useRef(0);
21947
+ const [, bumpTodosRevision] = useState(0);
21948
+ const notifyTodosChanged = useCallback(() => {
21949
+ bumpTodosRevision((n) => n + 1);
21950
+ for (const listener of todosListenersRef.current) listener();
21951
+ }, []);
21952
+ const subscribeTodos = useCallback((onChange) => {
21953
+ todosListenersRef.current.add(onChange);
21954
+ return () => {
21955
+ todosListenersRef.current.delete(onChange);
21956
+ };
21957
+ }, []);
21958
+ const commitTodos = useCallback((items) => {
21959
+ const session = sessionRef.current;
21960
+ if (!session) return;
21961
+ const runId = selectActiveTodos(session).runId;
21962
+ if (!runId) return;
21963
+ setTodosForRun(session, runId, items, { clearArchive: true });
21964
+ notifyTodosChanged();
21965
+ const daemon = daemonRef.current;
21966
+ if (!daemon) return;
21967
+ daemon.setTodos(items).then(async (applied) => {
21968
+ if (applied) return;
21969
+ await session.save();
21970
+ notifyTodosChanged();
21971
+ }).catch(() => {
21972
+ streamRef.current?.appendImmediate({
21973
+ kind: "info",
21974
+ text: "⚠ todo edit did not reach the daemon — it will re-sync on reconnect."
21975
+ });
21976
+ });
21977
+ }, [notifyTodosChanged]);
21978
+ useEffect(() => {
21979
+ if (!busy) return;
21980
+ let lastBeat = Promise.resolve();
21981
+ let beatSessionId = null;
21982
+ const timer = setInterval(() => {
21983
+ const agent = agentRef.current;
21984
+ const session = sessionRef.current;
21985
+ if (!settingsRef.current.useDaemon || isDaemonDisabledByEnv()) return;
21986
+ if (agent?.isRunning && session) {
21987
+ beatSessionId = session.id;
21988
+ lastBeat = store.updateStatus(session.id, "running").catch(() => void 0);
21989
+ }
21990
+ }, 6e4);
21991
+ return () => {
21992
+ clearInterval(timer);
21993
+ lastBeat.then(() => {
21994
+ const session = sessionRef.current;
21995
+ if (!beatSessionId || agentRef.current?.isRunning) return;
21996
+ const settled = session?.id === beatSessionId ? session.status : "idle";
21997
+ if (settled !== "running") store.updateStatus(beatSessionId, settled).catch(() => void 0);
21998
+ });
21999
+ };
22000
+ }, [busy, store]);
21453
22001
  useEffect(() => attachTuiRendererState(() => sessionRef.current ?? void 0), []);
21454
22002
  const chatReferenceScopeRef = useRef(chatReferenceScope);
21455
22003
  chatReferenceScopeRef.current = chatReferenceScope;
@@ -21789,6 +22337,7 @@ function AppShell() {
21789
22337
  setQueueSelectionIndex(null);
21790
22338
  suppressWakeRef.current = true;
21791
22339
  agentRef.current?.abort();
22340
+ daemonRef.current?.interrupt();
21792
22341
  }, [denyAll, interactions]);
21793
22342
  /**
21794
22343
  * `ProviderKey` → runtime `Provider.name` for the providers constructed
@@ -22347,6 +22896,16 @@ function AppShell() {
22347
22896
  });
22348
22897
  agent.hooks.hook("agent:done", () => {
22349
22898
  guardNoticed.clear();
22899
+ const record = [...session.runs].reverse().find((r) => r.parentRunId === void 0);
22900
+ if (!record) return;
22901
+ const receipt = runReceiptFromRun(record);
22902
+ const lastTurnId = session.turns.at(-1)?.id;
22903
+ if (hasRunReceipt(receipt)) stream.appendImmediate({
22904
+ kind: "run-summary",
22905
+ text: formatRunReceiptLine(receipt),
22906
+ runSummary: receipt,
22907
+ ...lastTurnId ? { turnId: lastTurnId } : {}
22908
+ });
22350
22909
  });
22351
22910
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
22352
22911
  registerInFlightTool({
@@ -22381,6 +22940,7 @@ function AppShell() {
22381
22940
  callId,
22382
22941
  turnId
22383
22942
  });
22943
+ if (name === "todowrite") notifyTodosChanged();
22384
22944
  });
22385
22945
  agent.hooks.hook("tool:error", ({ callId }) => {
22386
22946
  unregisterInFlightTool(callId);
@@ -22491,18 +23051,8 @@ function AppShell() {
22491
23051
  const reason = lastFinishReasonRef.current;
22492
23052
  lastFinishReasonRef.current = void 0;
22493
23053
  if (sessionRef.current?.id !== session.id) return;
22494
- if (reason === "length") emitNotice({
22495
- severity: "warn",
22496
- label: "output limit"
22497
- }, "stopped at the model output limit (max_tokens). Turn on \"Auto-resume on output limit\" or raise \"Max output tokens\" in Settings to continue past it.");
22498
- else if (reason === "pause") emitNotice({
22499
- severity: "warn",
22500
- label: "pause cap"
22501
- }, "stopped after repeated empty pause turns from the provider (maxConsecutivePauseTurns cap is armed).");
22502
- else if (reason === "content-filter") emitNotice({
22503
- severity: "warn",
22504
- label: "refusal"
22505
- }, "the provider refused to continue (safety filter / refusal stop) despite automatic retries. Rephrase the request, or start a fresh session if it keeps happening.");
23054
+ const stop = runEndNotice(reason);
23055
+ if (stop) emitNotice(stop.notice, stop.text);
22506
23056
  });
22507
23057
  agent.hooks.hook("compact:start", ({ usedFraction }) => {
22508
23058
  setCompacting(true);
@@ -22545,43 +23095,19 @@ function AppShell() {
22545
23095
  }, `removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}`);
22546
23096
  });
22547
23097
  const spawnStartedAt = /* @__PURE__ */ new Map();
22548
- agent.hooks.hook("spawn:before", ({ id, task, depth, model, resumesChildId }) => {
23098
+ agent.hooks.hook("spawn:before", (ctx) => {
22549
23099
  const startedAt = Date.now();
22550
- const taskPreview = previewLine(task, 80);
22551
- spawnStartedAt.set(id, {
23100
+ spawnStartedAt.set(ctx.id, {
22552
23101
  startedAt,
22553
- task: taskPreview,
22554
- ...model ? { model } : {}
22555
- });
22556
- stream.appendImmediate({
22557
- kind: "spawn-start",
22558
- text: resumesChildId ? `resumes ${resumesChildId} · ${taskPreview}` : taskPreview,
22559
- childId: id,
22560
- depth: depth ?? 1,
22561
- spawn: { startedAt },
22562
- ...model ? { model } : {}
23102
+ task: previewLine(ctx.task, 80),
23103
+ ...ctx.model ? { model: ctx.model } : {}
22563
23104
  });
23105
+ stream.appendImmediate(buildSpawnStartEvent(ctx, startedAt));
22564
23106
  });
22565
- agent.hooks.hook("spawn:complete", ({ id, depth, status, stats }) => {
22566
- const tag = status === "aborted" ? "aborted" : status === "error" ? "error" : "done";
22567
- const started = spawnStartedAt.get(id);
22568
- const startedAt = started?.startedAt;
22569
- spawnStartedAt.delete(id);
22570
- const elapsed = startedAt !== void 0 ? ` · ${formatDuration(Date.now() - startedAt)}` : "";
22571
- const cost = flattenTurns(stats).reduce((sum, turn) => sum + (turn.cost ?? 0), 0);
22572
- const segments = `${elapsed}${cost > 0 ? ` · $${cost.toFixed(cost < .01 ? 4 : 2)}` : ""}`;
22573
- stream.appendImmediate({
22574
- kind: "spawn-end",
22575
- text: segments ? `${tag}${segments} · ${formatTokenUsage(stats)}` : `${tag} ${formatTokenUsage(stats)}`,
22576
- childId: id,
22577
- depth: depth ?? 1,
22578
- spawn: {
22579
- startedAt: startedAt ?? Date.now(),
22580
- ...started?.task ? { task: started.task } : {},
22581
- turns: stats.turns
22582
- },
22583
- ...started?.model ? { model: started.model } : {}
22584
- });
23107
+ agent.hooks.hook("spawn:complete", (ctx) => {
23108
+ const started = spawnStartedAt.get(ctx.id);
23109
+ spawnStartedAt.delete(ctx.id);
23110
+ stream.appendImmediate(buildSpawnEndEvent(ctx, started));
22585
23111
  });
22586
23112
  agent.hooks.hook("spawn:error", ({ id, depth, error }) => {
22587
23113
  emitNotice({
@@ -22698,11 +23224,17 @@ function AppShell() {
22698
23224
  unregisterInFlightTool,
22699
23225
  extensionRegistry
22700
23226
  ]);
22701
- const [sessionsScopeAll, setSessionsScopeAll] = useState(null);
22702
- const sessionsShowAll = sessionsScopeAll ?? settings.showAllProjects;
23227
+ const [sessionsScopeOverride, setSessionsScopeOverride] = useState(null);
23228
+ const sessionsScope = sessionsScopeOverride ?? (settings.showAllProjects ? "all" : "project");
23229
+ const sessionsShowAll = sessionsScope !== "project";
22703
23230
  const toggleSessionsScope = useCallback(() => {
22704
- setSessionsScopeAll(!sessionsShowAll);
22705
- }, [sessionsShowAll]);
23231
+ setSessionsScopeOverride((prev) => {
23232
+ const current = prev ?? (settingsRef.current.showAllProjects ? "all" : "project");
23233
+ if (current === "project") return "all";
23234
+ if (current === "all") return settingsRef.current.useDaemon && !isDaemonDisabledByEnv() ? "active" : "project";
23235
+ return "project";
23236
+ });
23237
+ }, []);
22706
23238
  /** Debounced, cached full-history search for the conversations list. */
22707
23239
  const onSessionSearchQueryChange = useCallback((raw) => {
22708
23240
  sessionSearchQueryRef.current = raw;
@@ -22729,6 +23261,17 @@ function AppShell() {
22729
23261
  });
22730
23262
  sessionSearchTimerRef.current = setTimeout(() => {
22731
23263
  (async () => {
23264
+ const remote = await daemonRef.current?.searchHistory(query, sessions.map((meta) => meta.id), 3);
23265
+ if (remote) {
23266
+ if (generation !== sessionSearchGenerationRef.current) return;
23267
+ setSessionContentMatches(new Map(Object.entries(remote)));
23268
+ setSessionContentSearchProgress({
23269
+ done: sessions.length,
23270
+ total: sessions.length
23271
+ });
23272
+ setSessionContentSearching(false);
23273
+ return;
23274
+ }
22732
23275
  const matches = /* @__PURE__ */ new Map();
22733
23276
  let cursor = 0;
22734
23277
  let done = 0;
@@ -22839,18 +23382,112 @@ function AppShell() {
22839
23382
  sessionSearchAbortRef.current?.abort();
22840
23383
  historySearchWorkerRef.current?.terminate();
22841
23384
  }, []);
23385
+ /**
23386
+ * Resolved "Main" orchestration session id. Learned from
23387
+ * `ensureMainSession` — the on-enable effect and the `openMainSession`
23388
+ * chord — so it is set as soon as the setting is on, and `null` while the
23389
+ * feature is off. What is deliberately NOT done is resolving it because the
23390
+ * sessions list was merely opened: that path creates the session lazily, and
23391
+ * browsing must not conjure one. The list only pins a row for an id it can
23392
+ * already find, so a stale/unknown value here is harmless.
23393
+ */
23394
+ const [mainSessionId, setMainSessionId] = useState(null);
23395
+ /** In-flight local main creation — see `ensureMainSession`. */
23396
+ const mainSessionCreationRef = useRef(null);
23397
+ /**
23398
+ * Cache for the pin lookup below. Discovering the main costs an UNFILTERED
23399
+ * list (it is directory-less, so no scoped query returns it); the id is
23400
+ * stable for the store's lifetime, so pay that once and read one row by id
23401
+ * on every refresh after.
23402
+ */
23403
+ const pinnedMainIdRef = useRef(null);
23404
+ /**
23405
+ * The main's row for the pin, or `null` when the store has no main yet.
23406
+ * Re-resolves when the cached id no longer names a main (deleted, or a
23407
+ * different store).
23408
+ */
23409
+ const pinnedMainMeta = useCallback(async () => {
23410
+ const cached = pinnedMainIdRef.current;
23411
+ if (cached) {
23412
+ const meta = await getSessionMeta(store, cached);
23413
+ if (meta?.isMain) return meta;
23414
+ pinnedMainIdRef.current = null;
23415
+ }
23416
+ const found = (await listSessionMeta(store)).find((meta) => meta.isMain) ?? null;
23417
+ pinnedMainIdRef.current = found?.id ?? null;
23418
+ return found;
23419
+ }, [store]);
22842
23420
  const refreshSessions = useCallback(async () => {
22843
- const list = await listSessionMeta(store, sessionsShowAll ? void 0 : { projectRoot: projectDir });
23421
+ const raw = await listSessionMeta(store, sessionsShowAll ? void 0 : { projectRoot: projectDir });
23422
+ publishSessionRefs(raw);
23423
+ const list = settings.orchestration ? raw : raw.filter((meta) => !meta.isMain);
23424
+ if (settings.orchestration && !sessionsShowAll && !list.some((meta) => meta.isMain)) {
23425
+ const main = await pinnedMainMeta();
23426
+ if (main) {
23427
+ const merged = [main, ...list];
23428
+ setSessions(merged);
23429
+ return merged;
23430
+ }
23431
+ }
22844
23432
  setSessions(list);
22845
23433
  return list;
22846
23434
  }, [
22847
23435
  store,
22848
23436
  sessionsShowAll,
22849
- projectDir
23437
+ projectDir,
23438
+ settings.orchestration,
23439
+ pinnedMainMeta
22850
23440
  ]);
22851
23441
  useEffect(() => {
22852
23442
  refreshSessions();
22853
23443
  }, [refreshSessions]);
23444
+ useEffect(() => {
23445
+ setSessionRefResolver(async (id) => {
23446
+ const meta = await getSessionMeta(store, id);
23447
+ if (!meta) return void 0;
23448
+ const dir = sessionDirLabel(meta.projectRoot);
23449
+ return dir ? {
23450
+ title: meta.title,
23451
+ dir
23452
+ } : { title: meta.title };
23453
+ });
23454
+ return () => setSessionRefResolver(null);
23455
+ }, [store]);
23456
+ const ensureMainSession = useCallback(async () => {
23457
+ if (!settings.orchestration) return null;
23458
+ const daemon = daemonRef.current;
23459
+ if (daemon) {
23460
+ const id = await daemon.mainSession();
23461
+ if (id) pinnedMainIdRef.current = id;
23462
+ return id;
23463
+ }
23464
+ if (mainSessionCreationRef.current) return mainSessionCreationRef.current;
23465
+ const inflight = (async () => {
23466
+ const existing = await pinnedMainMeta();
23467
+ if (existing) return existing.id;
23468
+ const id = await createMainSession(store);
23469
+ pinnedMainIdRef.current = id;
23470
+ await refreshSessions();
23471
+ return id;
23472
+ })();
23473
+ mainSessionCreationRef.current = inflight;
23474
+ try {
23475
+ return await inflight;
23476
+ } finally {
23477
+ mainSessionCreationRef.current = null;
23478
+ }
23479
+ }, [
23480
+ settings.orchestration,
23481
+ store,
23482
+ refreshSessions,
23483
+ pinnedMainMeta
23484
+ ]);
23485
+ useEffect(() => {
23486
+ if (!settings.orchestration) return;
23487
+ ensureMainSession().then((id) => {
23488
+ if (id) setMainSessionId(id);
23489
+ }).catch((err) => debugLog("ensureMainSession failed", err));
23490
+ }, [settings.orchestration, ensureMainSession]);
22854
23491
  /**
22855
23492
  * Ref mirror for the resume-on-launch effect below: its calls must see
22856
23493
  * the live callback WITHOUT depending on its identity — `refreshSessions`
@@ -22862,6 +23499,27 @@ function AppShell() {
22862
23499
  const refreshSessionsRef = useRef(refreshSessions);
22863
23500
  refreshSessionsRef.current = refreshSessions;
22864
23501
  const teardown = useCallback(async () => {
23502
+ try {
23503
+ daemonRef.current?.dispose();
23504
+ } catch (err) {
23505
+ debugLog("teardown: daemon dispose failed", err);
23506
+ }
23507
+ daemonRef.current = null;
23508
+ setDaemonConnected(false);
23509
+ activationGenerationRef.current += 1;
23510
+ setRemoteDraft(null);
23511
+ setRemoteDraftAttachments(null);
23512
+ setRemoteSuggest(null);
23513
+ setRemoteSearchQuery(null);
23514
+ setRemoteHudState(null);
23515
+ setTranscriptSearchMode((prev) => {
23516
+ if (prev) searchPublishInitRef.current = true;
23517
+ return false;
23518
+ });
23519
+ setTranscriptSearchQuery((prev) => {
23520
+ if (prev.length > 0) searchPublishInitRef.current = true;
23521
+ return "";
23522
+ });
22865
23523
  try {
22866
23524
  denyAll();
22867
23525
  } catch (err) {
@@ -23015,20 +23673,388 @@ function AppShell() {
23015
23673
  }, [stream, autoCompactBehaviorFor]);
23016
23674
  const activateSession = useCallback(async (id, key, opts) => {
23017
23675
  await teardown();
23018
- const session = (id ? await loadSession(store, id) : null) ?? await createSession({
23676
+ activationGenerationRef.current += 1;
23677
+ const activationGeneration = activationGenerationRef.current;
23678
+ const loaded = id ? await loadSession(store, id) : null;
23679
+ const session = loaded ?? await createSession({
23019
23680
  store,
23020
23681
  projectRoot: projectDir,
23682
+ agentId: pickedAgentRef.current.id,
23021
23683
  ...id ? { id } : {},
23022
23684
  ...opts?.metadata ? { metadata: opts.metadata } : {}
23023
23685
  });
23024
23686
  sessionRef.current = session;
23025
- agentRef.current = buildAgent(session, key);
23687
+ let stampedLegacyAgent = false;
23688
+ if (loaded) {
23689
+ const persistedProfile = session.agentId ? liveAgentRegistryRef.current[session.agentId] : void 0;
23690
+ if (persistedProfile && persistedProfile.id !== pickedAgentRef.current.id) {
23691
+ pickedAgentRef.current = persistedProfile;
23692
+ setPickedAgent(persistedProfile);
23693
+ } else if (!session.agentId) {
23694
+ session.setAgentId(pickedAgentRef.current.id);
23695
+ stampedLegacyAgent = true;
23696
+ }
23697
+ }
23698
+ if (settingsRef.current.useDaemon && !isDaemonDisabledByEnv()) try {
23699
+ agentRef.current = null;
23700
+ if (!loaded && typeof session.metadata.cwd !== "string") {
23701
+ session.setMeta("cwd", cwdRef.current ?? process.cwd());
23702
+ await session.save();
23703
+ }
23704
+ const activePick = pickedRef.current;
23705
+ let seedModelViaRpc = false;
23706
+ if (activePick) {
23707
+ if (typeof session.metadata.providerKey !== "string") if (loaded) seedModelViaRpc = true;
23708
+ else {
23709
+ session.setMeta("providerKey", activePick.provider.key);
23710
+ session.setMeta("modelId", activePick.model);
23711
+ if (activePick.effort) session.setMeta("effort", activePick.effort);
23712
+ if (activePick.modelOptions) session.setMeta("modelOptions", activePick.modelOptions);
23713
+ await session.save();
23714
+ }
23715
+ lastPushedModelRef.current = JSON.stringify([
23716
+ activePick.provider.key,
23717
+ activePick.model,
23718
+ activePick.effort ?? null,
23719
+ activePick.modelOptions ?? null
23720
+ ]);
23721
+ } else lastPushedModelRef.current = null;
23722
+ const sessionId = session.id;
23723
+ const refreshHeaderFromStore = () => {
23724
+ store.load(sessionId).then((data) => {
23725
+ if (!data || sessionRef.current?.id !== sessionId) return;
23726
+ setCurrentSession({
23727
+ id: data.id,
23728
+ title: deriveSessionTitle(data.turns, data.metadata),
23729
+ turnCount: data.turns.length,
23730
+ userMessageCount: data.turns.reduce((n, t) => t.role === "user" ? n + 1 : n, 0),
23731
+ runCount: data.runs.length,
23732
+ updatedAt: data.updatedAt,
23733
+ ...data.projectRoot ? { projectRoot: data.projectRoot } : {}
23734
+ });
23735
+ }).catch(() => void 0);
23736
+ };
23737
+ let controllerHandle = null;
23738
+ const reflectRemoteModel = () => {
23739
+ store.load(sessionId).then((data) => {
23740
+ if (!data || sessionRef.current?.id !== sessionId) return;
23741
+ const providerKey = data.metadata.providerKey;
23742
+ const modelId = data.metadata.modelId;
23743
+ if (typeof providerKey !== "string" || typeof modelId !== "string") return;
23744
+ const auth = availableProvidersRef.current.find((p) => p.key === providerKey);
23745
+ if (!auth) return;
23746
+ const next = makePicked(auth, modelId);
23747
+ if (!next) return;
23748
+ const effort = data.metadata.effort;
23749
+ const modelOptions = data.metadata.modelOptions;
23750
+ const merged = {
23751
+ ...next,
23752
+ ...typeof effort === "string" ? { effort } : {},
23753
+ ...modelOptions && typeof modelOptions === "object" ? { modelOptions } : {}
23754
+ };
23755
+ const tuple = JSON.stringify([
23756
+ merged.provider.key,
23757
+ merged.model,
23758
+ merged.effort ?? null,
23759
+ merged.modelOptions ?? null
23760
+ ]);
23761
+ if (lastPushedModelRef.current === tuple) return;
23762
+ lastPushedModelRef.current = tuple;
23763
+ setPicked(merged);
23764
+ }).catch(() => void 0);
23765
+ };
23766
+ const reflectRemoteAgent = () => {
23767
+ (store.getMeta ? store.getMeta(sessionId) : store.load(sessionId)).then((meta) => {
23768
+ if (!meta || sessionRef.current?.id !== sessionId) return;
23769
+ const remote = meta.agentId;
23770
+ if (!remote || remote === pickedAgentRef.current.id) return;
23771
+ sessionRef.current.setAgentId(remote);
23772
+ const profile = liveAgentRegistryRef.current[remote];
23773
+ if (!profile) return;
23774
+ pickedAgentRef.current = profile;
23775
+ setPickedAgent(profile);
23776
+ }).catch(() => void 0);
23777
+ };
23778
+ const resyncTranscript = () => {
23779
+ controllerHandle?.fetchTranscript().then(async (canonical) => {
23780
+ if (!canonical || sessionRef.current?.id !== sessionId || runningRef.current) return;
23781
+ await streamRef.current?.whenDrained();
23782
+ if (sessionRef.current?.id !== sessionId || runningRef.current) return;
23783
+ streamRef.current?.reset();
23784
+ setEvents(capTranscriptEvents(canonical));
23785
+ }).catch(() => void 0);
23786
+ };
23787
+ const controller = await connectDaemonSession({
23788
+ sessionId,
23789
+ appVersion: "tui",
23790
+ callbacks: {
23791
+ onRetry: ({ attempt, max, source }) => setRetryInfo({
23792
+ attempt,
23793
+ max,
23794
+ source
23795
+ }),
23796
+ onRetryCleared: (source) => clearRetryInfo(source),
23797
+ onPromptTurns: (refs) => {
23798
+ stream.flushAndUpdate((events) => retagUserPromptEventsFromRefs(events, refs));
23799
+ },
23800
+ onCompactionProgress: (tokens) => setCompactionTokens(tokens),
23801
+ onModelFallback: (from, to) => {
23802
+ setActiveFallback({
23803
+ provider: to.provider,
23804
+ model: to.model
23805
+ });
23806
+ stream.appendImmediate({
23807
+ kind: "info",
23808
+ text: `⚠ model fallback: ${from.provider}/${from.model} failed — continuing on ${to.provider}/${to.model}`
23809
+ });
23810
+ },
23811
+ onCacheBreak: (info) => {
23812
+ const providerKey = pickedRef.current?.provider.key;
23813
+ const apply = (costUsd) => {
23814
+ setActiveCacheBreak(costUsd !== void 0 ? {
23815
+ ...info,
23816
+ costUsd
23817
+ } : info);
23818
+ };
23819
+ if (!providerKey) {
23820
+ apply(void 0);
23821
+ return;
23822
+ }
23823
+ estimateRebilledCostUsd(providerKey, info.model, info.rebilledTokens).then(apply).catch(() => apply(void 0));
23824
+ },
23825
+ onSystemMd: (file) => {
23826
+ sessionSystemFileRef.current = file;
23827
+ announceProjectSystemOverride();
23828
+ },
23829
+ onSettingsChanged: (remote) => {
23830
+ const local = settingsRef.current;
23831
+ const entries = Object.entries(remote).filter(([key, value]) => key in DEFAULT_SETTINGS && JSON.stringify(local[key]) !== JSON.stringify(value));
23832
+ if (entries.length === 0) return;
23833
+ suppressSettingsPublishRef.current = true;
23834
+ for (const [key, value] of entries) setSetting(key, value);
23835
+ },
23836
+ onSessionsChanged: (op, changedId) => {
23837
+ if (op === "update" && changedId === sessionId) {
23838
+ refreshHeaderFromStore();
23839
+ resyncTranscript();
23840
+ reflectRemoteModel();
23841
+ reflectRemoteAgent();
23842
+ }
23843
+ },
23844
+ onDraftChanged: (text, shell) => {
23845
+ draftSeqRef.current += 1;
23846
+ setRemoteDraft({
23847
+ seq: draftSeqRef.current,
23848
+ text,
23849
+ ...shell ? { shell: true } : {}
23850
+ });
23851
+ },
23852
+ onSuggestChanged: ({ basis, caret, suggestion, candidates }) => {
23853
+ remoteSuggestSeqRef.current += 1;
23854
+ setRemoteSuggest({
23855
+ seq: remoteSuggestSeqRef.current,
23856
+ basis,
23857
+ caret,
23858
+ suggestion,
23859
+ ...candidates ? { candidates } : {}
23860
+ });
23861
+ },
23862
+ onSearchChanged: ({ active, query }) => {
23863
+ setTranscriptSearchMode(active);
23864
+ setTranscriptSearchQuery(query);
23865
+ if (!active) setSelectedTurnId(null);
23866
+ remoteSearchSeqRef.current += 1;
23867
+ setRemoteSearchQuery(active ? {
23868
+ seq: remoteSearchSeqRef.current,
23869
+ query
23870
+ } : null);
23871
+ },
23872
+ onDraftAttachmentsChanged: (attachments) => {
23873
+ draftAttachmentsSeqRef.current += 1;
23874
+ setRemoteDraftAttachments({
23875
+ seq: draftAttachmentsSeqRef.current,
23876
+ attachments: attachments.map((att) => ({
23877
+ name: att.name ?? "attachment",
23878
+ mediaType: att.mediaType,
23879
+ content: Buffer.from(att.data, "base64")
23880
+ }))
23881
+ });
23882
+ },
23883
+ onSkillsChanged: (active) => {
23884
+ setActiveSkillNames(new Set(active));
23885
+ },
23886
+ onMcpAuth: ({ name, reason }) => {
23887
+ dispatchAuthRef.current({
23888
+ type: "auth-required",
23889
+ name,
23890
+ reason: reason === "auto-promoted" ? "auto-promoted" : "no-tokens"
23891
+ });
23892
+ },
23893
+ onHudState: (state) => {
23894
+ setRemoteHudState(state);
23895
+ },
23896
+ onHudNotify: (n) => {
23897
+ const allow = enabledExtensionsRef.current;
23898
+ if (allow !== void 0 && !allow.includes(n.extension)) return;
23899
+ stream.appendImmediate({
23900
+ kind: n.tone === "error" ? "error" : "info",
23901
+ text: `${n.extension}: ${n.tone === "warn" ? "⚠ " : ""}${n.text}`
23902
+ });
23903
+ },
23904
+ onExtensionSignal: (envelope) => {
23905
+ getTuiBus()?.deliver(envelope);
23906
+ },
23907
+ onTurnEnd: ({ usage, childId }) => {
23908
+ if (childId !== void 0) stream.flushAndUpdate((prev) => finalizeStreamingMarkdownForOwner(prev, childId));
23909
+ if (!usage) return;
23910
+ if (childId === void 0) {
23911
+ lastFinishReasonRef.current = usage.finishReason;
23912
+ const tokens = turnContextSize(usage);
23913
+ if (tokens > 0) {
23914
+ setLastInputTokens(tokens);
23915
+ lastInputTokensRef.current = tokens;
23916
+ }
23917
+ }
23918
+ const turnCost = usage.cost ?? 0;
23919
+ if (turnCost > 0) setSessionCost((prev) => prev + turnCost);
23920
+ },
23921
+ onTodosChanged: (snapshot) => {
23922
+ todosApplySeqRef.current += 1;
23923
+ const seq = todosApplySeqRef.current;
23924
+ const open = sessionRef.current;
23925
+ if (open?.id === sessionId && pickActiveRunId(open) === snapshot.runId) {
23926
+ applyTodosSnapshot(open, snapshot);
23927
+ notifyTodosChanged();
23928
+ return;
23929
+ }
23930
+ loadSession(store, sessionId).then((fresh) => {
23931
+ if (!fresh || sessionRef.current?.id !== sessionId) return;
23932
+ if (seq !== todosApplySeqRef.current) return;
23933
+ applyTodosSnapshot(fresh, snapshot);
23934
+ sessionRef.current = fresh;
23935
+ refreshHeaderFromStore();
23936
+ notifyTodosChanged();
23937
+ }).catch(() => void 0);
23938
+ },
23939
+ onWorkChanged: (items) => {
23940
+ const mapped = items.map((item) => ({
23941
+ kind: item.kind,
23942
+ callId: item.id,
23943
+ tool: item.label,
23944
+ startedAt: item.startedAt
23945
+ }));
23946
+ setInFlightTools(mapped.filter((m) => m.kind === "tool"));
23947
+ setBackgroundTasks(mapped.filter((m) => m.kind === "task"));
23948
+ },
23949
+ onQueueChanged: (items) => {
23950
+ daemonQueueIdsRef.current = items.map((item) => item.id);
23951
+ const mirrored = items.map((item) => ({
23952
+ prompt: item.text,
23953
+ references: [],
23954
+ attachments: []
23955
+ }));
23956
+ messageQueueRef.current = mirrored;
23957
+ setMessageQueue(mirrored);
23958
+ setQueueSelectionIndex((prev) => prev == null ? prev : mirrored.length === 0 ? null : Math.min(prev, mirrored.length - 1));
23959
+ },
23960
+ onInitialTranscript: (initial) => {
23961
+ setEvents(capTranscriptEvents(initial));
23962
+ },
23963
+ onStreamEvent: (event) => {
23964
+ const source = {
23965
+ ...event.turnId ? { turnId: event.turnId } : {},
23966
+ ...event.childId ? { childId: event.childId } : {},
23967
+ ...typeof event.depth === "number" ? { depth: event.depth } : {}
23968
+ };
23969
+ if (event.kind === "markdown" && event.streaming) stream.queueStreamDelta("markdown", event.text, source);
23970
+ else if (event.kind === "thinking") stream.queueStreamDelta("thinking", event.text, source);
23971
+ else stream.appendImmediate(event);
23972
+ },
23973
+ onRunState: (running, info) => {
23974
+ runningRef.current = running;
23975
+ setBusy(running);
23976
+ if (running) return;
23977
+ stream.flushAndUpdate(finalizeStreamingMarkdown);
23978
+ if (info && !info.ok && info.error) stream.appendImmediate({
23979
+ kind: "error",
23980
+ text: info.error
23981
+ });
23982
+ refreshHeaderFromStore();
23983
+ resyncTranscript();
23984
+ },
23985
+ onApproval: (request, resolve) => {
23986
+ requestApproval(request.tool, request.input, request.originator, request.id).then(resolve);
23987
+ },
23988
+ onApprovalResolved: (requestId) => {
23989
+ dropApprovalByCorrelation(requestId);
23990
+ },
23991
+ onInteraction: (request, actions) => {
23992
+ interactions.enqueue({
23993
+ request,
23994
+ resolve: actions.respond,
23995
+ cancel: () => actions.cancel()
23996
+ });
23997
+ },
23998
+ onInteractionResolved: (requestId) => {
23999
+ interactions.dropById(requestId);
24000
+ },
24001
+ onCompacting: (active, info) => {
24002
+ setCompacting(active);
24003
+ if (!active && info?.ok && info.effectiveTokens !== void 0) {
24004
+ setLastInputTokens(info.effectiveTokens);
24005
+ lastInputTokensRef.current = info.effectiveTokens;
24006
+ }
24007
+ },
24008
+ onDisconnect: () => {
24009
+ runningRef.current = false;
24010
+ setBusy(false);
24011
+ setDaemonHealth("connecting");
24012
+ stream.appendImmediate({
24013
+ kind: "info",
24014
+ text: "daemon connection lost — reconnecting…"
24015
+ });
24016
+ setTimeout(() => {
24017
+ if (sessionRef.current?.id === sessionId && settingsRef.current.useDaemon && daemonRef.current === controllerHandle) activateSessionRef.current(sessionId, pickedRef.current?.provider.key ?? "anthropic").catch(() => {
24018
+ stream.appendImmediate({
24019
+ kind: "error",
24020
+ text: "reconnect failed — press ctrl+r or reopen the session to retry"
24021
+ });
24022
+ });
24023
+ }, 1500);
24024
+ }
24025
+ }
24026
+ });
24027
+ if (activationGenerationRef.current !== activationGeneration) {
24028
+ controller.dispose();
24029
+ return;
24030
+ }
24031
+ controllerHandle = controller;
24032
+ daemonRef.current = controller;
24033
+ setDaemonConnected(true);
24034
+ if (stampedLegacyAgent) controller.setAgentId(pickedAgentRef.current.id);
24035
+ if (seedModelViaRpc && activePick) controller.applyModel({
24036
+ providerKey: activePick.provider.key,
24037
+ modelId: activePick.model,
24038
+ ...activePick.effort ? { effort: activePick.effort } : {},
24039
+ ...activePick.modelOptions ? { modelOptions: activePick.modelOptions } : {}
24040
+ });
24041
+ } catch (err) {
24042
+ debugLog("daemon activation failed — falling back to in-process runtime", err);
24043
+ daemonRef.current = null;
24044
+ setDaemonConnected(false);
24045
+ agentRef.current = buildAgent(session, key);
24046
+ stream.appendImmediate({
24047
+ kind: "info",
24048
+ text: `daemon unavailable (${errorMessage(err)}) — running this session in-process. Toggle “Use daemon runtime” off to silence this.`
24049
+ });
24050
+ }
24051
+ else agentRef.current = buildAgent(session, key);
23026
24052
  if (extensionRegistrySwapKeyRef.current === null || extensionRegistrySwapKeyRef.current === extensionRegistry) pendingAgentRebuildRef.current = false;
23027
24053
  announcedSystemOverrideRef.current = null;
23028
24054
  announceProjectSystemOverride();
23029
24055
  const persistedPins = readPinnedSkills(session.metadata["zidane.activeSkills"]);
23030
24056
  setActiveSkillNames(persistedPins);
23031
- setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
24057
+ if (!daemonRef.current) setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
23032
24058
  setActiveFallback(null);
23033
24059
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
23034
24060
  setLastInputTokens(replayedTokens);
@@ -23050,7 +24076,7 @@ function AppShell() {
23050
24076
  lastProvider: key,
23051
24077
  lastSessionId: session.id
23052
24078
  });
23053
- const pending = pendingInteractionsFromTurns(session.turns);
24079
+ const pending = daemonRef.current ? [] : pendingInteractionsFromTurns(session.turns);
23054
24080
  if (pending.length > 0) {
23055
24081
  const collected = /* @__PURE__ */ new Map();
23056
24082
  const automatic = interactionModeRef.current === "skip";
@@ -23083,7 +24109,6 @@ function AppShell() {
23083
24109
  extensionRegistry
23084
24110
  ]);
23085
24111
  /** Ref mirror — same rationale as `refreshSessionsRef` above. */
23086
- const activateSessionRef = useRef(activateSession);
23087
24112
  activateSessionRef.current = activateSession;
23088
24113
  useEffect(() => {
23089
24114
  if (launch) return;
@@ -23124,6 +24149,7 @@ function AppShell() {
23124
24149
  launch
23125
24150
  ]);
23126
24151
  const [availableProviders, setAvailableProviders] = useState([]);
24152
+ availableProvidersRef.current = availableProviders;
23127
24153
  const refreshAvailableProviders = useCallback(() => {
23128
24154
  clearProviderUsageCache();
23129
24155
  const detected = detectAuth(config.paths.userDir, providerRegistry);
@@ -23200,6 +24226,26 @@ function AppShell() {
23200
24226
  setCwd,
23201
24227
  activateSession
23202
24228
  ]);
24229
+ const orchestrationSeenRef = useRef(settings.orchestration);
24230
+ useEffect(() => {
24231
+ if (orchestrationSeenRef.current === settings.orchestration) return;
24232
+ orchestrationSeenRef.current = settings.orchestration;
24233
+ if (settings.orchestration) return;
24234
+ const current = sessionRef.current;
24235
+ if (!current || !isMainSession(current.metadata)) return;
24236
+ setMainSessionId(null);
24237
+ refreshSessions().then(() => setScreen("sessions")).catch((err) => debugLog("leave main: refresh failed", err));
24238
+ }, [settings.orchestration, refreshSessions]);
24239
+ const daemonHealth = useDaemonHealth();
24240
+ const useDaemonSeenRef = useRef(settings.useDaemon);
24241
+ useEffect(() => {
24242
+ if (useDaemonSeenRef.current === settings.useDaemon) return;
24243
+ useDaemonSeenRef.current = settings.useDaemon;
24244
+ const current = sessionRef.current;
24245
+ const key = pickedRef.current?.provider.key;
24246
+ if (!current || !key) return;
24247
+ activateSession(current.id, key).catch((err) => debugLog("runtime switch: re-activation failed", err));
24248
+ }, [settings.useDaemon, activateSession]);
23203
24249
  useEffect(() => {
23204
24250
  const pending = pendingSessionSwitchRef.current;
23205
24251
  if (!pending || cwd !== pending.dir || !picked || busy) return;
@@ -23553,6 +24599,11 @@ function AppShell() {
23553
24599
  return;
23554
24600
  }
23555
24601
  const dropIdx = Math.min(idx, queue.length - 1);
24602
+ if (daemonRef.current) {
24603
+ const id = daemonQueueIdsRef.current[dropIdx];
24604
+ if (id) daemonRef.current.queueRemove(id);
24605
+ return;
24606
+ }
23556
24607
  messageQueueRef.current = queue.filter((_, i) => i !== dropIdx);
23557
24608
  setMessageQueue(messageQueueRef.current.slice());
23558
24609
  if (messageQueueRef.current.length === 0) setQueueSelectionIndex(null);
@@ -23571,10 +24622,15 @@ function AppShell() {
23571
24622
  * it up as a regular prompt, so the message is never silently dropped.
23572
24623
  */
23573
24624
  const pushSelectedQueuedMessage = useCallback(() => {
23574
- const agent = agentRef.current;
23575
- if (!agent) return;
23576
24625
  const idx = queueSelectionIndexRef.current;
23577
24626
  if (idx == null) return;
24627
+ if (daemonRef.current) {
24628
+ const pushId = daemonQueueIdsRef.current[Math.min(idx, daemonQueueIdsRef.current.length - 1)];
24629
+ if (pushId) daemonRef.current.queuePushNow(pushId);
24630
+ return;
24631
+ }
24632
+ const agent = agentRef.current;
24633
+ if (!agent) return;
23578
24634
  const queue = messageQueueRef.current;
23579
24635
  if (queue.length === 0) {
23580
24636
  setQueueSelectionIndex(null);
@@ -23787,6 +24843,18 @@ function AppShell() {
23787
24843
  lastAgent: id
23788
24844
  });
23789
24845
  modal.close();
24846
+ const session = sessionRef.current;
24847
+ if (daemonRef.current) {
24848
+ if (session && session.agentId !== id) {
24849
+ session.setAgentId(id);
24850
+ await daemonRef.current.setAgentId(id).catch((err) => debugLog("setAgentId over the wire failed", err));
24851
+ }
24852
+ return;
24853
+ }
24854
+ if (session && session.agentId !== id) {
24855
+ session.setAgentId(id);
24856
+ await session.save().catch(() => void 0);
24857
+ }
23790
24858
  if (picked && currentSession && !busy) await activateSession(currentSession.id, picked.provider.key);
23791
24859
  }, [
23792
24860
  liveAgentRegistry,
@@ -23834,6 +24902,15 @@ function AppShell() {
23834
24902
  const agent = agentRef.current;
23835
24903
  const session = sessionRef.current;
23836
24904
  if (!agent || !session || !picked) return;
24905
+ const persisted = settingsRef.current.useDaemon && !isDaemonDisabledByEnv() ? await (store.getMeta ? store.getMeta(session.id) : store.load(session.id)).catch(() => null) : null;
24906
+ const ownEnd = lastLocalRunEndRef.current;
24907
+ if (persisted?.status === "running" && Date.now() - persisted.updatedAt < 300 * 1e3 && !agent.isRunning && !(ownEnd?.id === session.id && persisted.updatedAt <= ownEnd.at + 2e3)) {
24908
+ stream.appendImmediate({
24909
+ kind: "error",
24910
+ text: "this session is running in another process (a daemon window?) — wait for it to finish, interrupt it there, or enable \"Use daemon runtime\" to share the run."
24911
+ });
24912
+ return;
24913
+ }
23837
24914
  if (eventsLengthRef.current > 0) stream.appendImmediate({
23838
24915
  kind: "separator",
23839
24916
  text: ""
@@ -23932,6 +25009,10 @@ function AppShell() {
23932
25009
  label: "run"
23933
25010
  }, errorMessage(err), { session });
23934
25011
  } finally {
25012
+ lastLocalRunEndRef.current = {
25013
+ id: session.id,
25014
+ at: Date.now()
25015
+ };
23935
25016
  chatReferenceTokensRef.current = [];
23936
25017
  stream.flushAndUpdate(finalizeStreamingMarkdown);
23937
25018
  }
@@ -24302,6 +25383,7 @@ function AppShell() {
24302
25383
  } finally {
24303
25384
  commandAbortRef.current = null;
24304
25385
  await flushCommandTurn();
25386
+ daemonRef.current?.touchSession();
24305
25387
  setCurrentSession((prev) => prev ? {
24306
25388
  ...prev,
24307
25389
  title: deriveSessionTitle(session.turns, session.metadata),
@@ -24417,6 +25499,7 @@ function AppShell() {
24417
25499
  }, `!${command}: ${errorMessage(err)}`, { session });
24418
25500
  } finally {
24419
25501
  commandAbortRef.current = null;
25502
+ daemonRef.current?.touchSession();
24420
25503
  setCurrentSession((prev) => prev ? {
24421
25504
  ...prev,
24422
25505
  turnCount: session.turns.length,
@@ -24468,7 +25551,7 @@ function AppShell() {
24468
25551
  await rebuildActiveAgentRef.current().catch((err) => debugLog("agent-rebuild: rebuildActiveAgent failed", err));
24469
25552
  continue;
24470
25553
  }
24471
- if (messageQueueRef.current.length > 0) {
25554
+ if (!daemonRef.current && messageQueueRef.current.length > 0) {
24472
25555
  const next = messageQueueRef.current[0];
24473
25556
  messageQueueRef.current = messageQueueRef.current.slice(1);
24474
25557
  setMessageQueue(messageQueueRef.current.slice());
@@ -24491,8 +25574,9 @@ function AppShell() {
24491
25574
  const onSubmitPrompt = useCallback((prompt, references, attachments) => {
24492
25575
  if (!prompt.trim() && attachments.length === 0) return;
24493
25576
  const agent = agentRef.current;
25577
+ const daemon = daemonRef.current;
24494
25578
  const session = sessionRef.current;
24495
- if (!agent || !session || !picked) return;
25579
+ if (!agent && !daemon || !session || !picked) return;
24496
25580
  const wasRunning = runningRef.current;
24497
25581
  if (!wasRunning) runningRef.current = true;
24498
25582
  transcriptRevealHoldRef.current = false;
@@ -24517,6 +25601,31 @@ function AppShell() {
24517
25601
  debugLog(`prompt transform (${contribution.extensionName}) threw`, err);
24518
25602
  }
24519
25603
  const resolvedReferences = await resolveCompletionSubmit(effectivePrompt, completionProviders, { cursor: effectivePrompt.length }).catch(() => references);
25604
+ if (daemon) {
25605
+ if (!wasRunning) runningRef.current = false;
25606
+ if (parseCommandInvocation(effectivePrompt, commandsRef.current) !== null || parseBashInvocation(effectivePrompt) !== null) {
25607
+ daemon.publishDraft("");
25608
+ await runPromptOrCommand(effectivePrompt, resolvedReferences, attachments);
25609
+ return;
25610
+ }
25611
+ const skillNames = Array.from(/* @__PURE__ */ new Set([...activeSkillNamesRef.current, ...uniqueSkillNamesFromReferences(resolvedReferences)]));
25612
+ try {
25613
+ await daemon.queueAdd(effectivePrompt, {
25614
+ ...skillNames.length > 0 ? { activatedSkills: [...skillNames] } : {},
25615
+ ...attachments.length > 0 ? { attachments: attachments.map((a) => ({
25616
+ mediaType: a.mediaType,
25617
+ data: a.content.toString("base64"),
25618
+ name: a.name
25619
+ })) } : {}
25620
+ });
25621
+ } catch (err) {
25622
+ stream.appendImmediate({
25623
+ kind: "error",
25624
+ text: errorMessage(err)
25625
+ });
25626
+ }
25627
+ return;
25628
+ }
24520
25629
  if (wasRunning) {
24521
25630
  messageQueueRef.current = [...messageQueueRef.current, {
24522
25631
  prompt: effectivePrompt,
@@ -24541,7 +25650,8 @@ function AppShell() {
24541
25650
  runPromptOrCommand,
24542
25651
  drainFollowUps,
24543
25652
  completionProviders,
24544
- extensionRegistry
25653
+ extensionRegistry,
25654
+ stream
24545
25655
  ]);
24546
25656
  useEffect(() => {
24547
25657
  const bridge = getTuiPromptBridge();
@@ -24709,6 +25819,10 @@ function AppShell() {
24709
25819
  const session = sessionRef.current;
24710
25820
  const p = pickedRef.current;
24711
25821
  if (!session || !p) return;
25822
+ if (daemonRef.current) {
25823
+ await daemonRef.current.rebuildAgent();
25824
+ return;
25825
+ }
24712
25826
  const stale = agentRef.current;
24713
25827
  const staleWarnLog = warnLogRef.current;
24714
25828
  const fresh = buildAgentRef.current(session, p.provider.key);
@@ -24745,7 +25859,7 @@ function AppShell() {
24745
25859
  return (provider, source) => {
24746
25860
  const descriptor = providerRegistry[provider.key];
24747
25861
  if (!descriptor) return void 0;
24748
- setPreferredCredential(config.paths.userDir, descriptor, source);
25862
+ if (setPreferredCredential(config.paths.userDir, descriptor, source) !== "promoted") return void 0;
24749
25863
  applyApiKeyEnv(config.paths.userDir, providerRegistry, { log: (message) => getTuiLogging()?.sink.emit({
24750
25864
  level: "debug",
24751
25865
  timestamp: Date.now(),
@@ -25049,6 +26163,13 @@ function AppShell() {
25049
26163
  transcriptSearchAbortRef.current = abortController;
25050
26164
  setTranscriptSearchSearching(true);
25051
26165
  (async () => {
26166
+ const remote = await daemonRef.current?.searchTranscript(query);
26167
+ if (remote) {
26168
+ if (generation !== transcriptSearchGenerationRef.current) return;
26169
+ setTranscriptSearchResults(remote);
26170
+ setTranscriptSearchSearching(false);
26171
+ return;
26172
+ }
25052
26173
  const cached = transcriptSearchIndexRef.current;
25053
26174
  const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
25054
26175
  if (cached && cached.index !== index) historySearchWorkerRef.current?.release(cached.index);
@@ -25167,6 +26288,72 @@ function AppShell() {
25167
26288
  const onTranscriptSearchNavigate = useCallback((direction) => {
25168
26289
  cycleSelectedTurn(direction, transcriptSearchTurnIds);
25169
26290
  }, [cycleSelectedTurn, transcriptSearchTurnIds]);
26291
+ /**
26292
+ * Daemon-mode preamble for HISTORY MUTATIONS (edit / delete). These
26293
+ * write the shared store through the window's load-time wrapper, which
26294
+ * LAGS the daemon's live appends — mutating it blind would clobber
26295
+ * turns other windows added since this one loaded. Refuses mid-run
26296
+ * (the daemon's agent is appending right now), refreshes the wrapper
26297
+ * to the store's current truth, and returns it. `null` = don't mutate.
26298
+ */
26299
+ const prepareHistoryMutation = useCallback(async () => {
26300
+ const session = sessionRef.current;
26301
+ if (!session) return null;
26302
+ if (!daemonRef.current) return session;
26303
+ if (runningRef.current) {
26304
+ stream.appendImmediate({
26305
+ kind: "error",
26306
+ text: "history can't be edited while a run is in flight — interrupt it first."
26307
+ });
26308
+ return null;
26309
+ }
26310
+ const fresh = await loadSession(store, session.id).catch(() => null);
26311
+ if (!fresh) {
26312
+ stream.appendImmediate({
26313
+ kind: "error",
26314
+ text: "could not reload the session for editing — try again."
26315
+ });
26316
+ return null;
26317
+ }
26318
+ sessionRef.current = fresh;
26319
+ return fresh;
26320
+ }, [store, stream]);
26321
+ /**
26322
+ * Daemon-mode epilogue: the daemon's cached agent still holds the
26323
+ * PRE-EDIT history in memory — retire it so the next run reloads the
26324
+ * edited turns (and can't save the stale wrapper back over them) —
26325
+ * then announce the out-of-band store change so every window
26326
+ * re-converges now.
26327
+ */
26328
+ /**
26329
+ * Guarded save for history mutations: the run check in
26330
+ * `prepareHistoryMutation` and this save are not atomic — a run
26331
+ * starting in between (another window's send, a queue drain, a wake)
26332
+ * appends turns this full-document save would erase. Re-check the
26333
+ * store's run marker IMMEDIATELY before writing and refuse the same
26334
+ * way the preamble does. Narrows the race to the store round-trip;
26335
+ * a store-level updatedAt CAS is the tracked full fix.
26336
+ */
26337
+ const saveHistoryMutation = useCallback(async (session) => {
26338
+ if (daemonRef.current) {
26339
+ const meta = await (store.getMeta?.(session.id) ?? store.load(session.id)).catch(() => null);
26340
+ if (meta?.status === "running" && typeof meta.updatedAt === "number" && Date.now() - meta.updatedAt < 300 * 1e3) {
26341
+ stream.appendImmediate({
26342
+ kind: "error",
26343
+ text: "a run started while editing — history was not modified. Interrupt the run and retry."
26344
+ });
26345
+ return false;
26346
+ }
26347
+ }
26348
+ await session.save();
26349
+ return true;
26350
+ }, [store, stream]);
26351
+ const finishHistoryMutation = useCallback(async () => {
26352
+ const daemon = daemonRef.current;
26353
+ if (!daemon) return;
26354
+ await daemon.rebuildAgent();
26355
+ daemon.touchSession();
26356
+ }, []);
25170
26357
  const onForkTurn = useCallback(async (turnId) => {
25171
26358
  const source = sessionRef.current;
25172
26359
  if (!source || !picked) return;
@@ -25177,6 +26364,7 @@ function AppShell() {
25177
26364
  const inheritedRuns = source.runs.filter((r) => referencedRunIds.has(r.id)).map((r) => ({ ...r }));
25178
26365
  const fork = await createSession({
25179
26366
  store,
26367
+ agentId: source.agentId ?? pickedAgentRef.current.id,
25180
26368
  ...source.projectRoot ? { projectRoot: source.projectRoot } : {}
25181
26369
  });
25182
26370
  const forkPersistDir = resolvePersistDir({
@@ -25209,17 +26397,18 @@ function AppShell() {
25209
26397
  cacheDir
25210
26398
  ]);
25211
26399
  const onDeleteTurn = useCallback(async (turnId) => {
25212
- const session = sessionRef.current;
26400
+ const session = await prepareHistoryMutation();
25213
26401
  if (!session) return;
25214
26402
  const nextTurns = deleteTurnSafely(session.turns, turnId);
25215
26403
  if (!nextTurns) return;
25216
26404
  session.setTurns(nextTurns);
25217
26405
  try {
25218
- await session.save();
26406
+ if (!await saveHistoryMutation(session)) return;
25219
26407
  } catch (err) {
25220
26408
  debugLog("delete: save failed", err);
25221
26409
  return;
25222
26410
  }
26411
+ await finishHistoryMutation();
25223
26412
  const nextEvents = capTranscriptEvents(eventsFromTurns(session.turns, session.runs));
25224
26413
  const nextIds = selectableTurnIds(nextEvents, settings);
25225
26414
  setEvents(nextEvents);
@@ -25237,7 +26426,7 @@ function AppShell() {
25237
26426
  } : prev);
25238
26427
  }, [settings, turnIds]);
25239
26428
  const onEditTurn = useCallback(async (turnId, newText) => {
25240
- const session = sessionRef.current;
26429
+ const session = await prepareHistoryMutation();
25241
26430
  if (!session) return;
25242
26431
  const turn = session.turns.find((t) => t.id === turnId);
25243
26432
  if (!turn) return;
@@ -25252,11 +26441,12 @@ function AppShell() {
25252
26441
  turn.content = updatedContent;
25253
26442
  session.setTurns([...session.turns]);
25254
26443
  try {
25255
- await session.save();
26444
+ if (!await saveHistoryMutation(session)) return;
25256
26445
  } catch (err) {
25257
26446
  debugLog("edit: save failed", err);
25258
26447
  return;
25259
26448
  }
26449
+ await finishHistoryMutation();
25260
26450
  const rebuilt = eventsFromTurns(session.turns, session.runs);
25261
26451
  const patched = replaceTranscriptTurnEvents(eventsRef.current, rebuilt, turnId);
25262
26452
  setEvents(patched ?? capTranscriptEvents(rebuilt));
@@ -25342,6 +26532,7 @@ function AppShell() {
25342
26532
  turns = loadedData.turns;
25343
26533
  metadataRecord = loadedData.metadata;
25344
26534
  }
26535
+ if (isMainSession(metadataRecord)) throw new Error("The Main session keeps its name.");
25345
26536
  const title = await generateSessionTitle({
25346
26537
  provider: descriptor.factory(),
25347
26538
  model: picked.model,
@@ -25784,13 +26975,27 @@ function AppShell() {
25784
26975
  }
25785
26976
  if (matchesBinding(key, keybindings.openTodos) && screen === "chat" && currentSession) {
25786
26977
  modal.open(/* @__PURE__ */ jsx$1(TodosModal, {
25787
- session: sessionRef.current,
25788
- agent: agentRef.current,
26978
+ getSession: () => sessionRef.current,
26979
+ subscribe: subscribeTodos,
26980
+ onCommit: commitTodos,
25789
26981
  onSteer: (message) => onSubmitPrompt(message, [], [])
25790
26982
  }));
25791
26983
  return;
25792
26984
  }
25793
26985
  if (matchesBinding(key, keybindings.openContextPanel) && screen === "chat" && picked) {
26986
+ if (daemonRef.current) {
26987
+ const controller = daemonRef.current;
26988
+ controller.fetchContextBreakdown().then((breakdown) => {
26989
+ if (breakdown && daemonRef.current === controller) {
26990
+ const modelUsage = usageByModel(sessionRef.current?.runs ?? []);
26991
+ modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, {
26992
+ breakdown,
26993
+ modelUsage
26994
+ }));
26995
+ }
26996
+ }).catch((err) => debugLog("daemon contextBreakdown failed", err));
26997
+ return;
26998
+ }
25794
26999
  const agent = agentRef.current;
25795
27000
  if (!agent) return;
25796
27001
  const descriptor = providerRegistry[picked.provider.key];
@@ -25811,6 +27016,15 @@ function AppShell() {
25811
27016
  }).catch((err) => debugLog("getContextBreakdown failed", err));
25812
27017
  return;
25813
27018
  }
27019
+ if (matchesBinding(key, keybindings.openMainSession) && settings.orchestration) {
27020
+ ensureMainSession().then((mainId) => {
27021
+ if (mainId) {
27022
+ setMainSessionId(mainId);
27023
+ onSwitchSession(mainId);
27024
+ }
27025
+ }).catch((err) => debugLog("ensureMainSession failed", err));
27026
+ return;
27027
+ }
25814
27028
  if (matchesBinding(key, keybindings.openUsagePanel) && screen === "chat") {
25815
27029
  modal.open(/* @__PURE__ */ jsx$1(UsagePanelModal, {
25816
27030
  dataDir: config.paths.userDir,
@@ -25846,12 +27060,17 @@ function AppShell() {
25846
27060
  modal.open(/* @__PURE__ */ jsx$1(CancelToolModal, {
25847
27061
  inFlight: snapshot,
25848
27062
  onCancel: (entry, reason) => {
27063
+ if (daemonRef.current) return daemonRef.current.cancelWork(entry.callId, reason);
25849
27064
  const agent = agentRef.current;
25850
27065
  if (!agent) return false;
25851
27066
  if (entry.kind === "task") return agent.killBackgroundTask(entry.callId);
25852
27067
  return agent.cancelTool(entry.callId, reason);
25853
27068
  },
25854
27069
  onCancelAll: () => {
27070
+ if (daemonRef.current) {
27071
+ daemonRef.current.cancelWork("all");
27072
+ return;
27073
+ }
25855
27074
  const agent = agentRef.current;
25856
27075
  if (!agent) return;
25857
27076
  for (const entry of snapshot) if (entry.kind === "task") agent.killBackgroundTask(entry.callId);
@@ -25964,19 +27183,13 @@ function AppShell() {
25964
27183
  const updateHint = useMemo(() => buildUpdateHint(updateStatus, { color: COLOR.accent }), [updateStatus, COLOR.accent]);
25965
27184
  const cacheBreakHint = useMemo(() => {
25966
27185
  if (!activeCacheBreak) return null;
25967
- const rebilled = activeCacheBreak.rebilledTokens >= 1e3 ? `${(activeCacheBreak.rebilledTokens / 1e3).toFixed(1)}k` : String(activeCacheBreak.rebilledTokens);
25968
- const cost = activeCacheBreak.costUsd !== void 0 ? ` · ~$${activeCacheBreak.costUsd < .01 ? "<0.01" : activeCacheBreak.costUsd.toFixed(2)}` : "";
25969
- if (activeCacheBreak.changed.length === 0) return {
25970
- key: "⚡",
25971
- keyColor: COLOR.dim,
25972
- label: `cache expired · +${rebilled} tok${cost}`,
25973
- labelColor: COLOR.dim
25974
- };
27186
+ const chip = cacheBreakChip(activeCacheBreak);
27187
+ const color = chip.tone === "warn" ? COLOR.warn : COLOR.dim;
25975
27188
  return {
25976
- key: "⚡",
25977
- keyColor: COLOR.warn,
25978
- label: `cache break · ${activeCacheBreak.changed.join(",")} · +${rebilled} tok${cost}`,
25979
- labelColor: COLOR.warn
27189
+ key: chip.glyph,
27190
+ keyColor: color,
27191
+ label: chip.label,
27192
+ labelColor: color
25980
27193
  };
25981
27194
  }, [
25982
27195
  activeCacheBreak,
@@ -26136,7 +27349,7 @@ function AppShell() {
26136
27349
  initialConfigureKey: authConfigureKey
26137
27350
  }),
26138
27351
  screen === "sessions" && /* @__PURE__ */ jsx$1(SessionsScreen, {
26139
- sessions,
27352
+ sessions: sessionsScope === "active" ? sessions.filter((meta) => (sessionPresence.get(meta.id)?.windows ?? 0) > 0) : sessions,
26140
27353
  currentId: currentSession?.id ?? null,
26141
27354
  focusedSessionId,
26142
27355
  onPick: onSwitchSession,
@@ -26147,8 +27360,11 @@ function AppShell() {
26147
27360
  contentSearchProgress: sessionContentSearchProgress,
26148
27361
  onSearchQueryChange: onSessionSearchQueryChange,
26149
27362
  showAllProjects: sessionsShowAll,
27363
+ scopeLabel: sessionsScope === "active" ? "active windows" : sessionsScope === "all" ? "all projects" : void 0,
27364
+ presence: sessionPresence,
26150
27365
  onToggleScope: toggleSessionsScope,
26151
- currentProjectRoot: projectDir
27366
+ currentProjectRoot: projectDir,
27367
+ mainSessionId: settings.orchestration ? mainSessionId : null
26152
27368
  }),
26153
27369
  screen === "chat" && /* @__PURE__ */ jsx$1(ChatScreen, {
26154
27370
  cwd,
@@ -26161,6 +27377,13 @@ function AppShell() {
26161
27377
  onEnterQueueFromEmptyPrompt: enterQueueSelectionFromEmptyPrompt,
26162
27378
  settings,
26163
27379
  onSubmit: onSubmitPrompt,
27380
+ onDraftChange: onPromptDraftChange,
27381
+ remoteDraft,
27382
+ onDraftAttachmentsChange: onPromptDraftAttachmentsChange,
27383
+ remoteDraftAttachments,
27384
+ remoteSearchQuery,
27385
+ remoteSuggest,
27386
+ resolveGhostRemote,
26164
27387
  session: currentSession,
26165
27388
  pending: pendingApproval,
26166
27389
  onApproval: resolveHead,
@@ -26178,6 +27401,7 @@ function AppShell() {
26178
27401
  selectedTurnId,
26179
27402
  onSelectTurn: onTranscriptSelectTurn,
26180
27403
  onOpenTurn: onTranscriptOpenTurn,
27404
+ onOpenSessionEvent: settings.orchestration ? onSwitchSession : void 0,
26181
27405
  transcriptSearchMode,
26182
27406
  transcriptSearchQuery,
26183
27407
  transcriptSearchEvents,
@@ -26196,6 +27420,7 @@ function AppShell() {
26196
27420
  }),
26197
27421
  /* @__PURE__ */ jsx$1(Footer, {
26198
27422
  hints,
27423
+ daemonHealth: settings.useDaemon && !isDaemonDisabledByEnv() ? daemonHealth : null,
26199
27424
  context: contextUsage,
26200
27425
  cost: footerCost,
26201
27426
  status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : retryInfo && busy ? "retrying" : busy ? "busy" : null,
@@ -27638,6 +28863,8 @@ function SkillsSettingsModal({ catalog, onRefresh }) {
27638
28863
  * undefined behavior. We bail loudly instead.
27639
28864
  */
27640
28865
  let runTuiInvoked = false;
28866
+ /** `ZIDANE_DEBUG`-gated sink for the rejection net above `runTui`. */
28867
+ const tuiDebug = createDebugLog("tui");
27641
28868
  /**
27642
28869
  * Boot a full chat TUI with sensible defaults. **Does not return** under
27643
28870
  * normal use — it terminates the host process via `process.exit(0)` once
@@ -27726,6 +28953,9 @@ async function runTui(options = {}) {
27726
28953
  if (runTuiInvoked) throw new Error("runTui() can only be invoked once per process. Compose `<App config={resolveConfig(...)} />` against your own renderer if you need to run multiple TUIs in the same lifetime.");
27727
28954
  runTuiInvoked = true;
27728
28955
  bootTick("runTui:enter");
28956
+ process.on("unhandledRejection", (reason) => {
28957
+ tuiDebug("unhandled rejection", reason);
28958
+ });
27729
28959
  registerTreeSitterParsers();
27730
28960
  bootTick("runTui:after-registerTreeSitterParsers");
27731
28961
  initTreeSitterWorker().catch((err) => {
@@ -27767,6 +28997,7 @@ async function runTui(options = {}) {
27767
28997
  const hud = initTuiHud();
27768
28998
  const promptBridge = initTuiPromptBridge();
27769
28999
  const hostRuntime = initTuiHostRuntime();
29000
+ const bus = initTuiBus();
27770
29001
  const extensionRegistry = await discoverAndBuildExtensionRegistry({
27771
29002
  ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
27772
29003
  prefix,
@@ -27778,6 +29009,7 @@ async function runTui(options = {}) {
27778
29009
  hud,
27779
29010
  promptBridge,
27780
29011
  hostRuntime,
29012
+ bus,
27781
29013
  readSettings: readLiveSettings
27782
29014
  });
27783
29015
  bootTick("runTui:after-buildExtensionRegistry");