@asm-agent/coding-agent 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -7
  3. package/dist/asm-agent-runtime/src/__pycache__/asm_cm_driver.cpython-313.pyc +0 -0
  4. package/dist/asm-agent-runtime/src/__pycache__/phase76_address_head.cpython-313.pyc +0 -0
  5. package/dist/asm-agent-runtime/src/rlm/__init__.py +1 -1
  6. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-312.pyc +0 -0
  7. package/dist/asm-agent-runtime/src/rlm/__pycache__/__init__.cpython-313.pyc +0 -0
  8. package/dist/asm-agent-runtime/src/rlm/__pycache__/_winjob.cpython-313.pyc +0 -0
  9. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-312.pyc +0 -0
  10. package/dist/asm-agent-runtime/src/rlm/__pycache__/bash.cpython-313.pyc +0 -0
  11. package/dist/asm-agent-runtime/src/rlm/__pycache__/harness.cpython-313.pyc +0 -0
  12. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-312.pyc +0 -0
  13. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp.cpython-313.pyc +0 -0
  14. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-312.pyc +0 -0
  15. package/dist/asm-agent-runtime/src/rlm/__pycache__/mcp_base.cpython-313.pyc +0 -0
  16. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-312.pyc +0 -0
  17. package/dist/asm-agent-runtime/src/rlm/__pycache__/repl.cpython-313.pyc +0 -0
  18. package/dist/asm-agent-runtime/src/rlm/__pycache__/skill.cpython-313.pyc +0 -0
  19. package/dist/asm-agent-runtime/src/rlm/bash.py +94 -31
  20. package/dist/asm-agent-runtime/src/rlm/repl.md +3 -3
  21. package/dist/asm-agent-runtime/src/rlm/repl.py +46 -45
  22. package/dist/asm-agent-runtime/test/__pycache__/test_agent_message_skill.cpython-313.pyc +0 -0
  23. package/dist/asm-agent-runtime/test/__pycache__/test_bash.cpython-313.pyc +0 -0
  24. package/dist/asm-agent-runtime/test/__pycache__/test_harness.cpython-313.pyc +0 -0
  25. package/dist/asm-agent-runtime/test/__pycache__/test_mcp.cpython-313.pyc +0 -0
  26. package/dist/asm-agent-runtime/test/__pycache__/test_mcp_base.cpython-313.pyc +0 -0
  27. package/dist/asm-agent-runtime/test/__pycache__/test_repl.cpython-313.pyc +0 -0
  28. package/dist/asm-agent-runtime/test/__pycache__/test_subagent_registry.cpython-313.pyc +0 -0
  29. package/dist/asm-agent-runtime/test/__pycache__/test_winjob.cpython-313.pyc +0 -0
  30. package/dist/asm-agent-runtime/test/test_bash.py +74 -19
  31. package/dist/asm-agent-runtime/test/test_repl.py +67 -3
  32. package/dist/asm-agent-runtime/uv.lock +446 -155
  33. package/dist/bundle/{anthropic-XSMQHGYB.js → anthropic-ZOZNPFXS.js} +2 -2
  34. package/dist/bundle/{azure-openai-responses-XLQAIKKH.js → azure-openai-responses-TDHCU647.js} +2 -2
  35. package/dist/bundle/{bundled-modules-5OCN73BM.js → bundled-modules-Q2YODC4B.js} +3 -3
  36. package/dist/bundle/{chunk-IAQLPSMN.js → chunk-AMKLNEAK.js} +47 -47
  37. package/dist/bundle/{chunk-2LPSALBF.js → chunk-FPY7FFJS.js} +978 -1821
  38. package/dist/bundle/{chunk-E3YE2Q7A.js → chunk-G6SC4YYW.js} +4973 -1926
  39. package/dist/bundle/{chunk-YPNK4MRU.js → chunk-QLHAP3FT.js} +1 -1
  40. package/dist/bundle/{cli-main-QYX5R3LF.js → cli-main-277AG2BR.js} +3 -3
  41. package/dist/bundle/cli.js +1 -1
  42. package/dist/bundle/{google-ZXJEPWJA.js → google-Q3AJWUOY.js} +1 -1
  43. package/dist/bundle/{google-vertex-WAF3RIJ6.js → google-vertex-2CSNOUBS.js} +1 -1
  44. package/dist/bundle/{main-EOUAQCJH.js → main-TNXWJXIU.js} +3 -3
  45. package/dist/bundle/{mistral-GDZXSXRS.js → mistral-XRYJGLLG.js} +1 -1
  46. package/dist/bundle/{openai-codex-responses-7XMPW4LG.js → openai-codex-responses-C6BRO2BN.js} +2 -2
  47. package/dist/bundle/{openai-completions-LNSVGDMF.js → openai-completions-DNXCK2JW.js} +4 -4
  48. package/dist/bundle/{openai-responses-MFZOBMIT.js → openai-responses-BEMWVIXE.js} +2 -2
  49. package/dist/config.d.ts +0 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +0 -3
  52. package/dist/config.js.map +1 -1
  53. package/dist/core/agent-session-config.d.ts +1 -2
  54. package/dist/core/agent-session-config.d.ts.map +1 -1
  55. package/dist/core/agent-session-config.js +0 -3
  56. package/dist/core/agent-session-config.js.map +1 -1
  57. package/dist/core/agent-session-runtime.d.ts.map +1 -1
  58. package/dist/core/agent-session-runtime.js +0 -8
  59. package/dist/core/agent-session-runtime.js.map +1 -1
  60. package/dist/core/agent-session-services.d.ts +0 -2
  61. package/dist/core/agent-session-services.d.ts.map +1 -1
  62. package/dist/core/agent-session-services.js +0 -22
  63. package/dist/core/agent-session-services.js.map +1 -1
  64. package/dist/core/agent-session.d.ts +8 -1
  65. package/dist/core/agent-session.d.ts.map +1 -1
  66. package/dist/core/agent-session.js +100 -22
  67. package/dist/core/agent-session.js.map +1 -1
  68. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  69. package/dist/core/kernel/bootstrap.js +1 -1
  70. package/dist/core/kernel/bootstrap.js.map +1 -1
  71. package/dist/core/kernel/repl-manager.d.ts +32 -8
  72. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  73. package/dist/core/kernel/repl-manager.js +401 -102
  74. package/dist/core/kernel/repl-manager.js.map +1 -1
  75. package/dist/core/kernel/shared.d.ts +11 -7
  76. package/dist/core/kernel/shared.d.ts.map +1 -1
  77. package/dist/core/kernel/shared.js +2 -5
  78. package/dist/core/kernel/shared.js.map +1 -1
  79. package/dist/core/mcp/mcp-manager.d.ts +4 -2
  80. package/dist/core/mcp/mcp-manager.d.ts.map +1 -1
  81. package/dist/core/mcp/mcp-manager.js +9 -7
  82. package/dist/core/mcp/mcp-manager.js.map +1 -1
  83. package/dist/core/model-registry.d.ts.map +1 -1
  84. package/dist/core/model-registry.js +1 -1
  85. package/dist/core/model-registry.js.map +1 -1
  86. package/dist/core/ollama-models.d.ts +3 -1
  87. package/dist/core/ollama-models.d.ts.map +1 -1
  88. package/dist/core/ollama-models.js +31 -10
  89. package/dist/core/ollama-models.js.map +1 -1
  90. package/dist/core/prime-inference-auth.d.ts +0 -9
  91. package/dist/core/prime-inference-auth.d.ts.map +1 -1
  92. package/dist/core/prime-inference-auth.js +0 -50
  93. package/dist/core/prime-inference-auth.js.map +1 -1
  94. package/dist/core/provider-display-names.d.ts.map +1 -1
  95. package/dist/core/provider-display-names.js +0 -1
  96. package/dist/core/provider-display-names.js.map +1 -1
  97. package/dist/core/session-manager.d.ts +0 -4
  98. package/dist/core/session-manager.d.ts.map +1 -1
  99. package/dist/core/session-manager.js +0 -22
  100. package/dist/core/session-manager.js.map +1 -1
  101. package/dist/core/settings-manager.d.ts +4 -16
  102. package/dist/core/settings-manager.d.ts.map +1 -1
  103. package/dist/core/settings-manager.js +13 -43
  104. package/dist/core/settings-manager.js.map +1 -1
  105. package/dist/core/slash-commands.d.ts.map +1 -1
  106. package/dist/core/slash-commands.js +0 -5
  107. package/dist/core/slash-commands.js.map +1 -1
  108. package/dist/core/tools/acp-mcp.d.ts +6 -0
  109. package/dist/core/tools/acp-mcp.d.ts.map +1 -0
  110. package/dist/core/tools/acp-mcp.js +86 -0
  111. package/dist/core/tools/acp-mcp.js.map +1 -0
  112. package/dist/core/tools/index.d.ts +1 -0
  113. package/dist/core/tools/index.d.ts.map +1 -1
  114. package/dist/core/tools/index.js +1 -0
  115. package/dist/core/tools/index.js.map +1 -1
  116. package/dist/core/tools/ipython.d.ts.map +1 -1
  117. package/dist/core/tools/ipython.js +9 -4
  118. package/dist/core/tools/ipython.js.map +1 -1
  119. package/dist/main.d.ts.map +1 -1
  120. package/dist/main.js +2 -11
  121. package/dist/main.js.map +1 -1
  122. package/dist/modes/acp/acp-mode.d.ts.map +1 -1
  123. package/dist/modes/acp/acp-mode.js +1 -1
  124. package/dist/modes/acp/acp-mode.js.map +1 -1
  125. package/dist/modes/agent-connection/daemon-agent-connection.d.ts +0 -2
  126. package/dist/modes/agent-connection/daemon-agent-connection.d.ts.map +1 -1
  127. package/dist/modes/agent-connection/daemon-agent-connection.js +0 -2
  128. package/dist/modes/agent-connection/daemon-agent-connection.js.map +1 -1
  129. package/dist/modes/agents-view/agents-view-mode.d.ts.map +1 -1
  130. package/dist/modes/agents-view/agents-view-mode.js +0 -19
  131. package/dist/modes/agents-view/agents-view-mode.js.map +1 -1
  132. package/dist/modes/daemon/daemon-mode.d.ts +1 -10
  133. package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
  134. package/dist/modes/daemon/daemon-mode.js +134 -274
  135. package/dist/modes/daemon/daemon-mode.js.map +1 -1
  136. package/dist/modes/daemon/daemon-protocol.d.ts +11 -5
  137. package/dist/modes/daemon/daemon-protocol.d.ts.map +1 -1
  138. package/dist/modes/daemon/daemon-protocol.js +6 -8
  139. package/dist/modes/daemon/daemon-protocol.js.map +1 -1
  140. package/dist/modes/daemon/daemon-supervisor.d.ts +0 -3
  141. package/dist/modes/daemon/daemon-supervisor.d.ts.map +1 -1
  142. package/dist/modes/daemon/daemon-supervisor.js +18 -52
  143. package/dist/modes/daemon/daemon-supervisor.js.map +1 -1
  144. package/dist/modes/daemon/daemon-worker-protocol.d.ts +1 -6
  145. package/dist/modes/daemon/daemon-worker-protocol.d.ts.map +1 -1
  146. package/dist/modes/daemon/daemon-worker-protocol.js +0 -2
  147. package/dist/modes/daemon/daemon-worker-protocol.js.map +1 -1
  148. package/dist/modes/interactive/auth-flows.d.ts +0 -2
  149. package/dist/modes/interactive/auth-flows.d.ts.map +1 -1
  150. package/dist/modes/interactive/auth-flows.js +1 -101
  151. package/dist/modes/interactive/auth-flows.js.map +1 -1
  152. package/dist/modes/interactive/components/assistant-message.d.ts +6 -1
  153. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  154. package/dist/modes/interactive/components/assistant-message.js +13 -3
  155. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  156. package/dist/modes/interactive/components/footer.d.ts +1 -1
  157. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  158. package/dist/modes/interactive/components/footer.js +2 -2
  159. package/dist/modes/interactive/components/footer.js.map +1 -1
  160. package/dist/modes/interactive/components/mermaid.d.ts +12 -0
  161. package/dist/modes/interactive/components/mermaid.d.ts.map +1 -0
  162. package/dist/modes/interactive/components/mermaid.js +63 -0
  163. package/dist/modes/interactive/components/mermaid.js.map +1 -0
  164. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  165. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/settings-selector.js +10 -0
  167. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  168. package/dist/modes/interactive/feature-hints.d.ts.map +1 -1
  169. package/dist/modes/interactive/feature-hints.js +0 -4
  170. package/dist/modes/interactive/feature-hints.js.map +1 -1
  171. package/dist/modes/interactive/interactive-mode.d.ts +1 -7
  172. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  173. package/dist/modes/interactive/interactive-mode.js +23 -279
  174. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  175. package/dist/utils/version-check.d.ts.map +1 -1
  176. package/dist/utils/version-check.js +1 -1
  177. package/dist/utils/version-check.js.map +1 -1
  178. package/docs/settings.md +1 -31
  179. package/docs/usage.md +1 -4
  180. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  181. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  182. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  183. package/examples/extensions/sandbox/package-lock.json +2 -2
  184. package/examples/extensions/sandbox/package.json +1 -1
  185. package/examples/extensions/with-deps/package-lock.json +2 -2
  186. package/examples/extensions/with-deps/package.json +1 -1
  187. package/package.json +6 -5
  188. package/dist/core/agent-traces.d.ts +0 -130
  189. package/dist/core/agent-traces.d.ts.map +0 -1
  190. package/dist/core/agent-traces.js +0 -746
  191. package/dist/core/agent-traces.js.map +0 -1
  192. package/dist/core/telemetry.d.ts +0 -92
  193. package/dist/core/telemetry.d.ts.map +0 -1
  194. package/dist/core/telemetry.js +0 -633
  195. package/dist/core/telemetry.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"agents-view-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/agents-view/agents-view-mode.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { resolve as resolvePath } from \"node:path\";\nimport {\n\ttype AutocompleteProvider,\n\tCombinedAutocompleteProvider,\n\ttype Component,\n\tclippedFullscreenDockHeight,\n\ttype Focusable,\n\tProcessTerminal,\n\tsetKeybindings,\n\tTUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n\twrapTextWithAnsi,\n} from \"@asm-agent/tui\";\nimport { APP_TITLE, appendRotatingLog, getAgentDir, getClientErrorLogPath, VERSION } from \"../../config.js\";\nimport type { AgentSessionRuntimeConfig } from \"../../core/agent-session-config.js\";\nimport { KeybindingsManager } from \"../../core/keybindings.js\";\nimport { SessionManager } from \"../../core/session-manager.js\";\nimport {\n\tBUILTIN_SLASH_COMMANDS,\n\tisBuiltinSlashCommandName,\n\tisSessionSlashCommandName,\n\tparseSlashCommand,\n\tresolveBuiltinSlashCommandName,\n} from \"../../core/slash-commands.js\";\nimport { canonicalizePath } from \"../../utils/paths.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { DaemonAgentConnection } from \"../agent-connection/daemon-agent-connection.js\";\nimport type { AgentConnectionHeartbeat, AgentConnectionSavedSessionInfo } from \"../agent-connection/types.js\";\nimport { DaemonClient, getDaemonSocketCloseReason } from \"../daemon/daemon-client.js\";\nimport {\n\tcollectDaemonClientEnv,\n\ttype DaemonClosingReason,\n\ttype DaemonCommand,\n\ttype DaemonResponse,\n\tisUnknownDaemonCommandError,\n} from \"../daemon/daemon-protocol.js\";\nimport { resolveAttachModelFallbackMessage, type SessionSummary } from \"../daemon/daemon-session-list.js\";\nimport { listDaemonHeartbeats } from \"../daemon/heartbeat-catalog.js\";\nimport {\n\ttype DaemonSavedSessionCatalogContext,\n\tdeleteDaemonSavedSession,\n\tlistDaemonSavedSessions,\n\trenameDaemonSavedSession,\n} from \"../daemon/saved-session-catalog.js\";\nimport { CustomEditor } from \"../interactive/components/custom-editor.js\";\nimport { keyText } from \"../interactive/components/keybinding-hints.js\";\nimport { BrandSplashHeader, InteractiveMode } from \"../interactive/interactive-mode.js\";\nimport type { InteractiveModeUiServices } from \"../interactive/interactive-mode-services.js\";\nimport { ClientPromptStashStore } from \"../interactive/prompt-stash-state.js\";\nimport {\n\tgetEditorTheme,\n\tinitTheme,\n\tonThemeChange,\n\tsetRegisteredThemes,\n\tstopThemeWatcher,\n\ttheme,\n} from \"../interactive/theme/theme.js\";\nimport { WORKING_ICON_INTERVAL_MS, workingIconFrame } from \"../interactive/theme/working-icon.js\";\nimport {\n\tformatPackageUpdateNotice,\n\tformatTmuxWarningNotice,\n\tformatUpdateAvailableNotice,\n\tgatherStartupNotices,\n\ttype StartupNotices,\n} from \"../shared/startup-notices.js\";\nimport {\n\ttype AgentsViewRow,\n\ttype AgentsViewScopeFrame,\n\ttype AgentsViewScopeKey,\n\ttype AgentsViewSection,\n\ttype AgentsViewSelectionKey,\n\tbuildAgentsViewRows,\n\tbuildUnifiedSessionIndex,\n\tcreateUnattachableChildOpenResult,\n\tfilterUnifiedSessions,\n\tformatHeartbeatBadge,\n\tgetAgentsViewSelectionKey,\n\tgetAgentsViewSessionTitle,\n\tgetAgentsViewSummaryIdentity as getSummaryIdentity,\n\tgetUnifiedSessionAncestorSessionIds,\n\thasUnifiedSessionChildren,\n\tisSubagentSummary,\n\tmigrateAgentsViewIdentitySet,\n\treconcileUnifiedSessions,\n\tresolveAgentsViewLeftResult,\n\tresolveAgentsViewScopeFrames,\n\tresolveAgentsViewSelectionState,\n\tscopeToSessionSubtree,\n\tsectionTitle,\n\tshouldApplyScopeResolution,\n\tshouldShowAgentsViewSession,\n\tsummaryForUnifiedRecord,\n\ttransitionAgentsViewScope,\n\ttype UnifiedSessionIndex,\n\ttype UnifiedSessionRecord,\n} from \"./agents-view-state.js\";\nimport { matchesSearchText } from \"./session-view-search.js\";\n\nconst POLL_INTERVAL_MS = 1000;\nconst HEARTBEAT_POLL_INTERVAL_MS = 15000;\nconst RECONNECT_TIMEOUT_MS = 120000;\nconst RECONNECT_RETRY_MS = 1000;\nconst EXIT_HINT_DURATION_MS = 2000;\nconst DELETE_CONFIRM_DURATION_MS = 2000;\nconst STATUS_MESSAGE_DURATION_MS = 4500;\nconst SEARCH_PROMPT_PLACEHOLDER = \"Search sessions\";\nconst REPLY_PROMPT_FALLBACK_PLACEHOLDER = \"Write a reply to this agent\";\nconst RESUME_PROMPT_PLACEHOLDER = \"Write a prompt to resume this session\";\nconst COMPLETED_ROW_ICON = \"✓\";\nconst NEEDS_INPUT_ROW_ICON = \"●\";\nconst SELECTED_ROW_MARKER = \"\\0agents-view-selected-row\\0\";\nconst CODE_ROW_MARKER = \"\\0agents-view-code-row\\0\";\n\nexport interface AgentsViewModeOptions {\n\tsocketPath?: string;\n\tconfig: AgentSessionRuntimeConfig;\n\tuiServices: InteractiveModeUiServices;\n\tcreateUiServicesForSession?: (summary: SessionSummary) => Promise<InteractiveModeUiServices>;\n\tmigratedProviders?: string[];\n\tmodelFallbackMessage?: string;\n\tstartupModelId?: string;\n\tverbose?: boolean;\n\trecoverDaemon?: () => Promise<void>;\n\treconnectTimeoutMs?: number;\n\tpromptStashStore?: ClientPromptStashStore;\n\tinitialSession?: SessionSummary;\n\t/** When set, the first view is rooted at this session's direct children. */\n\tinitialScopeKey?: AgentsViewScopeKey;\n}\n\nexport type AgentsViewRunResult =\n\t| { type: \"exit\" }\n\t| {\n\t\t\ttype: \"scope_back\";\n\t\t\tselection: SessionSummary;\n\t\t\texpandedAncestorSessionIds: string[];\n\t\t\treturnChat?: SessionSummary;\n\t\t\thasChildren: boolean;\n\t }\n\t| {\n\t\t\ttype: \"open\";\n\t\t\tsummary: SessionSummary;\n\t\t\t/** Row restored after chat closes; differs from summary only for an unattachable-child fallback. */\n\t\t\tselection?: SessionSummary;\n\t\t\texpandedAncestorSessionIds?: string[];\n\t\t\thasChildren?: boolean;\n\t\t\tstatusMessage?: string;\n\t };\nexport type AgentsViewPersistentState = {\n\tselectedRowIdentity?: string;\n\tbackSession?: SessionSummary;\n\tscopeFrames?: AgentsViewScopeFrame[];\n\tscopeRootSummary?: SessionSummary;\n\tselectedSessionKey?: AgentsViewSelectionKey;\n\t// Ancestor chain to re-expand on return to a nested agent. Kept by sessionId,\n\t// not row identity, so it survives an active→persisted identity flip.\n\tpendingExpandedAncestorSessionIds?: string[];\n\texpandedSubagentParents?: Set<string>;\n\tprogramShownParents?: Set<string>;\n\tstatusMessage?: string;\n\t// Gathered once and reused across agents-view instances so the notices survive\n\t// re-entry and render the moment they resolve, even if the first view was left early.\n\tstartupNotices?: StartupNotices;\n\tstartupNoticesPromise?: Promise<StartupNotices>;\n\tquery?: string;\n\tsavedSessions?: AgentConnectionSavedSessionInfo[];\n\tlastSuccessfulSavedSessions?: AgentConnectionSavedSessionInfo[];\n\tlastSuccessfulLiveSummaries?: SessionSummary[];\n\tsavedCatalogGeneration?: number;\n\theartbeats?: AgentConnectionHeartbeat[];\n};\n\ntype PromptCommand = Extract<DaemonCommand, { type: \"prompt\" }>;\ntype PendingDeleteAgent = {\n\tidentity: string;\n\tactiveSessionId?: string;\n\tsessionFile?: string;\n\tsummary: SessionSummary;\n\tstopped: boolean;\n};\ntype PendingKillSubagent = {\n\tidentity: string;\n\trootActiveSessionId: string;\n\tchildId: string;\n};\n\nexport async function resolveAgentsViewSessionUiServices(\n\toptions: Pick<AgentsViewModeOptions, \"createUiServicesForSession\" | \"uiServices\">,\n\tsummary: SessionSummary,\n): Promise<InteractiveModeUiServices> {\n\treturn options.createUiServicesForSession ? await options.createUiServicesForSession(summary) : options.uiServices;\n}\n\n// Stripping cwd opens the session in its own stored directory; overrideCwd is\n// sent when that directory no longer exists so the daemon doesn't reject it.\nexport function createAgentsViewResumeConfig(\n\tconfig: AgentSessionRuntimeConfig,\n\toverrideCwd?: string,\n): AgentSessionRuntimeConfig {\n\tconst resumeConfig: AgentSessionRuntimeConfig = { ...config };\n\tif (overrideCwd) {\n\t\tresumeConfig.cwd = overrideCwd;\n\t} else {\n\t\tdelete resumeConfig.cwd;\n\t}\n\treturn resumeConfig;\n}\n\nexport function createAgentsViewListCommand(): Extract<DaemonCommand, { type: \"list\" }> {\n\t// Omitting `all` returns daemon-resident sessions only; on-disk ones come back\n\t// through the view's saved-session catalog.\n\treturn { type: \"list\" };\n}\n\nexport function resolveAgentsViewActiveSummaryForPath(\n\tsessionPath: string,\n\tsummaries: readonly SessionSummary[],\n): SessionSummary | undefined {\n\tconst selectedPath = resolvePath(canonicalizePath(sessionPath));\n\treturn summaries.find(\n\t\t(summary) =>\n\t\t\tsummary.activeSessionId !== undefined &&\n\t\t\tsummary.sessionFile !== undefined &&\n\t\t\tresolvePath(canonicalizePath(summary.sessionFile)) === selectedPath,\n\t);\n}\n\n// Status messages render in a single-row hint slot below the editor; embedded\n// newlines would make that row taller than the layout accounts for and overlap\n// the input, so flatten all whitespace runs to single spaces.\nexport function formatAgentsViewStatusLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim();\n}\n\nexport function combineAgentsViewStartupNotices(...notices: readonly (string | undefined)[]): string | undefined {\n\tconst formatted = notices\n\t\t.map((notice) => (notice ? formatAgentsViewStatusLine(notice) : \"\"))\n\t\t.filter((notice) => notice.length > 0);\n\treturn formatted.length > 0 ? formatted.join(\" · \") : undefined;\n}\n\nexport function shouldReconnectAgentsViewDaemon(reason: DaemonClosingReason | undefined): boolean {\n\treturn reason !== \"shutdown\";\n}\n\nexport function createAgentsViewReplyHeadline(text: string | undefined): string | undefined {\n\treturn text\n\t\t?.split(\"\\n\")\n\t\t.map((line) => line.replace(/\\s+/g, \" \").trim())\n\t\t.find((line) => line.length > 0);\n}\n\nexport function getAgentsViewDepth(scopeRoot: SessionSummary | undefined): number {\n\treturn scopeRoot ? (scopeRoot.rlmDepth ?? 0) + 1 : 0;\n}\n\nexport function createInitialAgentsViewScopeFrames(\n\tinitialScopeKey: AgentsViewScopeKey | undefined,\n\treturnChat: SessionSummary | undefined,\n): AgentsViewScopeFrame[] {\n\tif (!initialScopeKey) return [];\n\treturn [\n\t\t{\n\t\t\tscope: initialScopeKey,\n\t\t\t...(returnChat?.sessionId === initialScopeKey.sessionId ? { returnChat } : {}),\n\t\t},\n\t];\n}\n\nexport function createInitialAgentsViewPersistentState(\n\toptions: Pick<AgentsViewModeOptions, \"initialScopeKey\" | \"initialSession\">,\n): AgentsViewPersistentState {\n\tconst initialSession = options.initialSession;\n\treturn {\n\t\t...(initialSession\n\t\t\t? {\n\t\t\t\t\tselectedRowIdentity: getSummaryIdentity(initialSession),\n\t\t\t\t\tselectedSessionKey: getAgentsViewSelectionKey(initialSession),\n\t\t\t\t\tbackSession: initialSession,\n\t\t\t\t}\n\t\t\t: {}),\n\t\t...(options.initialScopeKey\n\t\t\t? {\n\t\t\t\t\tscopeFrames: createInitialAgentsViewScopeFrames(options.initialScopeKey, initialSession),\n\t\t\t\t\t...(initialSession ? { lastSuccessfulLiveSummaries: [initialSession] } : {}),\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n}\n\nexport function createScopeBackReturnChatOpenResult(\n\tresult: Extract<AgentsViewRunResult, { type: \"scope_back\" }>,\n): Extract<AgentsViewRunResult, { type: \"open\" }> | undefined {\n\tif (!result.returnChat) return undefined;\n\treturn {\n\t\ttype: \"open\",\n\t\tsummary: result.returnChat,\n\t\texpandedAncestorSessionIds: result.expandedAncestorSessionIds,\n\t\thasChildren: result.hasChildren,\n\t};\n}\n\ninterface OpenedAgentsViewSession {\n\tconnection: DaemonAgentConnection;\n\tsummary: SessionSummary;\n\tcwdFallbackNotice?: string;\n}\n\nexport function resolveAgentsViewOpenCwd(\n\tsummary: SessionSummary,\n\tfallbackCwd: string | undefined,\n): { overrideCwd?: string; notice?: string } {\n\tif (!summary.cwd || existsSync(summary.cwd) || !fallbackCwd) {\n\t\treturn {};\n\t}\n\treturn {\n\t\toverrideCwd: fallbackCwd,\n\t\tnotice: `Original directory is missing (${summary.cwd}); opened in ${fallbackCwd} instead.`,\n\t};\n}\n\nasync function openAgentsViewSession(\n\toptions: AgentsViewModeOptions,\n\tsummary: SessionSummary,\n): Promise<OpenedAgentsViewSession> {\n\tconst socketPath = options.socketPath;\n\tif (!socketPath) throw new Error(\"Agents view daemon socket is not configured\");\n\tlet client = await connectAgentsViewDaemonClient(socketPath);\n\tif (summary.activeSessionId) {\n\t\ttry {\n\t\t\tconst connection = await DaemonAgentConnection.attach(client, summary.activeSessionId, {\n\t\t\t\tcloseClientOnDispose: true,\n\t\t\t\trecoverDaemon: options.recoverDaemon,\n\t\t\t\treconnectTimeoutMs: options.reconnectTimeoutMs,\n\t\t\t\ttelemetryDisabled: options.config.telemetryDisabled,\n\t\t\t});\n\t\t\treturn { connection, summary };\n\t\t} catch (error) {\n\t\t\tclient.close();\n\t\t\tif (!summary.sessionFile || !isUnknownActiveSessionError(error)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tclient = await connectAgentsViewDaemonClient(socketPath);\n\t\t}\n\t}\n\n\tif (!summary.sessionFile) {\n\t\tclient.close();\n\t\tthrow new Error(\"Cannot open agent without an active runtime or saved session file\");\n\t}\n\n\ttry {\n\t\tconst resumed = await resumeSavedAgentsViewSession(client, options.config, summary);\n\t\tconst connection = await DaemonAgentConnection.attach(client, resumed.activeSessionId, {\n\t\t\tcloseClientOnDispose: true,\n\t\t\trecoverDaemon: options.recoverDaemon,\n\t\t\treconnectTimeoutMs: options.reconnectTimeoutMs,\n\t\t\ttelemetryDisabled: options.config.telemetryDisabled,\n\t\t});\n\t\treturn { connection, summary: resumed.summary, cwdFallbackNotice: resumed.cwdFallbackNotice };\n\t} catch (error) {\n\t\tclient.close();\n\t\tthrow error;\n\t}\n}\n\n/**\n * Resume a saved session file into the daemon and return the live summary.\n * The daemon's create-with-sessionPath is idempotent for this client: an\n * already-resident session is reused instead of resumed twice.\n */\nasync function resumeSavedAgentsViewSession(\n\tclient: DaemonClient,\n\tconfig: AgentSessionRuntimeConfig,\n\tsummary: SessionSummary,\n): Promise<{ summary: SessionSummary; activeSessionId: string; cwdFallbackNotice?: string }> {\n\tif (!summary.sessionFile) {\n\t\tthrow new Error(\"Cannot resume a session without a saved session file\");\n\t}\n\tconst { overrideCwd, notice } = resolveAgentsViewOpenCwd(summary, config.cwd);\n\tconst response = await client.request({\n\t\ttype: \"create\",\n\t\tconfig: createAgentsViewResumeConfig(config, overrideCwd),\n\t\tsessionPath: summary.sessionFile,\n\t});\n\tconst createdSummary = expectSessionSummary(requireDaemonData(response));\n\treturn {\n\t\tsummary: createdSummary,\n\t\tactiveSessionId: getRequiredActiveSessionId(createdSummary),\n\t\tcwdFallbackNotice: notice,\n\t};\n}\n\nasync function connectAgentsViewDaemonClient(socketPath: string): Promise<DaemonClient> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect();\n\t\treturn client;\n\t} catch (error) {\n\t\tclient.close();\n\t\tthrow error;\n\t}\n}\n\nfunction getRequiredActiveSessionId(summary: SessionSummary): string {\n\tif (!summary.activeSessionId) {\n\t\tthrow new Error(\"Daemon returned a session without an active session id\");\n\t}\n\treturn summary.activeSessionId;\n}\n\nfunction isUnknownActiveSessionError(error: unknown): boolean {\n\treturn error instanceof Error && error.message.startsWith(\"Unknown active session:\");\n}\n\nexport async function runAgentsViewMode(options: AgentsViewModeOptions): Promise<void> {\n\tconst persistentState = createInitialAgentsViewPersistentState(options);\n\tconst promptStashStore = options.promptStashStore ?? new ClientPromptStashStore();\n\n\twhile (true) {\n\t\tconst view = new AgentsViewMode(options, persistentState);\n\t\tconst viewResult = await view.run();\n\t\tif (viewResult.type === \"exit\") return;\n\t\tlet result: Extract<AgentsViewRunResult, { type: \"open\" }>;\n\t\tif (viewResult.type === \"scope_back\") {\n\t\t\tpersistentState.scopeFrames = transitionAgentsViewScope(persistentState.scopeFrames ?? [], { type: \"back\" });\n\t\t\tpersistentState.scopeRootSummary = undefined;\n\t\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(viewResult.selection);\n\t\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(viewResult.selection);\n\t\t\tpersistentState.pendingExpandedAncestorSessionIds = viewResult.expandedAncestorSessionIds;\n\t\t\tpersistentState.query = \"\";\n\t\t\tconst returnChatResult = createScopeBackReturnChatOpenResult(viewResult);\n\t\t\tif (!returnChatResult) continue;\n\t\t\tresult = returnChatResult;\n\t\t} else {\n\t\t\tresult = viewResult;\n\t\t}\n\n\t\tconst selection = result.selection ?? result.summary;\n\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(selection);\n\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(selection);\n\t\tpersistentState.pendingExpandedAncestorSessionIds = result.expandedAncestorSessionIds;\n\t\tif (result.statusMessage) persistentState.statusMessage = result.statusMessage;\n\n\t\tlet opened: OpenedAgentsViewSession | undefined;\n\t\ttry {\n\t\t\topened = await openAgentsViewSession(options, result.summary);\n\t\t\tpersistentState.backSession = opened.summary;\n\t\t\tif (opened.cwdFallbackNotice) {\n\t\t\t\tpersistentState.statusMessage = combineAgentsViewStartupNotices(\n\t\t\t\t\tresult.statusMessage,\n\t\t\t\t\topened.cwdFallbackNotice,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst uiServices = await resolveAgentsViewSessionUiServices(options, opened.summary);\n\t\t\tconst interactiveMode = new InteractiveMode({\n\t\t\t\tagentConnection: opened.connection,\n\t\t\t\tdaemonSocketPath: options.socketPath,\n\t\t\t\tuiServices,\n\t\t\t\tpromptStashStore,\n\t\t\t\tpromptStashSessionId: opened.summary.sessionId,\n\t\t\t\tbindLocalSessionExtensions: false,\n\t\t\t\tmigratedProviders: options.migratedProviders,\n\t\t\t\tmodelFallbackMessage: resolveAttachModelFallbackMessage(opened.summary, options.modelFallbackMessage),\n\t\t\t\tstartupNotice: combineAgentsViewStartupNotices(result.statusMessage, opened.cwdFallbackNotice),\n\t\t\t\tverbose: options.verbose,\n\t\t\t\treturnToAgentsView: true,\n\t\t\t\tforceFullscreen: true,\n\t\t\t\t// The agents view renders the global notices itself, so suppress them in-session.\n\t\t\t\tagentsViewOwnsStartupNotices: true,\n\t\t\t\tsessionDepth: opened.summary.rlmDepth,\n\t\t\t\tsessionHasChildren: result.hasChildren,\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst interactiveResult = await interactiveMode.run();\n\t\t\t\tconst source = interactiveResult.source;\n\t\t\t\tconst returnedSession: SessionSummary = {\n\t\t\t\t\t...opened.summary,\n\t\t\t\t\t...source,\n\t\t\t\t\tid: source.activeSessionId ?? opened.summary.id,\n\t\t\t\t};\n\t\t\t\t// Preserve an unattachable child's selection while its parent chat was open.\n\t\t\t\tif (selection.sessionId === result.summary.sessionId) {\n\t\t\t\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(returnedSession);\n\t\t\t\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(returnedSession);\n\t\t\t\t}\n\t\t\t\tif (interactiveResult.type === \"scoped_agents_view\") {\n\t\t\t\t\tconst nextScope = { sessionId: source.sessionId, activeSessionId: source.activeSessionId };\n\t\t\t\t\tpersistentState.scopeFrames = transitionAgentsViewScope(persistentState.scopeFrames ?? [], {\n\t\t\t\t\t\ttype: \"push\",\n\t\t\t\t\t\tscope: nextScope,\n\t\t\t\t\t\treturnChat: returnedSession,\n\t\t\t\t\t});\n\t\t\t\t\tconst cachedLiveSummaries = persistentState.lastSuccessfulLiveSummaries ?? [];\n\t\t\t\t\tconst cachedIndex = cachedLiveSummaries.findIndex(\n\t\t\t\t\t\t(summary) => summary.sessionId === returnedSession.sessionId,\n\t\t\t\t\t);\n\t\t\t\t\tpersistentState.lastSuccessfulLiveSummaries =\n\t\t\t\t\t\tcachedIndex === -1\n\t\t\t\t\t\t\t? [...cachedLiveSummaries, returnedSession]\n\t\t\t\t\t\t\t: cachedLiveSummaries.map((summary, index) => (index === cachedIndex ? returnedSession : summary));\n\t\t\t\t\tpersistentState.scopeRootSummary = undefined;\n\t\t\t\t\tpersistentState.query = \"\";\n\t\t\t\t}\n\t\t\t\tpersistentState.backSession = returnedSession;\n\t\t\t} catch (error) {\n\t\t\t\t// The session opened fine and then threw while running; label it as a\n\t\t\t\t// runtime crash so it isn't mixed in with true open failures.\n\t\t\t\tlogClientError(\"Agent session crashed\", error);\n\t\t\t\tpersistentState.statusMessage = formatError(\"Agent session crashed\", error);\n\t\t\t\t// Tear down the session TUI exactly as a normal back-navigation would\n\t\t\t\t// (drain input, stop renderer + theme watcher) so it doesn't fight the\n\t\t\t\t// agents-view UI for the terminal, then drop the daemon connection.\n\t\t\t\tawait interactiveMode.teardownSessionUi({ preserveAltScreen: true });\n\t\t\t\tawait opened.connection.dispose().catch(() => undefined);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait opened?.connection.dispose().catch(() => undefined);\n\t\t\tlogClientError(\"Failed to open agent\", error);\n\t\t\tpersistentState.statusMessage = formatError(\"Failed to open agent\", error);\n\t\t}\n\t}\n}\n\nconst AGENTS_VIEW_COMMAND_NAMES = [\"name\", \"kill\"] as const;\nexport type AgentsViewCommandName = (typeof AGENTS_VIEW_COMMAND_NAMES)[number];\nconst AGENTS_VIEW_COMMAND_NAME_SET: ReadonlySet<string> = new Set(AGENTS_VIEW_COMMAND_NAMES);\n\nexport interface AgentsViewCommand {\n\tname: AgentsViewCommandName;\n\targs: string;\n}\n\n/** Row-targeted commands the armed composer maps onto existing RPCs. */\nexport function parseAgentsViewCommand(text: string): AgentsViewCommand | undefined {\n\tconst parsed = parseSlashCommand(text);\n\tif (!parsed) return undefined;\n\tconst name = resolveBuiltinSlashCommandName(parsed.name);\n\tif (!AGENTS_VIEW_COMMAND_NAME_SET.has(name)) return undefined;\n\treturn { name: name as AgentsViewCommandName, args: parsed.args };\n}\n\n/**\n * Reject recognized built-ins that are neither session-owned nor view\n * commands, so they are never sent to the model as plain prompt text.\n */\nexport function getReplyComposerCommandRejection(text: string): string | undefined {\n\tconst parsed = parseSlashCommand(text);\n\tif (!parsed) return undefined;\n\tconst name = resolveBuiltinSlashCommandName(parsed.name);\n\tif (isSessionSlashCommandName(name)) return undefined;\n\tif (AGENTS_VIEW_COMMAND_NAME_SET.has(name)) return undefined;\n\tif (!isBuiltinSlashCommandName(parsed.name)) return undefined;\n\treturn `/${parsed.name} is not available here; open the session to run it`;\n}\n\nconst AGENTS_VIEW_COMMAND_DESCRIPTIONS: Record<AgentsViewCommandName, { description: string; argumentHint?: string }> =\n\t{\n\t\tname: { description: \"Set session display name\", argumentHint: \"<name>\" },\n\t\tkill: { description: \"Stop this agent's runtime (session stays resumable)\" },\n\t};\n\nfunction agentsViewSlashCommands(): {\n\tname: string;\n\taliases?: readonly string[];\n\tdescription: string;\n\targumentHint?: string;\n\ttakesArgument?: boolean;\n}[] {\n\treturn AGENTS_VIEW_COMMAND_NAMES.map((name) => {\n\t\tconst builtin = BUILTIN_SLASH_COMMANDS.find((command) => command.name === name);\n\t\tconst display = AGENTS_VIEW_COMMAND_DESCRIPTIONS[name];\n\t\treturn {\n\t\t\tname,\n\t\t\taliases: builtin?.aliases,\n\t\t\tdescription: display.description,\n\t\t\targumentHint: display.argumentHint ?? builtin?.argumentHint,\n\t\t\ttakesArgument: name === \"name\" ? true : builtin?.takesArgument,\n\t\t};\n\t});\n}\n\n/** Autocomplete for the reply composer: session-owned plus view commands. */\nexport function createReplyComposerAutocompleteProvider(cwd: string, fdPath?: string): AutocompleteProvider {\n\tconst sessionCommands = BUILTIN_SLASH_COMMANDS.filter((command) => isSessionSlashCommandName(command.name)).map(\n\t\t(command) => ({\n\t\t\tname: command.name,\n\t\t\taliases: command.aliases,\n\t\t\tdescription: command.description,\n\t\t\targumentHint: command.argumentHint,\n\t\t\ttakesArgument: command.takesArgument,\n\t\t}),\n\t);\n\treturn new CombinedAutocompleteProvider([...sessionCommands, ...agentsViewSlashCommands()], cwd, fdPath ?? null);\n}\n\nexport function resolveCurrentReplyTargetSummary(\n\trecords: readonly UnifiedSessionRecord[],\n\ttarget: { key: string; summary: SessionSummary },\n\tfindLive: (activeSessionId: string) => SessionSummary | undefined,\n): SessionSummary {\n\tconst identity = getSummaryIdentity(target.summary);\n\tconst current = records.find((record) => record.identity === identity || record.identityAliases.includes(identity));\n\tif (current) return summaryForUnifiedRecord(current);\n\tconst live = target.summary.activeSessionId ? findLive(target.summary.activeSessionId) : undefined;\n\tif (live) return live;\n\t// A persisted target missing from the current live catalog can still be\n\t// resumed from its captured file, but its captured runtime id is stale.\n\tif (target.summary.sessionFile && target.summary.activeSessionId) {\n\t\treturn { ...target.summary, activeSessionId: undefined, lifecycle: \"archived\", activity: \"idle\" };\n\t}\n\treturn target.summary;\n}\n\nexport class AgentsViewMode implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly ui: TUI;\n\tprivate readonly editor: CustomEditor;\n\tprivate readonly splash: BrandSplashHeader;\n\tprivate readonly fullscreenDock: Component;\n\tprivate readonly keybindings: KeybindingsManager;\n\tprivate client: DaemonClient | undefined;\n\tprivate unsubscribeClientClose: (() => void) | undefined;\n\tprivate unsubscribeClientMessage: (() => void) | undefined;\n\tprivate reconnectPromise: Promise<void> | undefined;\n\tprivate reconnectTimedOut = false;\n\tprivate daemonShutdownReceived = false;\n\tprivate resolveRun: ((result: AgentsViewRunResult) => void) | undefined;\n\tprivate pollTimer: NodeJS.Timeout | undefined;\n\tprivate heartbeatPollTimer: NodeJS.Timeout | undefined;\n\tprivate animationTimer: NodeJS.Timeout | undefined;\n\tprivate ctrlCExitHintExpiresAt = 0;\n\tprivate ctrlCExitHintTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate deleteConfirmExpiresAt = 0;\n\tprivate deleteConfirmTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate workingIconFrame = 0;\n\tprivate rows: AgentsViewRow[] = [];\n\tprivate lastListedSummaries: SessionSummary[] = [];\n\tprivate lastVisibleSummaries: SessionSummary[] = [];\n\tprivate savedSessions: AgentConnectionSavedSessionInfo[] = [];\n\tprivate lastSuccessfulSavedSessions: AgentConnectionSavedSessionInfo[] = [];\n\tprivate heartbeats: AgentConnectionHeartbeat[] = [];\n\tprivate unifiedRecords: UnifiedSessionRecord[] = [];\n\tprivate unifiedIndex: UnifiedSessionIndex = buildUnifiedSessionIndex([]);\n\tprivate scopedRecords: UnifiedSessionRecord[] = [];\n\tprivate scopeKey: AgentsViewScopeKey | undefined;\n\tprivate scopeRootSummary: SessionSummary | undefined;\n\tprivate liveCatalogReady = false;\n\tprivate savedCatalogReady = false;\n\tprivate savedCatalogGeneration = 0;\n\tprivate liveCatalogGeneration = 0;\n\tprivate heartbeatCatalogGeneration = 0;\n\tprivate liveCatalogPollPromise: Promise<void> | undefined;\n\tprivate liveCatalogRefreshPending = false;\n\tprivate savedCatalogRefreshPending = false;\n\tprivate expandedSubagentParents = new Set<string>();\n\t// Agent row identities whose full spawn program is currently shown.\n\t// The program key toggles each agent shown ↔ hidden.\n\tprivate programShownParents = new Set<string>();\n\tprivate selectedIndex = 0;\n\tprivate selectedRowIdentity: string | undefined;\n\tprivate selectedActiveSessionId: string | undefined;\n\tprivate selectedSessionKey: AgentsViewSelectionKey | undefined;\n\tprivate selectionAnchorPending = false;\n\t/** Armed reply composer target: a live agent or a saved session to resume on send. */\n\tprivate replyTarget: { key: string; summary: SessionSummary } | undefined;\n\t/** Provider bound to the armed target's cwd for file-path completions. */\n\tprivate replyAutocomplete: AutocompleteProvider | undefined;\n\tprivate fdPath: string | undefined;\n\tprivate creatingNewSession = false;\n\tprivate replyLastAssistantText: string | undefined;\n\tprivate replyLastAssistantTextLoading = false;\n\tprivate replyHeaderTime = \"\";\n\tprivate pendingDeleteAgent: PendingDeleteAgent | undefined;\n\tprivate pendingKillSubagent: PendingKillSubagent | undefined;\n\tprivate renameTarget: { activeSessionId?: string; sessionFile?: string; summary: SessionSummary } | undefined;\n\tprivate actionModeSearchQuery: string | undefined;\n\tprivate readonly inactiveAgentIdentities = new Set<string>();\n\tprivate statusMessage: string | undefined;\n\tprivate statusMessageTone: \"muted\" | \"error\" | \"warning\" = \"muted\";\n\tprivate statusMessageSticky = false;\n\tprivate statusMessageTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate stopped = false;\n\n\tconstructor(\n\t\tprivate readonly options: AgentsViewModeOptions,\n\t\tprivate readonly persistentState: AgentsViewPersistentState = {},\n\t) {\n\t\tconst initialFrames =\n\t\t\tpersistentState.scopeFrames ??\n\t\t\tcreateInitialAgentsViewScopeFrames(\n\t\t\t\toptions.initialScopeKey,\n\t\t\t\tpersistentState.backSession ?? options.initialSession,\n\t\t\t);\n\t\tpersistentState.scopeFrames = initialFrames;\n\t\tthis.scopeKey = initialFrames.at(-1)?.scope;\n\t\tthis.scopeRootSummary = persistentState.scopeRootSummary;\n\t\tthis.selectedRowIdentity = persistentState.selectedRowIdentity;\n\t\tthis.selectedSessionKey = persistentState.selectedSessionKey;\n\t\tthis.selectedActiveSessionId = persistentState.selectedSessionKey?.activeSessionId;\n\t\tthis.lastListedSummaries = persistentState.lastSuccessfulLiveSummaries ?? [];\n\t\tthis.savedSessions = persistentState.savedSessions ?? [];\n\t\tthis.lastSuccessfulSavedSessions = persistentState.lastSuccessfulSavedSessions ?? this.savedSessions;\n\t\tthis.savedCatalogReady = persistentState.lastSuccessfulSavedSessions !== undefined;\n\t\tthis.heartbeats = persistentState.heartbeats ?? [];\n\t\tthis.savedCatalogGeneration = persistentState.savedCatalogGeneration ?? 0;\n\t\tthis.expandedSubagentParents = persistentState.expandedSubagentParents ?? new Set();\n\t\tpersistentState.expandedSubagentParents = this.expandedSubagentParents;\n\t\tthis.programShownParents = persistentState.programShownParents ?? new Set();\n\t\tpersistentState.programShownParents = this.programShownParents;\n\t\tthis.keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(this.keybindings);\n\t\tsetRegisteredThemes(options.uiServices.getThemes());\n\t\tinitTheme(options.uiServices.settingsManager.getTheme(), true);\n\n\t\tthis.ui = new TUI(new ProcessTerminal(), options.uiServices.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(options.uiServices.settingsManager.getClearOnShrink());\n\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - Agents`);\n\t\tthis.editor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {\n\t\t\tpaddingX: options.uiServices.settingsManager.getEditorPaddingX(),\n\t\t\tautocompleteMaxVisible: options.uiServices.settingsManager.getAutocompleteMaxVisible(),\n\t\t\tplaceholder: SEARCH_PROMPT_PLACEHOLDER,\n\t\t\tplaceholderColor: (text) => theme.fg(\"dim\", text),\n\t\t});\n\t\tvoid ensureTool(\"fd\").then((fdPath) => {\n\t\t\tthis.fdPath = fdPath;\n\t\t\t// Rebind an already-armed provider so @-completion picks up fd.\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.replyAutocomplete = createReplyComposerAutocompleteProvider(this.replyTarget.summary.cwd, fdPath);\n\t\t\t}\n\t\t});\n\t\t// Search input never autocompletes; only the armed composer's provider answers.\n\t\tthis.editor.setAutocompleteProvider({\n\t\t\tgetSuggestions: async (lines, cursorLine, cursorCol, suggestOptions) => {\n\t\t\t\tif (!this.replyTarget) return null;\n\t\t\t\treturn this.replyAutocomplete?.getSuggestions(lines, cursorLine, cursorCol, suggestOptions) ?? null;\n\t\t\t},\n\t\t\tapplyCompletion: (lines, cursorLine, cursorCol, item, prefix) =>\n\t\t\t\tthis.replyAutocomplete?.applyCompletion(lines, cursorLine, cursorCol, item, prefix) ?? {\n\t\t\t\t\tlines,\n\t\t\t\t\tcursorLine,\n\t\t\t\t\tcursorCol,\n\t\t\t\t},\n\t\t});\n\t\tthis.editor.focused = true;\n\t\tthis.editor.getHeaderLine = () => this.renderReplyHeaderLine();\n\t\tthis.editor.onSubmit = (value) => {\n\t\t\tvoid this.submit(value);\n\t\t};\n\t\tthis.editor.setText(persistentState.query ?? \"\");\n\t\tthis.editor.onCtrlD = () => {\n\t\t\tthis.finish({ type: \"exit\" });\n\t\t};\n\t\tthis.editor.onAgentsBack = () => {\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (this.editor.getText().length > 0) return false;\n\t\t\tconst ancestors = this.scopeKey\n\t\t\t\t? getUnifiedSessionAncestorSessionIds(this.unifiedRecords, this.scopeKey, this.unifiedIndex)\n\t\t\t\t: [];\n\t\t\tconst scopeRoot = this.scopeRootSummary;\n\t\t\tconst result = resolveAgentsViewLeftResult(\n\t\t\t\tscopeRoot,\n\t\t\t\tancestors,\n\t\t\t\tthis.persistentState.scopeFrames?.at(-1)?.returnChat,\n\t\t\t);\n\t\t\tif (result && scopeRoot) {\n\t\t\t\tthis.finish({\n\t\t\t\t\t...result,\n\t\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\t\tgetAgentsViewSelectionKey(scopeRoot),\n\t\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Global view has no hierarchy parent: consume Left without opening chat.\n\t\t\treturn true;\n\t\t};\n\t\tthis.editor.onEscape = () => {\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t} else if (this.editor.getText().length > 0) {\n\t\t\t\tthis.setSearchQuery(\"\");\n\t\t\t} else {\n\t\t\t\tconst backSession = this.persistentState.backSession;\n\t\t\t\tthis.finish(\n\t\t\t\t\tbackSession\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\ttype: \"open\",\n\t\t\t\t\t\t\t\tsummary: backSession,\n\t\t\t\t\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\t\t\t\t\tgetAgentsViewSelectionKey(backSession),\n\t\t\t\t\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: { type: \"exit\" },\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\t\tthis.fullscreenDock = {\n\t\t\trender: (width) => this.renderDock(width),\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.editor.invalidate();\n\t\t\t},\n\t\t};\n\t\tthis.splash = new BrandSplashHeader(\n\t\t\tVERSION,\n\t\t\t() => this.getSplashModelId(),\n\t\t\t() => this.getSplashCwd(),\n\t\t\tundefined,\n\t\t\t{\n\t\t\t\ttopPadding: true,\n\t\t\t\tgetExtraMetadata: () => {\n\t\t\t\t\tconst root = this.scopeRootSummary;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{ label: \"agents\", value: this.getAgentCountsText() },\n\t\t\t\t\t\t{ label: \"scope\", value: root ? getAgentsViewSessionTitle(root) : \"global\" },\n\t\t\t\t\t\t{ label: \"depth\", value: String(getAgentsViewDepth(root)) },\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\tasync run(): Promise<AgentsViewRunResult> {\n\t\tthis.client = new DaemonClient(this.requireSocketPath());\n\t\tawait this.client.connect();\n\t\tthis.subscribeToClientClose(this.client);\n\t\tthis.unsubscribeClientMessage = this.client.onMessage((message) => {\n\t\t\tif (message.type === \"heartbeats_changed\") void this.refreshHeartbeats();\n\t\t});\n\n\t\tthis.ui.addChild(this);\n\t\tthis.ui.setFocus(this);\n\t\tthis.ui.start();\n\t\tthis.ui.enterFullscreen({\n\t\t\tscroll: [this],\n\t\t\tdock: this.fullscreenDock,\n\t\t\tmouse: false,\n\t\t\tviewportControls: false,\n\t\t});\n\t\tconst startupStatusMessage = this.persistentState.statusMessage;\n\t\tthis.persistentState.statusMessage = undefined;\n\t\tif (startupStatusMessage) {\n\t\t\tthis.setStatusMessage(startupStatusMessage, { render: false });\n\t\t}\n\t\tthis.ui.requestRender(true);\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tconst runPromise = new Promise<AgentsViewRunResult>((resolve) => {\n\t\t\tthis.resolveRun = resolve;\n\t\t});\n\t\tvoid this.refreshSessions();\n\t\tvoid this.refreshSavedSessions();\n\t\tvoid this.refreshHeartbeats();\n\t\tthis.loadStartupNotices();\n\t\tthis.pollTimer = setInterval(() => this.pollSessions(), POLL_INTERVAL_MS);\n\t\tthis.pollTimer.unref?.();\n\t\tthis.heartbeatPollTimer = setInterval(() => void this.refreshHeartbeats(), HEARTBEAT_POLL_INTERVAL_MS);\n\t\tthis.heartbeatPollTimer.unref?.();\n\t\tthis.animationTimer = setInterval(() => {\n\t\t\tif (!this.rows.some((row) => row.section === \"running\")) return;\n\t\t\tthis.workingIconFrame += 1;\n\t\t\tthis.ui.requestRender();\n\t\t}, WORKING_ICON_INTERVAL_MS);\n\t\tthis.animationTimer.unref?.();\n\n\t\treturn runPromise;\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.clearStickyStatusMessage();\n\t\tif (this.renameTarget) {\n\t\t\tif (this.keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\t\tthis.exitRenameMode();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.editor.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.clear\")) {\n\t\t\t// The composer hints advertise ctrl+c as cancel; it must not start the exit flow.\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.handleCtrlC();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.rename\")) {\n\t\t\tthis.enterRenameMode();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.delete\")) {\n\t\t\tthis.clearCtrlCExitHint({ render: false });\n\t\t\tvoid this.handleDeleteSelected();\n\t\t\treturn;\n\t\t}\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\tif (this.keybindings.matches(data, \"app.agents.reply\") && this.editor.getText().length === 0) {\n\t\t\tvoid this.toggleReplyTarget();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.replyTarget && this.keybindings.matches(data, \"app.agents.new\")) {\n\t\t\tvoid this.createNewSession();\n\t\t\treturn;\n\t\t}\n\t\tif (this.replyTarget && this.keybindings.matches(data, \"app.message.followUp\")) {\n\t\t\tthis.handleReplyFollowUp();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.program\")) {\n\t\t\tthis.cycleProgramForSelected();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.replyTarget && this.keybindings.matches(data, \"app.agents.open\")) {\n\t\t\tif (this.editor.getText().length === 0 || this.isSearchCursorAtEnd()) {\n\t\t\t\tthis.openSelected();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (!this.replyTarget && this.handleListNavigation(data)) {\n\t\t\treturn;\n\t\t}\n\t\tconst previous = this.editor.getText();\n\t\tthis.editor.handleInput(data);\n\t\tif (!this.replyTarget && this.editor.getText() !== previous) {\n\t\t\tthis.queryChanged();\n\t\t}\n\t}\n\n\tprivate isSearchCursorAtEnd(): boolean {\n\t\tconst lines = this.editor.getLines();\n\t\tconst cursor = this.editor.getCursor();\n\t\treturn cursor.line === lines.length - 1 && cursor.col === (lines[cursor.line]?.length ?? 0);\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst height = this.contentHeight(safeWidth);\n\t\tconst lines = this.renderContent(safeWidth, height).slice(0, height);\n\t\twhile (lines.length < height) {\n\t\t\tlines.push(\"\");\n\t\t}\n\t\treturn lines.slice(0, height).map((line) => this.finalizeRenderedLine(line, safeWidth));\n\t}\n\n\tinvalidate(): void {\n\t\tthis.editor.invalidate();\n\t\tthis.splash.invalidate();\n\t}\n\n\tprivate renderContent(width: number, height: number): string[] {\n\t\tif (height <= 0) {\n\t\t\treturn [];\n\t\t}\n\t\tconst headerLines = this.splash.render(width);\n\t\tconst noticeLines = this.renderStartupNotices(width);\n\t\tif (noticeLines.length > 0) {\n\t\t\theaderLines.push(\"\", ...noticeLines);\n\t\t}\n\t\theaderLines.push(\"\");\n\n\t\t// The prompt belongs to the scroll pane rather than the fullscreen dock, but\n\t\t// it must remain usable when a short viewport or wrapped notices exhaust the\n\t\t// header. Trim optional header chrome first and reserve one session-list row.\n\t\tconst promptLines = this.renderPrompt(width);\n\t\tconst listGap = height >= promptLines.length + 2 ? 1 : 0;\n\t\tconst headerRows = Math.max(0, height - promptLines.length - listGap - 1);\n\t\tconst lines = headerLines.slice(0, headerRows);\n\t\tlines.push(...promptLines);\n\t\tif (listGap > 0) lines.push(\"\");\n\t\tconst listRows = Math.max(0, height - lines.length);\n\t\tlines.push(...this.renderSessionRows(width, listRows));\n\t\treturn lines;\n\t}\n\n\tprivate loadStartupNotices(): void {\n\t\t// Notices live on persistentState (read directly in renderStartupNotices), so they\n\t\t// survive leaving and re-entering the agents view regardless of which instance's\n\t\t// gather resolved. Already have them? Nothing to do.\n\t\tif (this.persistentState.startupNotices) {\n\t\t\treturn;\n\t\t}\n\t\t// Reuse an in-flight gather from an earlier agents-view instance so re-entry does\n\t\t// not re-run the checks or lose a result that resolved meanwhile.\n\t\tconst promise =\n\t\t\tthis.persistentState.startupNoticesPromise ??\n\t\t\tgatherStartupNotices({\n\t\t\t\tversion: VERSION,\n\t\t\t\tcwd: this.options.uiServices.getInitialCwd(),\n\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\tsettingsManager: this.options.uiServices.settingsManager,\n\t\t\t});\n\t\tthis.persistentState.startupNoticesPromise = promise;\n\t\tvoid promise\n\t\t\t.then((notices) => {\n\t\t\t\tthis.persistentState.startupNotices = notices;\n\t\t\t\t// Best-effort immediate paint; a re-entered instance also picks this up on\n\t\t\t\t// its next poll tick since render reads persistentState directly.\n\t\t\t\tthis.ui.requestRender();\n\t\t\t})\n\t\t\t.catch(() => {});\n\t}\n\n\tprivate renderStartupNotices(width: number): string[] {\n\t\tconst notices = this.persistentState.startupNotices;\n\t\tif (!notices) {\n\t\t\treturn [];\n\t\t}\n\t\tconst formatted: string[] = [];\n\t\tif (notices.newVersion) {\n\t\t\tformatted.push(formatUpdateAvailableNotice(notices.newVersion));\n\t\t}\n\t\tif (notices.packageUpdates.length > 0) {\n\t\t\tformatted.push(formatPackageUpdateNotice(notices.packageUpdates));\n\t\t}\n\t\tif (notices.tmuxWarning) {\n\t\t\tformatted.push(formatTmuxWarningNotice(notices.tmuxWarning));\n\t\t}\n\t\t// Match the splash header's one-column gutter and wrap so long notices\n\t\t// (e.g. the tmux fix instructions) stay readable instead of truncating.\n\t\tconst wrapWidth = Math.max(1, width - 1);\n\t\treturn formatted.flatMap((line) => wrapTextWithAnsi(line, wrapWidth).map((wrapped) => ` ${wrapped}`));\n\t}\n\n\tprivate handleListNavigation(data: string): boolean {\n\t\tif (this.keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tthis.moveSelection(-1);\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.down\")) {\n\t\t\tthis.moveSelection(1);\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.pageUp\")) {\n\t\t\tthis.moveSelection(-Math.max(1, this.visibleListRows()));\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.pageDown\")) {\n\t\t\tthis.moveSelection(Math.max(1, this.visibleListRows()));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tthis.finish({ type: \"exit\" });\n\t\t\treturn;\n\t\t}\n\t\tthis.showCtrlCExitHint();\n\t}\n\n\tprivate showCtrlCExitHint(): void {\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = Date.now() + EXIT_HINT_DURATION_MS;\n\t\tthis.ctrlCExitHintTimer = setTimeout(() => {\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t\tif (!this.isCtrlCExitHintVisible()) {\n\t\t\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, EXIT_HINT_DURATION_MS);\n\t\tthis.ctrlCExitHintTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearCtrlCExitHint(options: { render?: boolean } = {}): void {\n\t\tif (!this.ctrlCExitHintTimer && this.ctrlCExitHintExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isCtrlCExitHintVisible(): boolean {\n\t\treturn this.ctrlCExitHintExpiresAt > Date.now();\n\t}\n\n\tprivate showDeleteConfirmation(): void {\n\t\tif (this.deleteConfirmTimer) {\n\t\t\tclearTimeout(this.deleteConfirmTimer);\n\t\t}\n\t\tthis.deleteConfirmExpiresAt = Date.now() + DELETE_CONFIRM_DURATION_MS;\n\t\tthis.deleteConfirmTimer = setTimeout(() => {\n\t\t\tthis.deleteConfirmTimer = undefined;\n\t\t\tif (!this.isDeleteConfirmationVisible()) {\n\t\t\t\tthis.deleteConfirmExpiresAt = 0;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, DELETE_CONFIRM_DURATION_MS);\n\t\tthis.deleteConfirmTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearDeleteConfirmation(options: { render?: boolean } = {}): void {\n\t\tthis.pendingKillSubagent = undefined;\n\t\tif (!this.deleteConfirmTimer && this.deleteConfirmExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.deleteConfirmTimer) {\n\t\t\tclearTimeout(this.deleteConfirmTimer);\n\t\t\tthis.deleteConfirmTimer = undefined;\n\t\t}\n\t\tthis.deleteConfirmExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isDeleteConfirmationVisible(): boolean {\n\t\treturn this.deleteConfirmExpiresAt > Date.now();\n\t}\n\n\tprivate setStatusMessage(\n\t\tmessage: string | undefined,\n\t\toptions: { render?: boolean; tone?: \"muted\" | \"error\" | \"warning\"; sticky?: boolean } = {},\n\t): void {\n\t\tconst statusLine = message === undefined ? undefined : formatAgentsViewStatusLine(message);\n\t\tif (this.statusMessageTimer) {\n\t\t\tclearTimeout(this.statusMessageTimer);\n\t\t\tthis.statusMessageTimer = undefined;\n\t\t}\n\t\tthis.statusMessage = statusLine;\n\t\t// Errors come both from explicit tones and from formatError-style messages.\n\t\tthis.statusMessageTone = options.tone ?? (statusLine?.startsWith(\"Failed\") ? \"error\" : \"muted\");\n\t\t// Sticky messages stay up until the next keypress instead of a timer.\n\t\tthis.statusMessageSticky = options.sticky === true && statusLine !== undefined;\n\t\tif (statusLine && !this.statusMessageSticky) {\n\t\t\tthis.statusMessageTimer = setTimeout(() => {\n\t\t\t\tthis.statusMessageTimer = undefined;\n\t\t\t\tif (this.statusMessage === statusLine) {\n\t\t\t\t\tthis.statusMessage = undefined;\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t}, STATUS_MESSAGE_DURATION_MS);\n\t\t\tthis.statusMessageTimer.unref?.();\n\t\t}\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\t/** Sticky messages (e.g. billing warnings) stay until the user acknowledges them with any keypress. */\n\tprivate clearStickyStatusMessage(): void {\n\t\tif (!this.statusMessageSticky || this.daemonShutdownReceived || this.reconnectPromise) {\n\t\t\treturn;\n\t\t}\n\t\tthis.statusMessageSticky = false;\n\t\tthis.statusMessage = undefined;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate moveSelection(delta: number): void {\n\t\tconst selectableIndexes = this.getSelectableRowIndexes();\n\t\tif (selectableIndexes.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst currentPosition = selectableIndexes.includes(this.selectedIndex)\n\t\t\t? selectableIndexes.indexOf(this.selectedIndex)\n\t\t\t: 0;\n\t\tconst nextPosition = Math.max(0, Math.min(selectableIndexes.length - 1, currentPosition + delta));\n\t\tthis.selectedIndex = selectableIndexes[nextPosition] ?? 0;\n\t\tthis.syncSelectedRowState();\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t// Reply stays armed only while the selection sits on the agent row it\n\t\t// targets; nested rows share the parent's session id but are read-only.\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\tif (\n\t\t\tthis.replyTarget &&\n\t\t\t(selectedRow?.kind !== \"agent\" || this.replyTarget.key !== this.selectedActiveSessionId)\n\t\t) {\n\t\t\tthis.setReplyTarget(undefined);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\t// Resolve the persisted sessionId breadcrumb to live row identities once, so\n\t// the rest of the expansion lifecycle stays uniformly identity-keyed.\n\tprivate applyPendingAncestorExpansion(): void {\n\t\tconst sessionIds = this.persistentState.pendingExpandedAncestorSessionIds;\n\t\tif (!sessionIds || sessionIds.length === 0) {\n\t\t\tthis.persistentState.pendingExpandedAncestorSessionIds = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.persistentState.pendingExpandedAncestorSessionIds = undefined;\n\t\tconst wanted = new Set(sessionIds);\n\t\t// A nested ancestor's row only appears once its own parent is expanded, so\n\t\t// expand-and-rebuild until a pass reveals nothing new.\n\t\tlet added = true;\n\t\twhile (added) {\n\t\t\tadded = false;\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tif (wanted.has(row.summary.sessionId) && !this.expandedSubagentParents.has(row.identity)) {\n\t\t\t\t\tthis.expandedSubagentParents.add(row.identity);\n\t\t\t\t\tadded = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (added) {\n\t\t\t\tthis.rebuildRows();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setSearchQuery(query: string): void {\n\t\tthis.editor.setText(query);\n\t\tthis.queryChanged();\n\t}\n\n\tprivate queryChanged(): void {\n\t\tthis.persistentState.query = this.editor.getText();\n\t\tthis.rebuildRows();\n\t\t// Typing must not claim the visible fallback row while the restored\n\t\t// anchor is still waiting for its catalog row.\n\t\tif (!this.selectionAnchorPending) this.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getFilteredRecords(): UnifiedSessionRecord[] {\n\t\tconst query = this.replyTarget || this.renameTarget ? (this.actionModeSearchQuery ?? \"\") : this.editor.getText();\n\t\treturn filterUnifiedSessions(this.scopedRecords, (text) => matchesSearchText(text, query));\n\t}\n\n\t/** Rebuild rows from the last fetched summaries, keeping selection on the same row. */\n\tprivate rebuildRows(): void {\n\t\tconst selectedIdentity = this.rows[this.selectedIndex]?.identity;\n\t\tthis.rows = buildAgentsViewRows(\n\t\t\tthis.getFilteredRecords(),\n\t\t\tthis.expandedSubagentParents,\n\t\t\tthis.programShownParents,\n\t\t\tthis.scopeKey,\n\t\t);\n\t\tconst index =\n\t\t\tselectedIdentity === undefined ? -1 : this.rows.findIndex((row) => row.identity === selectedIdentity);\n\t\tif (index >= 0) {\n\t\t\tthis.selectedIndex = index;\n\t\t} else {\n\t\t\tthis.restoreSelection();\n\t\t}\n\t}\n\n\tprivate async submit(value: string, delivery: \"steer\" | \"followUp\" = \"steer\"): Promise<void> {\n\t\tif (this.renameTarget) {\n\t\t\tawait this.confirmRename(value);\n\t\t\treturn;\n\t\t}\n\t\tif (this.replyTarget) {\n\t\t\tconst target = this.replyTarget;\n\t\t\tconst text = value.trim();\n\t\t\tconst viewCommand = parseAgentsViewCommand(text);\n\t\t\tif (viewCommand) {\n\t\t\t\t// Stale summaries mis-route the RPCs after a runtime replacement.\n\t\t\t\tconst currentSummary = resolveCurrentReplyTargetSummary(\n\t\t\t\t\tthis.unifiedRecords ?? [],\n\t\t\t\t\ttarget,\n\t\t\t\t\t(activeSessionId) => this.findSummaryByActiveSessionId(activeSessionId),\n\t\t\t\t);\n\t\t\t\tconst succeeded = await this.runAgentsViewCommand(viewCommand, currentSummary);\n\t\t\t\tif (!succeeded && this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\tthis.editor.setText(value);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rejection = getReplyComposerCommandRejection(text);\n\t\t\tif (rejection) {\n\t\t\t\t// submitValue cleared the buffer before onSubmit; keep the draft.\n\t\t\t\tif (this.editor.getText().length === 0) this.editor.setText(value);\n\t\t\t\tthis.setStatusMessage(rejection, { tone: \"warning\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tconst sent = await this.sendReply(target, text, delivery);\n\t\t\t\tif (sent) {\n\t\t\t\t\tif (this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the send outcome (or sticky cwd notice) that sendReply just surfaced.\n\t\t\t\t\tawait this.refreshSessions({ preserveStatusOnError: true });\n\t\t\t\t} else if (this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\tthis.editor.setText(value);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Search text is never a prompt or a command; Enter opens the selection.\n\t\tthis.openSelected();\n\t}\n\n\t/** Alt+Enter in the reply composer queues the reply as a follow-up. */\n\tprivate handleReplyFollowUp(): void {\n\t\tif (!this.replyTarget) return;\n\t\t// Unlike Enter, this path skips submitValue, so expand paste markers here.\n\t\tconst text = this.editor.getExpandedText();\n\t\tif (!text.trim()) return;\n\t\tvoid this.submit(text, \"followUp\");\n\t}\n\n\tprivate getSavedSessionCwd(): string {\n\t\treturn this.options.config.cwd ?? this.options.uiServices.getInitialCwd();\n\t}\n\n\tprivate getSavedSessionCatalogContext(): DaemonSavedSessionCatalogContext {\n\t\treturn { cwd: this.getSavedSessionCwd(), sessionDir: this.options.config.sessionDir };\n\t}\n\n\tprivate openSelected(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row?.selectable || this.isPendingDeleteRow(row)) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.selectionAnchorPending) {\n\t\t\tthis.setStatusMessage(\"Waiting for the selected session to load\");\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent-summary\") {\n\t\t\tthis.toggleSubagentList(row);\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent\") {\n\t\t\tthis.openSelectedSubagent(row);\n\t\t\treturn;\n\t\t}\n\t\tif (!row.summary.activeSessionId && !row.summary.sessionFile) {\n\t\t\tthis.setStatusMessage(\"Cannot open agent without an active runtime or saved session file\");\n\t\t\treturn;\n\t\t}\n\t\tthis.finish({\n\t\t\ttype: \"open\",\n\t\t\tsummary: row.summary,\n\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\tthis.unifiedRecords,\n\t\t\t\tgetAgentsViewSelectionKey(row.summary),\n\t\t\t\tthis.unifiedIndex,\n\t\t\t),\n\t\t});\n\t}\n\n\tprivate toggleSubagentList(row: AgentsViewRow): void {\n\t\tif (!row.parentIdentity) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.expandedSubagentParents.has(row.parentIdentity)) {\n\t\t\tthis.expandedSubagentParents.delete(row.parentIdentity);\n\t\t\t// A collapsed agent's revealed program collapses with it, so reopening\n\t\t\t// starts from the hidden state rather than a stale reveal.\n\t\t\tthis.programShownParents.delete(row.parentIdentity);\n\t\t} else {\n\t\t\tthis.expandedSubagentParents.add(row.parentIdentity);\n\t\t}\n\t\tthis.rebuildRows();\n\t\tthis.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/**\n\t * Toggle the full spawn program for the agent owning the selected row:\n\t * one press shows it, another hides it. The subagent list is expanded as\n\t * needed so the code sits directly above the subagents it launched.\n\t */\n\tprivate cycleProgramForSelected(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row) {\n\t\t\treturn;\n\t\t}\n\t\tconst target = row.kind === \"agent\" ? row.identity : row.parentIdentity;\n\t\tif (!target) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.targetHasSpawnCode(target)) {\n\t\t\tthis.setStatusMessage(\"No program recorded for these subagents\");\n\t\t\treturn;\n\t\t}\n\t\t// Code only renders inside an expanded subagent list, so reveal it too.\n\t\tthis.expandedSubagentParents.add(target);\n\t\tif (this.programShownParents.has(target)) {\n\t\t\tthis.programShownParents.delete(target);\n\t\t} else {\n\t\t\tthis.programShownParents.add(target);\n\t\t}\n\t\tthis.rebuildRows();\n\t\tthis.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Whether any subagent under the given agent identity carries spawn code. */\n\tprivate targetHasSpawnCode(target: string): boolean {\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.parentIdentity !== target) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (row.kind === \"subagent-summary\") {\n\t\t\t\treturn row.hasSpawnCode === true;\n\t\t\t}\n\t\t\tif (row.kind === \"subagent\" && rowHasSpawnCode(row)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** True when the selected row exposes the \"show program\" affordance. */\n\tprivate selectedRowCanShowProgram(): boolean {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row) {\n\t\t\treturn false;\n\t\t}\n\t\tconst target = row.kind === \"agent\" ? row.identity : row.parentIdentity;\n\t\treturn target !== undefined && this.targetHasSpawnCode(target);\n\t}\n\n\tprivate openSelectedSubagent(row: AgentsViewRow): void {\n\t\tconst expandedAncestorSessionIds = this.collectSubagentAncestorSessionIds(row);\n\t\tif (row.summary.activeSessionId || row.summary.sessionFile) {\n\t\t\tthis.finish({\n\t\t\t\ttype: \"open\",\n\t\t\t\tsummary: row.summary,\n\t\t\t\texpandedAncestorSessionIds,\n\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\tgetAgentsViewSelectionKey(row.summary),\n\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst root = this.findSubagentRootRow(row);\n\t\tif (!root || !(root.summary.activeSessionId || root.summary.sessionFile)) {\n\t\t\tthis.setStatusMessage(\"Cannot open agent without an active runtime or saved session file\");\n\t\t\treturn;\n\t\t}\n\t\tthis.finish(\n\t\t\tcreateUnattachableChildOpenResult(\n\t\t\t\trow.summary,\n\t\t\t\troot.summary,\n\t\t\t\texpandedAncestorSessionIds,\n\t\t\t\thasUnifiedSessionChildren(this.unifiedRecords, getAgentsViewSelectionKey(root.summary), this.unifiedIndex),\n\t\t\t),\n\t\t);\n\t}\n\n\t/** Session ids of every ancestor of a subagent row, root-most first. */\n\tprivate collectSubagentAncestorSessionIds(row: AgentsViewRow): string[] {\n\t\tconst ancestors: string[] = [];\n\t\tlet parentIdentity = row.parentIdentity;\n\t\twhile (parentIdentity !== undefined) {\n\t\t\tconst parent = this.rows.find((candidate) => candidate.identity === parentIdentity);\n\t\t\tif (!parent) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tancestors.unshift(parent.summary.sessionId);\n\t\t\tparentIdentity = parent.parentIdentity;\n\t\t}\n\t\treturn ancestors;\n\t}\n\n\t/**\n\t * The whole subagent tree belongs to the root agent's session, so nested\n\t * subagents also resolve to their top-level ancestor.\n\t */\n\tprivate findSubagentRootRow(row: AgentsViewRow): AgentsViewRow | undefined {\n\t\tlet root = this.rows.find((candidate) => candidate.identity === row.parentIdentity);\n\t\twhile (root && root.kind !== \"agent\") {\n\t\t\tconst parentIdentity = root.parentIdentity;\n\t\t\troot = this.rows.find((candidate) => candidate.identity === parentIdentity);\n\t\t}\n\t\treturn root;\n\t}\n\n\tprivate async toggleReplyTarget(): Promise<void> {\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\t// Subagents are read-only; replying is reserved for top-level agents.\n\t\tif (selectedRow?.kind !== \"agent\") {\n\t\t\treturn;\n\t\t}\n\t\tconst summary = selectedRow.summary;\n\t\t// Live agents reply directly; saved sessions are resumed when the reply is\n\t\t// sent. Rows with neither runtime nor file have nothing to receive a prompt.\n\t\tif (!summary.activeSessionId && !summary.sessionFile) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.pendingDeleteAgent?.identity === getSelectedRowIdentity(selectedRow)) {\n\t\t\treturn;\n\t\t}\n\t\tconst key = summary.activeSessionId ?? summary.id;\n\t\tif (this.replyTarget?.key === key) {\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\treturn;\n\t\t}\n\t\tthis.setReplyTarget({ key, summary });\n\t\tif (!summary.activeSessionId) {\n\t\t\t// Inactive sessions have no live transcript endpoint; the persisted recap\n\t\t\t// (or opener) is the best preview and needs no daemon round-trip.\n\t\t\tthis.replyLastAssistantText = summary.summary ?? summary.firstMessage;\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tconst activeSessionId = summary.activeSessionId;\n\t\tthis.replyLastAssistantTextLoading = true;\n\t\ttry {\n\t\t\tconst latestAssistantText = await this.getLastAssistantText(activeSessionId);\n\t\t\tif (this.replyTarget?.key === key) {\n\t\t\t\tthis.replyLastAssistantText = latestAssistantText;\n\t\t\t\tthis.replyLastAssistantTextLoading = false;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (this.replyTarget?.key === key) {\n\t\t\t\tthis.replyLastAssistantTextLoading = false;\n\t\t\t\tthis.setStatusMessage(formatError(\"Failed to load latest response\", error));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setReplyTarget(target: { key: string; summary: SessionSummary } | undefined): void {\n\t\tif (target && !this.replyTarget) {\n\t\t\tthis.actionModeSearchQuery = this.editor.getText();\n\t\t\tthis.editor.setText(\"\");\n\t\t} else if (!target && this.replyTarget) {\n\t\t\tthis.editor.setText(this.actionModeSearchQuery ?? this.persistentState.query ?? \"\");\n\t\t\tthis.actionModeSearchQuery = undefined;\n\t\t}\n\t\tthis.replyTarget = target;\n\t\tthis.replyAutocomplete = target\n\t\t\t? createReplyComposerAutocompleteProvider(target.summary.cwd, this.fdPath)\n\t\t\t: undefined;\n\t\tthis.replyLastAssistantText = undefined;\n\t\tthis.replyLastAssistantTextLoading = false;\n\t\tthis.replyHeaderTime = target\n\t\t\t? formatAgentsViewRelativeTime(target.summary.modified ?? target.summary.created)\n\t\t\t: \"\";\n\t\tthis.editor.setPlaceholder(\n\t\t\ttarget\n\t\t\t\t? target.summary.activeSessionId\n\t\t\t\t\t? REPLY_PROMPT_FALLBACK_PLACEHOLDER\n\t\t\t\t\t: RESUME_PROMPT_PLACEHOLDER\n\t\t\t\t: SEARCH_PROMPT_PLACEHOLDER,\n\t\t);\n\t\tif (!target) this.rebuildRows();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate enterRenameMode(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\t// Only top-level agents carry a renameable session; subagents do not.\n\t\tif (row?.kind !== \"agent\" || !row.selectable) {\n\t\t\treturn;\n\t\t}\n\t\tconst activeSessionId = row.summary.activeSessionId;\n\t\tconst sessionFile = row.summary.sessionFile;\n\t\tif (!activeSessionId && !sessionFile) {\n\t\t\tthis.setStatusMessage(\"This session cannot be renamed\");\n\t\t\treturn;\n\t\t}\n\t\tthis.setReplyTarget(undefined);\n\t\tthis.actionModeSearchQuery = this.editor.getText();\n\t\tthis.pendingDeleteAgent = undefined;\n\t\tthis.pendingKillSubagent = undefined;\n\t\tthis.renameTarget = { activeSessionId, sessionFile, summary: row.summary };\n\t\tthis.editor.setPlaceholder(\"Name this agent session\");\n\t\tthis.editor.setText(row.summary.sessionName ?? \"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate exitRenameMode(): void {\n\t\tthis.renameTarget = undefined;\n\t\tthis.editor.setText(this.actionModeSearchQuery ?? this.persistentState.query ?? \"\");\n\t\tthis.actionModeSearchQuery = undefined;\n\t\tthis.editor.setPlaceholder(SEARCH_PROMPT_PLACEHOLDER);\n\t\tthis.rebuildRows();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async confirmRename(value: string): Promise<void> {\n\t\tconst target = this.renameTarget;\n\t\tif (!target) {\n\t\t\treturn;\n\t\t}\n\t\tconst name = value.trim();\n\t\tif (!name) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\t\tthis.exitRenameMode();\n\t\tawait this.renameSession(target.summary, name);\n\t}\n\n\t/** Shared by rename mode and /name: rename, refresh both catalogs, report. */\n\tprivate async renameSession(summary: SessionSummary, name: string): Promise<boolean> {\n\t\tthis.setStatusMessage(\"Renaming agent...\");\n\t\ttry {\n\t\t\tif (summary.activeSessionId) {\n\t\t\t\trequireDaemonData(\n\t\t\t\t\tawait this.requireClient().request({ type: \"rename\", activeSessionId: summary.activeSessionId, name }),\n\t\t\t\t);\n\t\t\t} else if (summary.sessionFile) {\n\t\t\t\tawait renameDaemonSavedSession(\n\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\tsummary.sessionFile,\n\t\t\t\t\tname,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.setStatusMessage(\"This session cannot be renamed\", { tone: \"warning\" });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst refreshed = await this.refreshBothCatalogs();\n\t\t\tthis.setStatusMessage(refreshed ? `Renamed to ${name}` : `Renamed to ${name}; refresh failed`);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(\n\t\t\t\tisUnknownDaemonCommandError(error, \"rename\")\n\t\t\t\t\t? \"Failed to rename: the daemon is running an older build; restart the daemon and try again\"\n\t\t\t\t\t: formatError(\"Failed to rename agent\", error),\n\t\t\t);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate findSummaryByActiveSessionId(activeSessionId: string): SessionSummary | undefined {\n\t\treturn this.rows.find((row) => (row.summary.activeSessionId ?? row.summary.id) === activeSessionId)?.summary;\n\t}\n\n\tprivate renderReplyHeaderLine(): string | undefined {\n\t\tif (this.renameTarget) {\n\t\t\treturn theme.fg(\"warning\", \"Rename agent session\");\n\t\t}\n\t\tif (!this.replyTarget) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst headline =\n\t\t\tcreateAgentsViewReplyHeadline(this.replyLastAssistantText) ??\n\t\t\ttheme.fg(\"dim\", this.replyLastAssistantTextLoading ? \"Loading last response...\" : \"No response yet\");\n\t\treturn this.replyHeaderTime ? `${theme.fg(\"warning\", this.replyHeaderTime)} ${headline}` : headline;\n\t}\n\n\tprivate async getLastAssistantText(activeSessionId: string): Promise<string | undefined> {\n\t\tconst response = await this.requireClient().request({\n\t\t\ttype: \"get_last_assistant_text\",\n\t\t\tactiveSessionId,\n\t\t});\n\t\tconst data = requireDaemonData(response);\n\t\tif (!isRecord(data)) {\n\t\t\tthrow new Error(\"Daemon returned an invalid last assistant response\");\n\t\t}\n\t\tif (data.text === null || data.text === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof data.text !== \"string\") {\n\t\t\tthrow new Error(\"Daemon returned an invalid last assistant response\");\n\t\t}\n\t\treturn data.text;\n\t}\n\n\tprivate async sendReply(\n\t\ttarget: { key: string; summary: SessionSummary },\n\t\ttext: string,\n\t\tdelivery: \"steer\" | \"followUp\" = \"steer\",\n\t): Promise<boolean> {\n\t\tconst currentSummary = resolveCurrentReplyTargetSummary(this.unifiedRecords ?? [], target, (activeSessionId) =>\n\t\t\tthis.findSummaryByActiveSessionId(activeSessionId),\n\t\t);\n\t\tlet activeSessionId = currentSummary.activeSessionId;\n\t\tlet liveSummary = activeSessionId ? currentSummary : undefined;\n\t\tlet cwdFallbackNotice: string | undefined;\n\t\tlet didResume = false;\n\t\ttry {\n\t\t\tif (!activeSessionId) {\n\t\t\t\t// Saved session: resume it into the daemon first, then deliver the\n\t\t\t\t// prompt through the same path as a live reply.\n\t\t\t\tthis.setStatusMessage(\"Resuming session...\");\n\t\t\t\tconst resumed = await resumeSavedAgentsViewSession(\n\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\tthis.options.config,\n\t\t\t\t\tcurrentSummary,\n\t\t\t\t);\n\t\t\t\tactiveSessionId = resumed.activeSessionId;\n\t\t\t\tdidResume = true;\n\t\t\t\t// The rows are still pre-resume; the fresh summary is the authoritative\n\t\t\t\t// streaming state for scheduling the prompt.\n\t\t\t\tliveSummary = resumed.summary;\n\t\t\t\tcwdFallbackNotice = resumed.cwdFallbackNotice;\n\t\t\t\tthis.inactiveAgentIdentities.delete(getSummaryIdentity(target.summary));\n\t\t\t\t// The resume and delivery still belong to this submission, but selection\n\t\t\t\t// belongs to the current composer. Do not steal it after cancellation.\n\t\t\t\tif (this.replyTarget === target) this.selectSummary(resumed.summary);\n\t\t\t}\n\t\t\tconst behavior = delivery === \"followUp\" ? \"followUp\" : liveSummary?.isStreaming ? \"steer\" : undefined;\n\t\t\tthis.setStatusMessage(\"Sending reply...\");\n\t\t\tawait this.sendPrompt(activeSessionId, text, behavior);\n\t\t\t// The fallback-directory notice must outlive the transient send statuses.\n\t\t\tif (cwdFallbackNotice) this.setStatusMessage(cwdFallbackNotice, { sticky: true });\n\t\t\telse this.setStatusMessage(\"Reply sent\");\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to send reply\", error));\n\t\t\tif (didResume) await this.refreshSessions({ preserveStatusOnError: true });\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Create a fresh daemon session and open it in the chat view. */\n\tprivate async createNewSession(): Promise<boolean> {\n\t\tif (this.creatingNewSession || this.stopped) return false;\n\t\tthis.creatingNewSession = true;\n\t\ttry {\n\t\t\tconst client = await this.connectDedicatedClient();\n\t\t\ttry {\n\t\t\t\tthis.setStatusMessage(\"Creating session...\");\n\t\t\t\tconst response = await client.request({\n\t\t\t\t\ttype: \"create\",\n\t\t\t\t\tconfig: this.options.config,\n\t\t\t\t\tenv: collectDaemonClientEnv(),\n\t\t\t\t});\n\t\t\t\tconst created = expectSessionSummary(requireDaemonData(response));\n\t\t\t\t// The view can finish mid-create; kill the fresh session instead of orphaning it.\n\t\t\t\tif (this.stopped) {\n\t\t\t\t\tif (created.activeSessionId) {\n\t\t\t\t\t\tawait client\n\t\t\t\t\t\t\t.request({ type: \"kill\", activeSessionId: created.activeSessionId })\n\t\t\t\t\t\t\t.catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tthis.selectSummary(created);\n\t\t\t\tthis.finish({ type: \"open\", summary: created });\n\t\t\t\treturn true;\n\t\t\t} finally {\n\t\t\t\tclient.close();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!this.stopped) this.setStatusMessage(formatError(\"Failed to create session\", error));\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis.creatingNewSession = false;\n\t\t}\n\t}\n\n\t/**\n\t * Run a view command against the armed composer's target. Returns whether\n\t * it completed so callers can restore the draft; disarms are guarded\n\t * against a composer re-armed during the awaited RPCs.\n\t */\n\tprivate async runAgentsViewCommand(command: AgentsViewCommand, target: SessionSummary): Promise<boolean> {\n\t\tconst armedAtStart = this.replyTarget;\n\t\tconst disarmIfUnchanged = () => {\n\t\t\tif (armedAtStart && this.replyTarget === armedAtStart) this.setReplyTarget(undefined);\n\t\t};\n\t\ttry {\n\t\t\tswitch (command.name) {\n\t\t\t\tcase \"name\": {\n\t\t\t\t\tconst name = command.args.trim();\n\t\t\t\t\tif (!name) {\n\t\t\t\t\t\tthis.setStatusMessage(\"Usage: /name <session name>\", { tone: \"warning\" });\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn await this.renameSession(target, name);\n\t\t\t\t}\n\t\t\t\tcase \"kill\": {\n\t\t\t\t\tif (!target.activeSessionId) {\n\t\t\t\t\t\tthis.setStatusMessage(\"/kill needs a running agent; this session is inactive\", { tone: \"warning\" });\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\trequireDaemonData(\n\t\t\t\t\t\t\tawait this.requireClient().request({ type: \"kill\", activeSessionId: target.activeSessionId }),\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// As in deactivatePendingAgent: an agent that already finished counts as stopped.\n\t\t\t\t\t\tif (!isUnknownActiveSessionError(error)) throw error;\n\t\t\t\t\t}\n\t\t\t\t\tdisarmIfUnchanged();\n\t\t\t\t\tthis.setStatusMessage(\"Agent stopped\");\n\t\t\t\t\tawait this.refreshBothCatalogs();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(`Failed to run /${command.name}`, error));\n\t\t\treturn false;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Outlives finish(), which closes the shared client and would reject an\n\t * in-flight create while the daemon still materializes the session.\n\t */\n\tprivate async connectDedicatedClient(): Promise<DaemonClient> {\n\t\treturn connectAgentsViewDaemonClient(this.requireSocketPath());\n\t}\n\n\t/** Point selection (and its persisted key) at a freshly resumed session row. */\n\tprivate selectSummary(summary: SessionSummary): void {\n\t\tthis.selectedRowIdentity = getSummaryIdentity(summary);\n\t\tthis.selectedActiveSessionId = summary.activeSessionId ?? summary.id;\n\t\tthis.selectedSessionKey = getAgentsViewSelectionKey(summary);\n\t\tthis.persistentState.selectedRowIdentity = this.selectedRowIdentity;\n\t\tthis.persistentState.selectedSessionKey = this.selectedSessionKey;\n\t}\n\n\tprivate async handleDeleteSelected(): Promise<void> {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row?.selectable) {\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent\") {\n\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\tawait this.handleKillSubagentSelected(row);\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind !== \"agent\") {\n\t\t\treturn;\n\t\t}\n\t\tthis.pendingKillSubagent = undefined;\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tif (!row.summary.activeSessionId && row.summary.sessionFile) {\n\t\t\tif (this.pendingDeleteAgent?.identity === identity && this.isDeleteConfirmationVisible()) {\n\t\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\t\ttry {\n\t\t\t\t\tconst latest = expectSessionList(\n\t\t\t\t\t\trequireDaemonData(await this.requireClient().request(createAgentsViewListCommand())),\n\t\t\t\t\t);\n\t\t\t\t\tthis.lastListedSummaries = latest;\n\t\t\t\t\tconst active = resolveAgentsViewActiveSummaryForPath(row.summary.sessionFile, latest);\n\t\t\t\t\tif (active) {\n\t\t\t\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\t\t\t\tthis.setStatusMessage(\"Session became active; stop it before deleting\", { tone: \"warning\" });\n\t\t\t\t\t\tawait this.refreshSessions();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst result = await deleteDaemonSavedSession(\n\t\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\t\trow.summary.sessionFile,\n\t\t\t\t\t);\n\t\t\t\t\tif (!result.ok) {\n\t\t\t\t\t\tthis.setStatusMessage(`Failed to delete session: ${result.error ?? \"Unknown error\"}`, {\n\t\t\t\t\t\t\ttone: \"error\",\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\t\t\tconst refreshed = await this.refreshSavedSessions({ preserveStatusOnError: true });\n\t\t\t\t\tconst success = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\t\tthis.setStatusMessage(refreshed ? success : `${success}; refresh failed`);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to delete session\", error));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tif (this.pendingDeleteAgent?.identity === identity) {\n\t\t\tif (this.isDeleteConfirmationVisible()) {\n\t\t\t\tawait this.deactivatePendingAgent();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tawait this.stopAgentForDeletion(row);\n\t}\n\n\tprivate async handleKillSubagentSelected(row: AgentsViewRow): Promise<void> {\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tif (this.pendingKillSubagent?.identity === identity && this.isDeleteConfirmationVisible()) {\n\t\t\tconst pending = this.pendingKillSubagent;\n\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\tawait this.killSubagent(pending, row);\n\t\t\treturn;\n\t\t}\n\t\tconst childId = row.summary.rlmChildId;\n\t\tconst rootActiveSessionId = this.findSubagentRootRow(row)?.summary.activeSessionId;\n\t\tif (!childId || !rootActiveSessionId) {\n\t\t\tthis.setStatusMessage(\"Cannot stop subagent without its parent agent\");\n\t\t\treturn;\n\t\t}\n\t\tthis.pendingKillSubagent = { identity, rootActiveSessionId, childId };\n\t\tthis.showDeleteConfirmation();\n\t}\n\n\tprivate async killSubagent(pending: PendingKillSubagent, currentRow: AgentsViewRow): Promise<void> {\n\t\tconst running = currentRow.section === \"running\";\n\t\tconst client = this.requireClient();\n\t\tthis.setStatusMessage(running ? \"Stopping subagent...\" : \"Deleting subagent...\");\n\t\ttry {\n\t\t\tif (!running && client.supportsServerCapability(\"delete_rlm_subagent\")) {\n\t\t\t\tconst data = requireDaemonData(\n\t\t\t\t\tawait client.request({\n\t\t\t\t\t\ttype: \"delete_rlm_subagent\",\n\t\t\t\t\t\tactiveSessionId: pending.rootActiveSessionId,\n\t\t\t\t\t\tchildId: pending.childId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tconst deleted = isRecord(data) && data.deleted === true;\n\t\t\t\tconst stillRunning = isRecord(data) && data.reason === \"running\";\n\t\t\t\tthis.setStatusMessage(\n\t\t\t\t\tdeleted\n\t\t\t\t\t\t? \"Subagent deleted\"\n\t\t\t\t\t\t: stillRunning\n\t\t\t\t\t\t\t? \"Subagent is running; stop it first\"\n\t\t\t\t\t\t\t: \"Subagent already removed\",\n\t\t\t\t\t{ render: false },\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst data = requireDaemonData(\n\t\t\t\t\tawait client.request({\n\t\t\t\t\t\ttype: \"cancel_rlm_child\",\n\t\t\t\t\t\tactiveSessionId: pending.rootActiveSessionId,\n\t\t\t\t\t\tchildId: pending.childId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tconst cancelled = isRecord(data) && data.cancelled === true;\n\t\t\t\tthis.setStatusMessage(\n\t\t\t\t\trunning\n\t\t\t\t\t\t? cancelled\n\t\t\t\t\t\t\t? \"Subagent stopped\"\n\t\t\t\t\t\t\t: \"Subagent already finished\"\n\t\t\t\t\t\t: \"The daemon cannot delete subagents; it was left unchanged\",\n\t\t\t\t\t{ render: false, ...(running ? {} : { tone: \"warning\" as const }) },\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tconst command = running ? \"cancel_rlm_child\" : \"delete_rlm_subagent\";\n\t\t\tthis.setStatusMessage(\n\t\t\t\tisUnknownDaemonCommandError(error, command)\n\t\t\t\t\t? \"Failed to update subagent: the daemon is running an older build; restart the daemon and try again\"\n\t\t\t\t\t: formatError(\"Failed to update subagent\", error),\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async stopAgentForDeletion(row: AgentsViewRow): Promise<void> {\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tconst activeSessionId = row.summary.activeSessionId;\n\t\tif (!activeSessionId) {\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tif (!isRunningSessionSummary(row.summary)) {\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tactiveSessionId,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tthis.setStatusMessage(\"Stopping agent...\");\n\t\ttry {\n\t\t\tconst response = await this.requireClient().request({\n\t\t\t\ttype: \"kill\",\n\t\t\t\tactiveSessionId,\n\t\t\t});\n\t\t\trequireDaemonData(response);\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tactiveSessionId,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: true,\n\t\t\t};\n\t\t\tthis.selectedActiveSessionId = activeSessionId;\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.showDeleteConfirmation();\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to stop agent\", error));\n\t\t}\n\t}\n\n\tprivate async deactivatePendingAgent(): Promise<void> {\n\t\tconst pending = this.pendingDeleteAgent;\n\t\tif (!pending) {\n\t\t\treturn;\n\t\t}\n\t\tthis.setStatusMessage(\"Deactivating agent...\");\n\t\ttry {\n\t\t\tif (pending.activeSessionId) {\n\t\t\t\ttry {\n\t\t\t\t\tconst killResponse = await this.requireClient().request({\n\t\t\t\t\t\ttype: \"kill\",\n\t\t\t\t\t\tactiveSessionId: pending.activeSessionId,\n\t\t\t\t\t});\n\t\t\t\t\trequireDaemonData(killResponse);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!isUnknownActiveSessionError(error)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Skip a file deleted between listing and now: SessionManager.open would\n\t\t\t// recreate a stub at the old path instead of loading it.\n\t\t\tif (pending.sessionFile && existsSync(pending.sessionFile)) {\n\t\t\t\t// Persist archived unless it already is: sessions with no prior\n\t\t\t\t// session_state entry would otherwise resurface on the next scan.\n\t\t\t\tconst sessionManager = SessionManager.open(pending.sessionFile, this.options.config.sessionDir);\n\t\t\t\tif (sessionManager.getSessionState()?.status !== \"archived\") {\n\t\t\t\t\tsessionManager.appendSessionState({ status: \"archived\" });\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.inactiveAgentIdentities.add(pending.identity);\n\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\tthis.selectedActiveSessionId = undefined;\n\t\t\tthis.setStatusMessage(\"Agent inactive\", { render: false });\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to deactivate agent\", error));\n\t\t}\n\t}\n\n\tprivate async sendPrompt(\n\t\tactiveSessionId: string,\n\t\tmessage: string,\n\t\tstreamingBehavior?: \"steer\" | \"followUp\",\n\t): Promise<void> {\n\t\tif (this.options.config.telemetryDisabled) {\n\t\t\tconst client = await this.connectDedicatedClient();\n\t\t\tconst connection = await DaemonAgentConnection.attach(client, activeSessionId, {\n\t\t\t\tcloseClientOnDispose: true,\n\t\t\t\tsupportsExtensionUi: false,\n\t\t\t\trecoverDaemon: this.options.recoverDaemon,\n\t\t\t\treconnectTimeoutMs: this.options.reconnectTimeoutMs,\n\t\t\t\ttelemetryDisabled: true,\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tawait connection.prompt(message, streamingBehavior === undefined ? undefined : { streamingBehavior });\n\t\t\t} finally {\n\t\t\t\tawait connection.dispose();\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tconst command: PromptCommand = { type: \"prompt\", activeSessionId, message };\n\t\tif (streamingBehavior) command.streamingBehavior = streamingBehavior;\n\t\tconst response = await this.requireClient().request(command);\n\t\trequireDaemonData(response);\n\t}\n\n\tprivate pollSessions(): void {\n\t\tif (this.liveCatalogPollPromise) return;\n\t\tconst poll = this.refreshSessions().then(() => undefined);\n\t\tthis.liveCatalogPollPromise = poll;\n\t\tvoid poll.finally(() => {\n\t\t\tif (this.liveCatalogPollPromise === poll) this.liveCatalogPollPromise = undefined;\n\t\t});\n\t}\n\n\tprivate async refreshSessions(options: { preserveStatusOnError?: boolean } = {}): Promise<boolean> {\n\t\tif (this.reconnectPromise || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.liveCatalogGeneration;\n\t\tthis.liveCatalogRefreshPending = true;\n\t\ttry {\n\t\t\tconst client = this.requireClient();\n\t\t\ttry {\n\t\t\t\tconst response = await client.request(createAgentsViewListCommand());\n\t\t\t\tif (generation !== this.liveCatalogGeneration) return false;\n\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\tthis.applySessionList(expectSessionList(requireDaemonData(response)), true);\n\t\t\t\treturn true;\n\t\t\t} catch (error) {\n\t\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\t\t// A completed failed attempt is still catalog-settled: otherwise a\n\t\t\t\t\t// vanished scope can permanently trap an empty view.\n\t\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\t\tif (!options.preserveStatusOnError && !this.reconnectPromise) {\n\t\t\t\t\t\tif (client.isConnected) this.setStatusMessage(formatError(\"Failed to refresh agents\", error));\n\t\t\t\t\t\telse this.startClientReconnect(client, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\tif (!options.preserveStatusOnError) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to refresh current session\", error));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\tthis.liveCatalogRefreshPending = false;\n\t\t\t\tthis.resolveMissingSelectionAnchor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applySessionList(sessions: SessionSummary[], successful = false): void {\n\t\tthis.lastListedSummaries = sessions;\n\t\tif (successful) this.persistentState.lastSuccessfulLiveSummaries = sessions;\n\t\tthis.reconcileCatalogs();\n\t}\n\n\tprivate reconcileCatalogs(): void {\n\t\tconst visibleSessions = this.lastListedSummaries.filter((summary) =>\n\t\t\tshouldShowAgentsViewSession(summary, this.inactiveAgentIdentities.has(getSummaryIdentity(summary))),\n\t\t);\n\t\tthis.lastVisibleSummaries = this.withPendingDeleteSession(visibleSessions);\n\t\tthis.unifiedRecords = reconcileUnifiedSessions(this.lastVisibleSummaries, this.savedSessions, this.heartbeats);\n\t\tthis.unifiedIndex = buildUnifiedSessionIndex(this.unifiedRecords);\n\t\tmigrateAgentsViewIdentitySet(this.expandedSubagentParents, this.unifiedIndex.byKey);\n\t\tmigrateAgentsViewIdentitySet(this.programShownParents, this.unifiedIndex.byKey);\n\n\t\tconst frames = this.persistentState.scopeFrames ?? [];\n\t\tconst resolution = resolveAgentsViewScopeFrames(this.unifiedRecords, frames, this.unifiedIndex);\n\t\tif (shouldApplyScopeResolution(resolution.droppedFrames, this.liveCatalogReady, this.savedCatalogReady)) {\n\t\t\tthis.persistentState.scopeFrames = resolution.frames;\n\t\t\tthis.scopeKey = resolution.frames.at(-1)?.scope;\n\t\t\tthis.scopeRootSummary = resolution.root ? summaryForUnifiedRecord(resolution.root) : undefined;\n\t\t\tthis.persistentState.scopeRootSummary = this.scopeRootSummary;\n\t\t\tif (resolution.droppedFrames > 0) {\n\t\t\t\tconst destination = resolution.root ? \"the nearest available parent\" : \"the global view\";\n\t\t\t\tthis.setStatusMessage(`Scope is no longer available; returned to ${destination}`, { render: false });\n\t\t\t}\n\t\t}\n\t\tthis.scopedRecords = scopeToSessionSubtree(this.unifiedRecords, this.scopeKey, this.unifiedIndex);\n\t\tthis.rows = buildAgentsViewRows(\n\t\t\tthis.getFilteredRecords(),\n\t\t\tthis.expandedSubagentParents,\n\t\t\tthis.programShownParents,\n\t\t\tthis.scopeKey,\n\t\t);\n\t\tthis.applyPendingAncestorExpansion();\n\t\tthis.restoreSelection();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** A session appears in both catalogs; mutations must refresh both. */\n\tprivate async refreshBothCatalogs(): Promise<boolean> {\n\t\tconst results = await Promise.all([\n\t\t\tthis.refreshSessions({ preserveStatusOnError: true }),\n\t\t\tthis.refreshSavedSessions({ preserveStatusOnError: true }),\n\t\t]);\n\t\treturn results.every(Boolean);\n\t}\n\n\tprivate async refreshSavedSessions(\n\t\toptions: { duringReconnect?: boolean; preserveStatusOnError?: boolean } = {},\n\t): Promise<boolean> {\n\t\tif ((!options.duringReconnect && this.reconnectPromise) || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.savedCatalogGeneration;\n\t\tthis.persistentState.savedCatalogGeneration = generation;\n\t\tthis.savedCatalogRefreshPending = true;\n\t\tthis.savedCatalogReady = false;\n\t\tconst successfulSessions = this.lastSuccessfulSavedSessions;\n\t\tconst progressiveSessions = new Map(\n\t\t\tsuccessfulSessions.map((session) => [resolvePath(canonicalizePath(session.path)), session]),\n\t\t);\n\t\ttry {\n\t\t\tconst onSession = (session: AgentConnectionSavedSessionInfo) => {\n\t\t\t\tif (generation !== this.savedCatalogGeneration) return;\n\t\t\t\tprogressiveSessions.set(resolvePath(canonicalizePath(session.path)), session);\n\t\t\t\tthis.savedSessions = [...progressiveSessions.values()];\n\t\t\t\tthis.persistentState.savedSessions = this.savedSessions;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t};\n\t\t\tconst sessions = await listDaemonSavedSessions(\n\t\t\t\tthis.requireClient(),\n\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\"all\",\n\t\t\t\t{\n\t\t\t\t\tonSession,\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (generation !== this.savedCatalogGeneration) return false;\n\t\t\tthis.savedSessions = sessions;\n\t\t\tthis.lastSuccessfulSavedSessions = sessions;\n\t\t\tthis.savedCatalogReady = true;\n\t\t\tthis.persistentState.lastSuccessfulSavedSessions = sessions;\n\t\t\tthis.persistentState.savedSessions = sessions;\n\t\t\tthis.reconcileCatalogs();\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (generation === this.savedCatalogGeneration) {\n\t\t\t\tthis.savedSessions = successfulSessions;\n\t\t\t\tthis.persistentState.savedSessions = successfulSessions;\n\t\t\t\t// Treat a terminal failure as settled so scope fallback cannot soft-lock.\n\t\t\t\tthis.savedCatalogReady = true;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\tif (!options.preserveStatusOnError && !this.reconnectPromise && !this.daemonShutdownReceived) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to load saved sessions\", error));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tif (generation === this.savedCatalogGeneration) {\n\t\t\t\tthis.savedCatalogRefreshPending = false;\n\t\t\t\tthis.resolveMissingSelectionAnchor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async refreshHeartbeats(options: { duringReconnect?: boolean } = {}): Promise<boolean> {\n\t\tif ((!options.duringReconnect && this.reconnectPromise) || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.heartbeatCatalogGeneration;\n\t\ttry {\n\t\t\tconst heartbeats = await listDaemonHeartbeats(this.requireClient());\n\t\t\tif (generation !== this.heartbeatCatalogGeneration) return false;\n\t\t\tthis.heartbeats = heartbeats;\n\t\t\tthis.persistentState.heartbeats = heartbeats;\n\t\t\tthis.reconcileCatalogs();\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (generation === this.heartbeatCatalogGeneration && !this.reconnectPromise) {\n\t\t\t\tthis.setStatusMessage(formatError(\"Failed to refresh heartbeats\", error));\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate withPendingDeleteSession(sessions: readonly SessionSummary[]): SessionSummary[] {\n\t\tconst pending = this.pendingDeleteAgent;\n\t\t// Saved-only rows already come from the durable catalog. Injecting their\n\t\t// synthetic archived summary as a daemon record would move confirmation\n\t\t// from Inactive to Idle.\n\t\tif (!pending || pending.summary.lifecycle !== \"live\") {\n\t\t\treturn [...sessions];\n\t\t}\n\t\tif (!this.isDeleteConfirmationVisible()) {\n\t\t\treturn [...sessions];\n\t\t}\n\t\tlet replaced = false;\n\t\tconst merged = sessions.map((summary) => {\n\t\t\tif (getSummaryIdentity(summary) !== pending.identity) {\n\t\t\t\treturn summary;\n\t\t\t}\n\t\t\treplaced = true;\n\t\t\treturn pending.summary;\n\t\t});\n\t\treturn replaced ? merged : [...merged, pending.summary];\n\t}\n\n\tprivate resolveMissingSelectionAnchor(): void {\n\t\tif (!this.selectionAnchorPending || this.liveCatalogRefreshPending || this.savedCatalogRefreshPending) {\n\t\t\treturn;\n\t\t}\n\t\t// Unblock the fallback row, but keep the anchor identities: a later poll\n\t\t// can still deliver the intended session and re-anchor selection to it.\n\t\tthis.selectionAnchorPending = false;\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = row?.selectable ? (row.summary.activeSessionId ?? row.summary.id) : undefined;\n\t}\n\n\tprivate restoreSelection(): void {\n\t\tif (this.rows.length === 0) {\n\t\t\tthis.selectedIndex = 0;\n\t\t\tthis.selectedActiveSessionId = undefined;\n\t\t\treturn;\n\t\t}\n\t\tconst resolution = resolveAgentsViewSelectionState(\n\t\t\tthis.rows,\n\t\t\tthis.selectedIndex,\n\t\t\tthis.selectedRowIdentity ?? this.persistentState.selectedRowIdentity,\n\t\t\tthis.selectedSessionKey ?? this.persistentState.selectedSessionKey,\n\t\t);\n\t\tthis.selectedIndex = resolution.index;\n\t\tif (resolution.resolved) {\n\t\t\tthis.syncSelectedRowState();\n\t\t\treturn;\n\t\t}\n\t\tthis.selectionAnchorPending = Boolean(\n\t\t\tthis.selectedRowIdentity ??\n\t\t\t\tthis.persistentState.selectedRowIdentity ??\n\t\t\t\tthis.selectedSessionKey ??\n\t\t\t\tthis.persistentState.selectedSessionKey,\n\t\t);\n\t\t// Catalogs stream independently. Show a temporary fallback row without\n\t\t// replacing the source-session anchor before its daemon row arrives.\n\t\tconst fallback = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = fallback?.selectable\n\t\t\t? (fallback.summary.activeSessionId ?? fallback.summary.id)\n\t\t\t: undefined;\n\t}\n\n\tprivate getSelectableRowIndexes(): number[] {\n\t\treturn this.rows.flatMap((row, index) => (row.selectable ? [index] : []));\n\t}\n\n\tprivate syncSelectedRowState(): void {\n\t\tthis.selectionAnchorPending = false;\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = row?.selectable ? (row.summary.activeSessionId ?? row.summary.id) : undefined;\n\t\tthis.selectedRowIdentity = getSelectedRowIdentity(row);\n\t\tthis.selectedSessionKey = row?.selectable ? getAgentsViewSelectionKey(row.summary) : undefined;\n\t\tthis.persistentState.selectedRowIdentity = this.selectedRowIdentity;\n\t\tthis.persistentState.selectedSessionKey = this.selectedSessionKey;\n\t}\n\n\tprivate finish(result: AgentsViewRunResult): void {\n\t\tif (this.stopped) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stopped = true;\n\t\tthis.savedCatalogGeneration += 1;\n\t\tthis.liveCatalogGeneration += 1;\n\t\tthis.heartbeatCatalogGeneration += 1;\n\t\tif (this.pollTimer) {\n\t\t\tclearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.heartbeatPollTimer) {\n\t\t\tclearInterval(this.heartbeatPollTimer);\n\t\t\tthis.heartbeatPollTimer = undefined;\n\t\t}\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = undefined;\n\t\t}\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\tthis.setStatusMessage(undefined, { render: false });\n\t\tthis.ui.stop({\n\t\t\tpreserveAltScreen: result.type !== \"exit\",\n\t\t\tflushFullscreen: false,\n\t\t});\n\t\tstopThemeWatcher();\n\t\tthis.unsubscribeClientClose?.();\n\t\tthis.unsubscribeClientClose = undefined;\n\t\tthis.unsubscribeClientMessage?.();\n\t\tthis.unsubscribeClientMessage = undefined;\n\t\tthis.client?.close();\n\t\tthis.client = undefined;\n\t\tthis.resolveRun?.(result);\n\t\tthis.resolveRun = undefined;\n\t}\n\n\tprivate subscribeToClientClose(client: DaemonClient): void {\n\t\tthis.unsubscribeClientClose?.();\n\t\tthis.unsubscribeClientClose = client.onClose((error) => {\n\t\t\tif (!shouldReconnectAgentsViewDaemon(getDaemonSocketCloseReason(error))) {\n\t\t\t\tthis.handleDaemonShutdown(client, error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.startClientReconnect(client, error);\n\t\t});\n\t}\n\n\tprivate handleDaemonShutdown(client: DaemonClient, error: Error): void {\n\t\tif (this.stopped || client !== this.client) {\n\t\t\treturn;\n\t\t}\n\t\tthis.daemonShutdownReceived = true;\n\t\tthis.reconnectTimedOut = false;\n\t\tthis.setStatusMessage(`ASM Agent daemon shut down. Restart ASM Agent to reconnect. ${error.message}`, {\n\t\t\ttone: \"error\",\n\t\t\tsticky: true,\n\t\t});\n\t\tthis.applySessionList([]);\n\t}\n\n\tprivate startClientReconnect(client: DaemonClient, error: unknown): void {\n\t\tif (this.stopped || client !== this.client || this.reconnectPromise || this.daemonShutdownReceived) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.reconnectTimedOut) {\n\t\t\tthis.setStatusMessage(\"Daemon connection lost; reconnecting…\", { tone: \"warning\", sticky: true });\n\t\t}\n\t\tconst reconnectPromise = this.reconnectClient(client, error).finally(() => {\n\t\t\tif (this.reconnectPromise === reconnectPromise) {\n\t\t\t\tthis.reconnectPromise = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.reconnectPromise = reconnectPromise;\n\t}\n\n\tprivate async reconnectClient(client: DaemonClient, initialError: unknown): Promise<void> {\n\t\tconst deadline = Date.now() + (this.options.reconnectTimeoutMs ?? RECONNECT_TIMEOUT_MS);\n\t\tlet lastError = initialError;\n\t\twhile (!this.stopped && !this.daemonShutdownReceived && client === this.client && Date.now() < deadline) {\n\t\t\ttry {\n\t\t\t\tawait this.options.recoverDaemon?.();\n\t\t\t\tawait client.reconnect(1000);\n\t\t\t\tconst response = await client.request(createAgentsViewListCommand());\n\t\t\t\tconst data = requireDaemonData(response);\n\t\t\t\tconst sessions = expectSessionList(data);\n\t\t\t\tconst heartbeatsRefreshed = await this.refreshHeartbeats({ duringReconnect: true });\n\t\t\t\tif (!heartbeatsRefreshed) throw new Error(\"Heartbeat catalog did not refresh during reconnect\");\n\t\t\t\tthis.daemonShutdownReceived = false;\n\t\t\t\tthis.reconnectTimedOut = false;\n\t\t\t\tthis.setStatusMessage(\"Daemon reconnected\", { render: false });\n\t\t\t\tthis.applySessionList(sessions, true);\n\t\t\t\tvoid this.refreshSavedSessions({ duringReconnect: true, preserveStatusOnError: true });\n\t\t\t\treturn;\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t}\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tconst retryTimer = setTimeout(resolve, RECONNECT_RETRY_MS);\n\t\t\t\tretryTimer.unref?.();\n\t\t\t});\n\t\t}\n\t\tif (!this.stopped && !this.daemonShutdownReceived && client === this.client) {\n\t\t\tthis.reconnectTimedOut = true;\n\t\t\tthis.setStatusMessage(formatError(\"Daemon unavailable; retrying\", lastError), {\n\t\t\t\ttone: \"error\",\n\t\t\t\tsticky: true,\n\t\t\t\trender: false,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate requireSocketPath(): string {\n\t\tif (!this.options.socketPath) throw new Error(\"Session view daemon socket is not configured\");\n\t\treturn this.options.socketPath;\n\t}\n\n\tprivate requireClient(): DaemonClient {\n\t\tif (!this.client) {\n\t\t\tthrow new Error(\"Agents view daemon client is not connected\");\n\t\t}\n\t\treturn this.client;\n\t}\n\n\tprivate getAgentCountsText(): string {\n\t\tconst counts = countRowsBySection(this.rows);\n\t\treturn `${counts.running} running, ${counts.idle} idle, ${counts.inactive} inactive`;\n\t}\n\n\tprivate renderSessionRows(width: number, maxRows: number): string[] {\n\t\tif (maxRows <= 0) {\n\t\t\treturn [];\n\t\t}\n\t\tif (this.rows.length === 0) {\n\t\t\treturn [theme.bold(sectionTitle(\"running\")), theme.fg(\"dim\", \" No sessions match your search.\")].slice(\n\t\t\t\t0,\n\t\t\t\tmaxRows,\n\t\t\t);\n\t\t}\n\n\t\tconst displayItems = buildDisplayItems(this.rows);\n\t\tconst selectedIdentity = this.rows[this.selectedIndex]?.identity;\n\t\tconst selectedDisplayIndex = displayItems.findIndex(\n\t\t\t(item) => item.type === \"row\" && item.row.identity === selectedIdentity,\n\t\t);\n\t\tconst visibleRows = Math.min(maxRows, this.visibleListRows());\n\t\tconst start = Math.max(\n\t\t\t0,\n\t\t\tMath.min(displayItems.length - visibleRows, selectedDisplayIndex - Math.floor(visibleRows / 2)),\n\t\t);\n\t\tconst showLeadingEllipsis = start > 0;\n\t\tlet showTrailingEllipsis = start + visibleRows < displayItems.length;\n\t\tif ((showLeadingEllipsis ? 1 : 0) + (showTrailingEllipsis ? 1 : 0) >= visibleRows) {\n\t\t\tshowTrailingEllipsis = false;\n\t\t}\n\t\tconst contentVisibleRows = Math.max(\n\t\t\t0,\n\t\t\tvisibleRows - (showLeadingEllipsis ? 1 : 0) - (showTrailingEllipsis ? 1 : 0),\n\t\t);\n\t\tconst visibleItems = displayItems.slice(start, start + contentVisibleRows);\n\t\tconst lines = visibleItems.map((item) => {\n\t\t\tif (item.type === \"spacer\") {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tif (item.type === \"heading\") {\n\t\t\t\treturn theme.bold(sectionTitle(item.section));\n\t\t\t}\n\t\t\tif (item.type === \"empty\") {\n\t\t\t\treturn theme.fg(\"dim\", \" No agents\");\n\t\t\t}\n\t\t\treturn this.renderRow(item.row, width);\n\t\t});\n\t\tif (showLeadingEllipsis) {\n\t\t\tlines.unshift(theme.fg(\"dim\", \" ...\"));\n\t\t}\n\t\tif (showTrailingEllipsis) {\n\t\t\tlines.push(theme.fg(\"dim\", \" ...\"));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderRow(row: AgentsViewRow, width: number): string {\n\t\tconst selected = row.selectable && row.identity === this.rows[this.selectedIndex]?.identity;\n\t\tif (row.kind === \"subagent-code\") {\n\t\t\treturn this.renderCodeRow(row);\n\t\t}\n\t\tif (row.kind === \"subagent-summary\") {\n\t\t\tconst indent = \" \".repeat(row.depth);\n\t\t\tconst hint = row.hasSpawnCode ? theme.fg(\"dim\", ` · ${keyText(\"app.agents.program\")} show program`) : \"\";\n\t\t\tconst label = `${theme.fg(\"dim\", `${row.expanded ? \"▾\" : \"▸\"} ${row.title}`)}${hint}`;\n\t\t\tconst line = padLine(truncateToWidth(`${indent}${label}`, width, \"\"), width);\n\t\t\treturn selected ? `${SELECTED_ROW_MARKER}${line}` : line;\n\t\t}\n\t\tconst pendingDelete = row.kind === \"agent\" && this.isPendingDeleteRow(row);\n\t\tconst pendingKill = row.kind === \"subagent\" && this.isPendingKillSubagentRow(row);\n\t\tconst rawIcon = this.getRowIcon(row.section);\n\t\tconst icon = this.formatRowIcon(row.section, rawIcon);\n\t\tconst indent = \" \".repeat(row.depth);\n\t\tconst age = formatSessionDuration(row.summary);\n\t\tconst details = row.section === \"inactive\" ? `${row.summary.messageCount} · ${age}` : age;\n\t\tconst detailsWidth = row.section === \"inactive\" ? Math.max(10, visibleWidth(details)) : 10;\n\t\tconst heartbeatBadge = !pendingDelete && !pendingKill ? formatHeartbeatBadge(row.heartbeat) : \"\";\n\t\tconst heartbeatCell = heartbeatBadge ? theme.fg(\"error\", heartbeatBadge) : \"\";\n\t\tconst heartbeatWidth = visibleWidth(heartbeatBadge);\n\t\tconst titleWidth = Math.max(\n\t\t\t0,\n\t\t\twidth -\n\t\t\t\tvisibleWidth(indent) -\n\t\t\t\tvisibleWidth(rawIcon) -\n\t\t\t\tdetailsWidth -\n\t\t\t\t2 -\n\t\t\t\t(heartbeatWidth > 0 ? heartbeatWidth + 1 : 0),\n\t\t);\n\t\tconst title = pendingDelete\n\t\t\t? this.getPendingDeleteTitle()\n\t\t\t: pendingKill\n\t\t\t\t? `${keyText(\"app.agents.delete\")} again to ${row.section === \"running\" ? \"stop\" : \"delete\"}`\n\t\t\t\t: styleRowTitle(row);\n\t\t// Keep stable model information ahead of the variable summary so narrow rows truncate the summary first.\n\t\tconst summaryText = !pendingDelete && !pendingKill ? row.summary.summary : undefined;\n\t\tconst modelLabel =\n\t\t\tisSubagentSummary(row.summary) && !pendingDelete && !pendingKill && row.summary.model\n\t\t\t\t? `${row.summary.model.provider}/${row.summary.model.id}${row.summary.thinkingLevel && row.summary.thinkingLevel !== \"off\" ? `:${row.summary.thinkingLevel}` : \"\"}`\n\t\t\t\t: undefined;\n\t\tconst suffixes = [modelLabel, summaryText].filter(\n\t\t\t(suffix): suffix is string => suffix !== undefined && suffix.length > 0,\n\t\t);\n\t\tconst titleContent = suffixes.length > 0 ? `${title} ${theme.fg(\"dim\", `· ${suffixes.join(\" · \")}`)}` : title;\n\t\tconst titleCell = formatTableCell(titleContent, titleWidth);\n\t\tconst cells = [\n\t\t\ticon,\n\t\t\tpendingDelete || pendingKill ? theme.fg(\"error\", titleCell) : titleCell,\n\t\t\tformatRightTableCell(details, detailsWidth),\n\t\t];\n\t\tconst base = `${indent}${cells[0]} ${heartbeatCell ? `${heartbeatCell} ` : \"\"}${cells[1]} ${cells[2]}`;\n\t\tconst line = padLine(truncateToWidth(base, width, \"\"), width);\n\t\treturn selected ? `${SELECTED_ROW_MARKER}${line}` : line;\n\t}\n\n\t// Spawn-code rows are read-only context. They render deemphasized — muted\n\t// text on a panel background (applied in finalizeRenderedLine) so the program\n\t// reads as one quiet segmented block rather than competing with agent rows.\n\tprivate renderCodeRow(row: AgentsViewRow): string {\n\t\tconst indent = \" \".repeat(row.depth);\n\t\tconst body = theme.fg(\"muted\", row.code || \" \");\n\t\treturn `${CODE_ROW_MARKER}${indent} ${body}`;\n\t}\n\n\tprivate finalizeRenderedLine(line: string, width: number): string {\n\t\tconst code = line.startsWith(CODE_ROW_MARKER);\n\t\tconst selected = !code && line.startsWith(SELECTED_ROW_MARKER);\n\t\tlet content = code\n\t\t\t? line.slice(CODE_ROW_MARKER.length)\n\t\t\t: selected\n\t\t\t\t? line.slice(SELECTED_ROW_MARKER.length)\n\t\t\t\t: line;\n\t\t// Each rendered line must occupy exactly one terminal row; a stray\n\t\t// newline would shift every line below it and overlap the editor.\n\t\tif (content.includes(\"\\n\") || content.includes(\"\\r\")) {\n\t\t\tcontent = content.replace(/[\\r\\n]+/g, \" \");\n\t\t}\n\t\tconst padded = padLine(truncateToWidth(content, width), width);\n\t\tif (code) {\n\t\t\treturn theme.bg(\"toolPanelBg\", padded);\n\t\t}\n\t\tif (!selected) {\n\t\t\treturn padded;\n\t\t}\n\t\t// Truncating styled cells embeds full \\x1b[0m resets; re-open the\n\t\t// selection background after each so the highlight spans the whole row.\n\t\tconst applySelectionBg = theme.getSelectionBackgroundColor();\n\t\treturn padded.split(\"\\x1b[0m\").map(applySelectionBg).join(\"\\x1b[0m\");\n\t}\n\n\tprivate isPendingDeleteRow(row: AgentsViewRow): boolean {\n\t\treturn (\n\t\t\tgetSummaryIdentity(row.summary) === this.pendingDeleteAgent?.identity && this.isDeleteConfirmationVisible()\n\t\t);\n\t}\n\n\tprivate isPendingKillSubagentRow(row: AgentsViewRow): boolean {\n\t\treturn (\n\t\t\tgetSummaryIdentity(row.summary) === this.pendingKillSubagent?.identity && this.isDeleteConfirmationVisible()\n\t\t);\n\t}\n\n\tprivate getPendingDeleteTitle(): string {\n\t\tconst deleteKey = keyText(\"app.agents.delete\");\n\t\treturn this.pendingDeleteAgent?.stopped\n\t\t\t? `stopped - ${deleteKey} again to remove`\n\t\t\t: `${deleteKey} again to remove`;\n\t}\n\n\tprivate renderPrompt(width: number): string[] {\n\t\treturn this.editor.render(width);\n\t}\n\n\tprivate renderDock(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\treturn [this.renderHints(safeWidth)].map((line) => this.finalizeRenderedLine(line, safeWidth));\n\t}\n\n\tprivate renderHints(width: number): string {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tconst clearKey = keyText(\"app.clear\");\n\t\t\tconst hint = clearKey ? `Press ${clearKey} again to exit` : \"Press again to exit\";\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", hint), width);\n\t\t}\n\t\tif (this.statusMessage) {\n\t\t\treturn truncateToWidth(theme.fg(this.statusMessageTone, this.statusMessage), width);\n\t\t}\n\t\tif (this.renameTarget) {\n\t\t\tconst hint = `${keyText(\"tui.select.confirm\")} save ${keyText(\"tui.select.cancel\")} cancel`;\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", hint), width);\n\t\t}\n\t\tif (this.replyTarget) {\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", this.renderReplyComposerHints()), width);\n\t\t}\n\t\t// Replying is reserved for top-level agents; subagents can be stopped or deleted.\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\tconst selectedAgent = selectedRow?.kind === \"agent\";\n\t\tconst selectedSubagent = selectedRow?.kind === \"subagent\";\n\t\tconst selectedSummary = selectedRow?.kind === \"subagent-summary\";\n\t\tconst hints = [\n\t\t\t`${keyText(\"tui.select.up\")}/${keyText(\"tui.select.down\")} move`,\n\t\t\tselectedSummary\n\t\t\t\t? `${keyText(\"tui.select.confirm\")} ${selectedRow?.expanded ? \"collapse\" : \"expand\"}`\n\t\t\t\t: `${keyText(\"tui.select.confirm\")} open`,\n\t\t\tselectedSummary ? undefined : `${keyText(\"app.agents.open\")} open`,\n\t\t\tselectedAgent\n\t\t\t\t? `${keyText(\"app.agents.reply\")} ${selectedRow?.section === \"inactive\" ? \"resume\" : \"reply\"}`\n\t\t\t\t: undefined,\n\t\t\t`${keyText(\"app.agents.new\")} new`,\n\t\t\tselectedAgent ? `${keyText(\"app.agents.rename\")} rename` : undefined,\n\t\t\tselectedAgent\n\t\t\t\t? `${keyText(\"app.agents.delete\")} ${selectedRow?.section === \"inactive\" ? \"delete\" : \"stop/deactivate\"}`\n\t\t\t\t: undefined,\n\t\t\tselectedSubagent\n\t\t\t\t? `${keyText(\"app.agents.delete\")} ${selectedRow.section === \"running\" ? \"stop\" : \"delete\"}`\n\t\t\t\t: undefined,\n\t\t\tthis.selectedRowCanShowProgram() ? `${keyText(\"app.agents.program\")} program` : undefined,\n\t\t]\n\t\t\t.filter((hint): hint is string => hint !== undefined)\n\t\t\t.join(\" \");\n\t\treturn truncateToWidth(theme.fg(\"muted\", hints), width);\n\t}\n\n\tprivate renderReplyComposerHints(): string {\n\t\tconst target = this.replyTarget!;\n\t\tconst current = resolveCurrentReplyTargetSummary(this.unifiedRecords ?? [], target, (activeSessionId) =>\n\t\t\tthis.findSummaryByActiveSessionId(activeSessionId),\n\t\t);\n\t\tconst streaming = current.activeSessionId !== undefined && current.isStreaming;\n\t\tconst hasText = this.editor.getText().trim().length > 0;\n\t\treturn [\n\t\t\t`${keyText(\"tui.select.confirm\")} ${streaming ? \"steer\" : current.activeSessionId ? \"send\" : \"resume & send\"}`,\n\t\t\thasText ? `${keyText(\"app.message.followUp\")} queue` : undefined,\n\t\t\t`${keyText(\"tui.select.cancel\")} cancel`,\n\t\t]\n\t\t\t.filter((hint): hint is string => hint !== undefined)\n\t\t\t.join(\" \");\n\t}\n\n\tprivate visibleListRows(): number {\n\t\treturn Math.max(4, this.ui.terminal.rows - 9);\n\t}\n\n\tprivate contentHeight(width: number): number {\n\t\tconst rows = this.ui.terminal.rows;\n\t\tconst dockHeight = clippedFullscreenDockHeight(this.renderDock(width).length, rows);\n\t\treturn Math.max(0, rows - dockHeight);\n\t}\n\n\tprivate getSplashModelId(): string | undefined {\n\t\treturn this.rows[this.selectedIndex]?.summary.model?.id ?? this.options.startupModelId;\n\t}\n\n\tprivate getSplashCwd(): string {\n\t\treturn this.rows[this.selectedIndex]?.summary.cwd ?? this.options.uiServices.getInitialCwd();\n\t}\n\n\tprivate getRowIcon(section: AgentsViewSection): string {\n\t\tswitch (section) {\n\t\t\tcase \"running\":\n\t\t\t\treturn workingIconFrame(this.workingIconFrame);\n\t\t\tcase \"idle\":\n\t\t\t\treturn NEEDS_INPUT_ROW_ICON;\n\t\t\tcase \"inactive\":\n\t\t\t\treturn COMPLETED_ROW_ICON;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = section;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatRowIcon(section: AgentsViewSection, icon: string): string {\n\t\tswitch (section) {\n\t\t\tcase \"running\":\n\t\t\t\treturn theme.bold(icon);\n\t\t\tcase \"idle\":\n\t\t\t\treturn theme.fg(\"warning\", icon);\n\t\t\tcase \"inactive\":\n\t\t\t\treturn theme.fg(\"dim\", icon);\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = section;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype DisplayItem =\n\t| { type: \"spacer\" }\n\t| { type: \"heading\"; section: AgentsViewSection }\n\t| { type: \"empty\"; section: AgentsViewSection }\n\t| { type: \"row\"; row: AgentsViewRow };\n\nfunction buildDisplayItems(rows: readonly AgentsViewRow[]): DisplayItem[] {\n\tconst items: DisplayItem[] = [];\n\tconst sections: AgentsViewSection[] = [\"running\", \"idle\", \"inactive\"];\n\tfor (const [index, section] of sections.entries()) {\n\t\tif (index > 0) {\n\t\t\titems.push({ type: \"spacer\" });\n\t\t}\n\t\titems.push({ type: \"heading\", section });\n\t\tconst sectionRows = getDisplayRowsForSection(rows, section);\n\t\tif (sectionRows.length === 0) {\n\t\t\titems.push({ type: \"empty\", section });\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const row of sectionRows) {\n\t\t\titems.push({ type: \"row\", row });\n\t\t}\n\t}\n\treturn items;\n}\n\n// Nested rows (subagent summaries and expanded subagents) always render in\n// their top-level agent's section block, regardless of their own section.\nfunction getDisplayRowsForSection(rows: readonly AgentsViewRow[], section: AgentsViewSection): AgentsViewRow[] {\n\tconst result: AgentsViewRow[] = [];\n\tlet include = false;\n\tfor (const row of rows) {\n\t\tif (row.depth === 0) {\n\t\t\tinclude = row.section === section;\n\t\t}\n\t\tif (include) {\n\t\t\tresult.push(row);\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction countRowsBySection(rows: readonly AgentsViewRow[]): Record<AgentsViewSection, number> {\n\tconst agents = rows.filter((row) => row.kind === \"agent\");\n\treturn {\n\t\trunning: agents.filter((row) => row.section === \"running\").length,\n\t\tidle: agents.filter((row) => row.section === \"idle\").length,\n\t\tinactive: agents.filter((row) => row.section === \"inactive\").length,\n\t};\n}\n\nfunction getSelectedRowIdentity(row: AgentsViewRow | undefined): string | undefined {\n\treturn row?.identity;\n}\n\nfunction rowHasSpawnCode(row: AgentsViewRow): boolean {\n\tconst code = row.summary.spawnCode;\n\treturn typeof code === \"string\" && code.trim().length > 0;\n}\n\nfunction isRunningSessionSummary(summary: SessionSummary): boolean {\n\treturn summary.activity === \"working\";\n}\n\n// Explicit session names read bold so they stand out from fallback titles\n// (first prompt, cwd, ids); the \"(no messages)\" placeholder reads italic.\nfunction styleRowTitle(row: AgentsViewRow): string {\n\tif (row.summary.sessionName?.replace(/\\s+/g, \" \").trim()) {\n\t\treturn theme.bold(row.title);\n\t}\n\tif (row.title === \"(no messages)\") {\n\t\treturn theme.italic(row.title);\n\t}\n\treturn row.title;\n}\n\nfunction formatTableCell(value: string, width: number): string {\n\tconst truncated = truncateToWidth(value, width, \"\");\n\treturn truncated + \" \".repeat(Math.max(0, width - visibleWidth(truncated)));\n}\n\nfunction formatRightTableCell(value: string, width: number): string {\n\tconst truncated = truncateToWidth(value, width, \"\");\n\treturn \" \".repeat(Math.max(0, width - visibleWidth(truncated))) + truncated;\n}\n\nfunction formatSessionDuration(summary: SessionSummary): string {\n\treturn formatAgentsViewRelativeTime(\n\t\tsummary.activeSessionId ? (summary.created ?? summary.modified) : (summary.modified ?? summary.created),\n\t);\n}\n\nexport function formatAgentsViewRelativeTime(value: string | undefined, now: number = Date.now()): string {\n\tconst timestamp = parseSessionTimestamp(value);\n\tif (!timestamp) {\n\t\treturn \"\";\n\t}\n\tconst seconds = Math.max(0, Math.floor((now - timestamp) / 1000));\n\tif (seconds < 60) {\n\t\treturn `${seconds}s`;\n\t}\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) {\n\t\treturn `${minutes}m`;\n\t}\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) {\n\t\treturn `${hours}h`;\n\t}\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d`;\n}\n\nfunction parseSessionTimestamp(value: string | undefined): number | undefined {\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\tconst timestamp = Date.parse(value);\n\treturn Number.isNaN(timestamp) ? undefined : timestamp;\n}\n\nfunction requireDaemonData(response: DaemonResponse): unknown {\n\tif (!response.success) {\n\t\tthrow new Error(response.error);\n\t}\n\treturn response.data;\n}\n\nfunction expectSessionList(value: unknown): SessionSummary[] {\n\tif (!isRecord(value) || !Array.isArray(value.sessions)) {\n\t\tthrow new Error(\"Daemon returned an invalid session list response\");\n\t}\n\tif (!value.sessions.every(isSessionSummary)) {\n\t\tthrow new Error(\"Daemon returned an invalid session summary\");\n\t}\n\treturn value.sessions;\n}\n\nfunction expectSessionSummary(value: unknown): SessionSummary {\n\tif (!isSessionSummary(value)) {\n\t\tthrow new Error(\"Daemon returned an invalid session summary\");\n\t}\n\treturn value;\n}\n\nfunction isSessionSummary(value: unknown): value is SessionSummary {\n\treturn isRecord(value) && typeof value.id === \"string\" && typeof value.sessionId === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction formatError(prefix: string, error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn formatAgentsViewStatusLine(`${prefix}: ${message}`);\n}\n\n// The agents view shows open failures as a one-line status only, so a client-side\n// crash (e.g. \"Maximum call stack size exceeded\") leaves no stack to debug from.\n// Persist the full stack to a file — the TUI owns stdout/stderr, so a log file is\n// the only safe sink.\nfunction logClientError(prefix: string, error: unknown): void {\n\tconst detail = error instanceof Error ? (error.stack ?? error.message) : String(error);\n\tappendRotatingLog(getClientErrorLogPath(), `[${new Date().toISOString()}] ${prefix}: ${detail}`);\n}\n\nfunction padLine(line: string, width: number): string {\n\treturn line + \" \".repeat(Math.max(0, width - visibleWidth(line)));\n}\n"]}
1
+ {"version":3,"file":"agents-view-mode.d.ts","sourceRoot":"","sources":["../../../src/modes/agents-view/agents-view-mode.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync } from \"node:fs\";\nimport { resolve as resolvePath } from \"node:path\";\nimport {\n\ttype AutocompleteProvider,\n\tCombinedAutocompleteProvider,\n\ttype Component,\n\tclippedFullscreenDockHeight,\n\ttype Focusable,\n\tProcessTerminal,\n\tsetKeybindings,\n\tTUI,\n\ttruncateToWidth,\n\tvisibleWidth,\n\twrapTextWithAnsi,\n} from \"@asm-agent/tui\";\nimport { APP_TITLE, appendRotatingLog, getAgentDir, getClientErrorLogPath, VERSION } from \"../../config.js\";\nimport type { AgentSessionRuntimeConfig } from \"../../core/agent-session-config.js\";\nimport { KeybindingsManager } from \"../../core/keybindings.js\";\nimport { SessionManager } from \"../../core/session-manager.js\";\nimport {\n\tBUILTIN_SLASH_COMMANDS,\n\tisBuiltinSlashCommandName,\n\tisSessionSlashCommandName,\n\tparseSlashCommand,\n\tresolveBuiltinSlashCommandName,\n} from \"../../core/slash-commands.js\";\nimport { canonicalizePath } from \"../../utils/paths.js\";\nimport { ensureTool } from \"../../utils/tools-manager.js\";\nimport { DaemonAgentConnection } from \"../agent-connection/daemon-agent-connection.js\";\nimport type { AgentConnectionHeartbeat, AgentConnectionSavedSessionInfo } from \"../agent-connection/types.js\";\nimport { DaemonClient, getDaemonSocketCloseReason } from \"../daemon/daemon-client.js\";\nimport {\n\tcollectDaemonClientEnv,\n\ttype DaemonClosingReason,\n\ttype DaemonCommand,\n\ttype DaemonResponse,\n\tisUnknownDaemonCommandError,\n} from \"../daemon/daemon-protocol.js\";\nimport { resolveAttachModelFallbackMessage, type SessionSummary } from \"../daemon/daemon-session-list.js\";\nimport { listDaemonHeartbeats } from \"../daemon/heartbeat-catalog.js\";\nimport {\n\ttype DaemonSavedSessionCatalogContext,\n\tdeleteDaemonSavedSession,\n\tlistDaemonSavedSessions,\n\trenameDaemonSavedSession,\n} from \"../daemon/saved-session-catalog.js\";\nimport { CustomEditor } from \"../interactive/components/custom-editor.js\";\nimport { keyText } from \"../interactive/components/keybinding-hints.js\";\nimport { BrandSplashHeader, InteractiveMode } from \"../interactive/interactive-mode.js\";\nimport type { InteractiveModeUiServices } from \"../interactive/interactive-mode-services.js\";\nimport { ClientPromptStashStore } from \"../interactive/prompt-stash-state.js\";\nimport {\n\tgetEditorTheme,\n\tinitTheme,\n\tonThemeChange,\n\tsetRegisteredThemes,\n\tstopThemeWatcher,\n\ttheme,\n} from \"../interactive/theme/theme.js\";\nimport { WORKING_ICON_INTERVAL_MS, workingIconFrame } from \"../interactive/theme/working-icon.js\";\nimport {\n\tformatPackageUpdateNotice,\n\tformatTmuxWarningNotice,\n\tformatUpdateAvailableNotice,\n\tgatherStartupNotices,\n\ttype StartupNotices,\n} from \"../shared/startup-notices.js\";\nimport {\n\ttype AgentsViewRow,\n\ttype AgentsViewScopeFrame,\n\ttype AgentsViewScopeKey,\n\ttype AgentsViewSection,\n\ttype AgentsViewSelectionKey,\n\tbuildAgentsViewRows,\n\tbuildUnifiedSessionIndex,\n\tcreateUnattachableChildOpenResult,\n\tfilterUnifiedSessions,\n\tformatHeartbeatBadge,\n\tgetAgentsViewSelectionKey,\n\tgetAgentsViewSessionTitle,\n\tgetAgentsViewSummaryIdentity as getSummaryIdentity,\n\tgetUnifiedSessionAncestorSessionIds,\n\thasUnifiedSessionChildren,\n\tisSubagentSummary,\n\tmigrateAgentsViewIdentitySet,\n\treconcileUnifiedSessions,\n\tresolveAgentsViewLeftResult,\n\tresolveAgentsViewScopeFrames,\n\tresolveAgentsViewSelectionState,\n\tscopeToSessionSubtree,\n\tsectionTitle,\n\tshouldApplyScopeResolution,\n\tshouldShowAgentsViewSession,\n\tsummaryForUnifiedRecord,\n\ttransitionAgentsViewScope,\n\ttype UnifiedSessionIndex,\n\ttype UnifiedSessionRecord,\n} from \"./agents-view-state.js\";\nimport { matchesSearchText } from \"./session-view-search.js\";\n\nconst POLL_INTERVAL_MS = 1000;\nconst HEARTBEAT_POLL_INTERVAL_MS = 15000;\nconst RECONNECT_TIMEOUT_MS = 120000;\nconst RECONNECT_RETRY_MS = 1000;\nconst EXIT_HINT_DURATION_MS = 2000;\nconst DELETE_CONFIRM_DURATION_MS = 2000;\nconst STATUS_MESSAGE_DURATION_MS = 4500;\nconst SEARCH_PROMPT_PLACEHOLDER = \"Search sessions\";\nconst REPLY_PROMPT_FALLBACK_PLACEHOLDER = \"Write a reply to this agent\";\nconst RESUME_PROMPT_PLACEHOLDER = \"Write a prompt to resume this session\";\nconst COMPLETED_ROW_ICON = \"✓\";\nconst NEEDS_INPUT_ROW_ICON = \"●\";\nconst SELECTED_ROW_MARKER = \"\\0agents-view-selected-row\\0\";\nconst CODE_ROW_MARKER = \"\\0agents-view-code-row\\0\";\n\nexport interface AgentsViewModeOptions {\n\tsocketPath?: string;\n\tconfig: AgentSessionRuntimeConfig;\n\tuiServices: InteractiveModeUiServices;\n\tcreateUiServicesForSession?: (summary: SessionSummary) => Promise<InteractiveModeUiServices>;\n\tmigratedProviders?: string[];\n\tmodelFallbackMessage?: string;\n\tstartupModelId?: string;\n\tverbose?: boolean;\n\trecoverDaemon?: () => Promise<void>;\n\treconnectTimeoutMs?: number;\n\tpromptStashStore?: ClientPromptStashStore;\n\tinitialSession?: SessionSummary;\n\t/** When set, the first view is rooted at this session's direct children. */\n\tinitialScopeKey?: AgentsViewScopeKey;\n}\n\nexport type AgentsViewRunResult =\n\t| { type: \"exit\" }\n\t| {\n\t\t\ttype: \"scope_back\";\n\t\t\tselection: SessionSummary;\n\t\t\texpandedAncestorSessionIds: string[];\n\t\t\treturnChat?: SessionSummary;\n\t\t\thasChildren: boolean;\n\t }\n\t| {\n\t\t\ttype: \"open\";\n\t\t\tsummary: SessionSummary;\n\t\t\t/** Row restored after chat closes; differs from summary only for an unattachable-child fallback. */\n\t\t\tselection?: SessionSummary;\n\t\t\texpandedAncestorSessionIds?: string[];\n\t\t\thasChildren?: boolean;\n\t\t\tstatusMessage?: string;\n\t };\nexport type AgentsViewPersistentState = {\n\tselectedRowIdentity?: string;\n\tbackSession?: SessionSummary;\n\tscopeFrames?: AgentsViewScopeFrame[];\n\tscopeRootSummary?: SessionSummary;\n\tselectedSessionKey?: AgentsViewSelectionKey;\n\t// Ancestor chain to re-expand on return to a nested agent. Kept by sessionId,\n\t// not row identity, so it survives an active→persisted identity flip.\n\tpendingExpandedAncestorSessionIds?: string[];\n\texpandedSubagentParents?: Set<string>;\n\tprogramShownParents?: Set<string>;\n\tstatusMessage?: string;\n\t// Gathered once and reused across agents-view instances so the notices survive\n\t// re-entry and render the moment they resolve, even if the first view was left early.\n\tstartupNotices?: StartupNotices;\n\tstartupNoticesPromise?: Promise<StartupNotices>;\n\tquery?: string;\n\tsavedSessions?: AgentConnectionSavedSessionInfo[];\n\tlastSuccessfulSavedSessions?: AgentConnectionSavedSessionInfo[];\n\tlastSuccessfulLiveSummaries?: SessionSummary[];\n\tsavedCatalogGeneration?: number;\n\theartbeats?: AgentConnectionHeartbeat[];\n};\n\ntype PromptCommand = Extract<DaemonCommand, { type: \"prompt\" }>;\ntype PendingDeleteAgent = {\n\tidentity: string;\n\tactiveSessionId?: string;\n\tsessionFile?: string;\n\tsummary: SessionSummary;\n\tstopped: boolean;\n};\ntype PendingKillSubagent = {\n\tidentity: string;\n\trootActiveSessionId: string;\n\tchildId: string;\n};\n\nexport async function resolveAgentsViewSessionUiServices(\n\toptions: Pick<AgentsViewModeOptions, \"createUiServicesForSession\" | \"uiServices\">,\n\tsummary: SessionSummary,\n): Promise<InteractiveModeUiServices> {\n\treturn options.createUiServicesForSession ? await options.createUiServicesForSession(summary) : options.uiServices;\n}\n\n// Stripping cwd opens the session in its own stored directory; overrideCwd is\n// sent when that directory no longer exists so the daemon doesn't reject it.\nexport function createAgentsViewResumeConfig(\n\tconfig: AgentSessionRuntimeConfig,\n\toverrideCwd?: string,\n): AgentSessionRuntimeConfig {\n\tconst resumeConfig: AgentSessionRuntimeConfig = { ...config };\n\tif (overrideCwd) {\n\t\tresumeConfig.cwd = overrideCwd;\n\t} else {\n\t\tdelete resumeConfig.cwd;\n\t}\n\treturn resumeConfig;\n}\n\nexport function createAgentsViewListCommand(): Extract<DaemonCommand, { type: \"list\" }> {\n\t// Omitting `all` returns daemon-resident sessions only; on-disk ones come back\n\t// through the view's saved-session catalog.\n\treturn { type: \"list\" };\n}\n\nexport function resolveAgentsViewActiveSummaryForPath(\n\tsessionPath: string,\n\tsummaries: readonly SessionSummary[],\n): SessionSummary | undefined {\n\tconst selectedPath = resolvePath(canonicalizePath(sessionPath));\n\treturn summaries.find(\n\t\t(summary) =>\n\t\t\tsummary.activeSessionId !== undefined &&\n\t\t\tsummary.sessionFile !== undefined &&\n\t\t\tresolvePath(canonicalizePath(summary.sessionFile)) === selectedPath,\n\t);\n}\n\n// Status messages render in a single-row hint slot below the editor; embedded\n// newlines would make that row taller than the layout accounts for and overlap\n// the input, so flatten all whitespace runs to single spaces.\nexport function formatAgentsViewStatusLine(text: string): string {\n\treturn text.replace(/\\s+/g, \" \").trim();\n}\n\nexport function combineAgentsViewStartupNotices(...notices: readonly (string | undefined)[]): string | undefined {\n\tconst formatted = notices\n\t\t.map((notice) => (notice ? formatAgentsViewStatusLine(notice) : \"\"))\n\t\t.filter((notice) => notice.length > 0);\n\treturn formatted.length > 0 ? formatted.join(\" · \") : undefined;\n}\n\nexport function shouldReconnectAgentsViewDaemon(reason: DaemonClosingReason | undefined): boolean {\n\treturn reason !== \"shutdown\";\n}\n\nexport function createAgentsViewReplyHeadline(text: string | undefined): string | undefined {\n\treturn text\n\t\t?.split(\"\\n\")\n\t\t.map((line) => line.replace(/\\s+/g, \" \").trim())\n\t\t.find((line) => line.length > 0);\n}\n\nexport function getAgentsViewDepth(scopeRoot: SessionSummary | undefined): number {\n\treturn scopeRoot ? (scopeRoot.rlmDepth ?? 0) + 1 : 0;\n}\n\nexport function createInitialAgentsViewScopeFrames(\n\tinitialScopeKey: AgentsViewScopeKey | undefined,\n\treturnChat: SessionSummary | undefined,\n): AgentsViewScopeFrame[] {\n\tif (!initialScopeKey) return [];\n\treturn [\n\t\t{\n\t\t\tscope: initialScopeKey,\n\t\t\t...(returnChat?.sessionId === initialScopeKey.sessionId ? { returnChat } : {}),\n\t\t},\n\t];\n}\n\nexport function createInitialAgentsViewPersistentState(\n\toptions: Pick<AgentsViewModeOptions, \"initialScopeKey\" | \"initialSession\">,\n): AgentsViewPersistentState {\n\tconst initialSession = options.initialSession;\n\treturn {\n\t\t...(initialSession\n\t\t\t? {\n\t\t\t\t\tselectedRowIdentity: getSummaryIdentity(initialSession),\n\t\t\t\t\tselectedSessionKey: getAgentsViewSelectionKey(initialSession),\n\t\t\t\t\tbackSession: initialSession,\n\t\t\t\t}\n\t\t\t: {}),\n\t\t...(options.initialScopeKey\n\t\t\t? {\n\t\t\t\t\tscopeFrames: createInitialAgentsViewScopeFrames(options.initialScopeKey, initialSession),\n\t\t\t\t\t...(initialSession ? { lastSuccessfulLiveSummaries: [initialSession] } : {}),\n\t\t\t\t}\n\t\t\t: {}),\n\t};\n}\n\nexport function createScopeBackReturnChatOpenResult(\n\tresult: Extract<AgentsViewRunResult, { type: \"scope_back\" }>,\n): Extract<AgentsViewRunResult, { type: \"open\" }> | undefined {\n\tif (!result.returnChat) return undefined;\n\treturn {\n\t\ttype: \"open\",\n\t\tsummary: result.returnChat,\n\t\texpandedAncestorSessionIds: result.expandedAncestorSessionIds,\n\t\thasChildren: result.hasChildren,\n\t};\n}\n\ninterface OpenedAgentsViewSession {\n\tconnection: DaemonAgentConnection;\n\tsummary: SessionSummary;\n\tcwdFallbackNotice?: string;\n}\n\nexport function resolveAgentsViewOpenCwd(\n\tsummary: SessionSummary,\n\tfallbackCwd: string | undefined,\n): { overrideCwd?: string; notice?: string } {\n\tif (!summary.cwd || existsSync(summary.cwd) || !fallbackCwd) {\n\t\treturn {};\n\t}\n\treturn {\n\t\toverrideCwd: fallbackCwd,\n\t\tnotice: `Original directory is missing (${summary.cwd}); opened in ${fallbackCwd} instead.`,\n\t};\n}\n\nasync function openAgentsViewSession(\n\toptions: AgentsViewModeOptions,\n\tsummary: SessionSummary,\n): Promise<OpenedAgentsViewSession> {\n\tconst socketPath = options.socketPath;\n\tif (!socketPath) throw new Error(\"Agents view daemon socket is not configured\");\n\tlet client = await connectAgentsViewDaemonClient(socketPath);\n\tif (summary.activeSessionId) {\n\t\ttry {\n\t\t\tconst connection = await DaemonAgentConnection.attach(client, summary.activeSessionId, {\n\t\t\t\tcloseClientOnDispose: true,\n\t\t\t\trecoverDaemon: options.recoverDaemon,\n\t\t\t\treconnectTimeoutMs: options.reconnectTimeoutMs,\n\t\t\t});\n\t\t\treturn { connection, summary };\n\t\t} catch (error) {\n\t\t\tclient.close();\n\t\t\tif (!summary.sessionFile || !isUnknownActiveSessionError(error)) {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t\tclient = await connectAgentsViewDaemonClient(socketPath);\n\t\t}\n\t}\n\n\tif (!summary.sessionFile) {\n\t\tclient.close();\n\t\tthrow new Error(\"Cannot open agent without an active runtime or saved session file\");\n\t}\n\n\ttry {\n\t\tconst resumed = await resumeSavedAgentsViewSession(client, options.config, summary);\n\t\tconst connection = await DaemonAgentConnection.attach(client, resumed.activeSessionId, {\n\t\t\tcloseClientOnDispose: true,\n\t\t\trecoverDaemon: options.recoverDaemon,\n\t\t\treconnectTimeoutMs: options.reconnectTimeoutMs,\n\t\t});\n\t\treturn { connection, summary: resumed.summary, cwdFallbackNotice: resumed.cwdFallbackNotice };\n\t} catch (error) {\n\t\tclient.close();\n\t\tthrow error;\n\t}\n}\n\n/**\n * Resume a saved session file into the daemon and return the live summary.\n * The daemon's create-with-sessionPath is idempotent for this client: an\n * already-resident session is reused instead of resumed twice.\n */\nasync function resumeSavedAgentsViewSession(\n\tclient: DaemonClient,\n\tconfig: AgentSessionRuntimeConfig,\n\tsummary: SessionSummary,\n): Promise<{ summary: SessionSummary; activeSessionId: string; cwdFallbackNotice?: string }> {\n\tif (!summary.sessionFile) {\n\t\tthrow new Error(\"Cannot resume a session without a saved session file\");\n\t}\n\tconst { overrideCwd, notice } = resolveAgentsViewOpenCwd(summary, config.cwd);\n\tconst response = await client.request({\n\t\ttype: \"create\",\n\t\tconfig: createAgentsViewResumeConfig(config, overrideCwd),\n\t\tsessionPath: summary.sessionFile,\n\t});\n\tconst createdSummary = expectSessionSummary(requireDaemonData(response));\n\treturn {\n\t\tsummary: createdSummary,\n\t\tactiveSessionId: getRequiredActiveSessionId(createdSummary),\n\t\tcwdFallbackNotice: notice,\n\t};\n}\n\nasync function connectAgentsViewDaemonClient(socketPath: string): Promise<DaemonClient> {\n\tconst client = new DaemonClient(socketPath);\n\ttry {\n\t\tawait client.connect();\n\t\treturn client;\n\t} catch (error) {\n\t\tclient.close();\n\t\tthrow error;\n\t}\n}\n\nfunction getRequiredActiveSessionId(summary: SessionSummary): string {\n\tif (!summary.activeSessionId) {\n\t\tthrow new Error(\"Daemon returned a session without an active session id\");\n\t}\n\treturn summary.activeSessionId;\n}\n\nfunction isUnknownActiveSessionError(error: unknown): boolean {\n\treturn error instanceof Error && error.message.startsWith(\"Unknown active session:\");\n}\n\nexport async function runAgentsViewMode(options: AgentsViewModeOptions): Promise<void> {\n\tconst persistentState = createInitialAgentsViewPersistentState(options);\n\tconst promptStashStore = options.promptStashStore ?? new ClientPromptStashStore();\n\n\twhile (true) {\n\t\tconst view = new AgentsViewMode(options, persistentState);\n\t\tconst viewResult = await view.run();\n\t\tif (viewResult.type === \"exit\") return;\n\t\tlet result: Extract<AgentsViewRunResult, { type: \"open\" }>;\n\t\tif (viewResult.type === \"scope_back\") {\n\t\t\tpersistentState.scopeFrames = transitionAgentsViewScope(persistentState.scopeFrames ?? [], { type: \"back\" });\n\t\t\tpersistentState.scopeRootSummary = undefined;\n\t\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(viewResult.selection);\n\t\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(viewResult.selection);\n\t\t\tpersistentState.pendingExpandedAncestorSessionIds = viewResult.expandedAncestorSessionIds;\n\t\t\tpersistentState.query = \"\";\n\t\t\tconst returnChatResult = createScopeBackReturnChatOpenResult(viewResult);\n\t\t\tif (!returnChatResult) continue;\n\t\t\tresult = returnChatResult;\n\t\t} else {\n\t\t\tresult = viewResult;\n\t\t}\n\n\t\tconst selection = result.selection ?? result.summary;\n\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(selection);\n\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(selection);\n\t\tpersistentState.pendingExpandedAncestorSessionIds = result.expandedAncestorSessionIds;\n\t\tif (result.statusMessage) persistentState.statusMessage = result.statusMessage;\n\n\t\tlet opened: OpenedAgentsViewSession | undefined;\n\t\ttry {\n\t\t\topened = await openAgentsViewSession(options, result.summary);\n\t\t\tpersistentState.backSession = opened.summary;\n\t\t\tif (opened.cwdFallbackNotice) {\n\t\t\t\tpersistentState.statusMessage = combineAgentsViewStartupNotices(\n\t\t\t\t\tresult.statusMessage,\n\t\t\t\t\topened.cwdFallbackNotice,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst uiServices = await resolveAgentsViewSessionUiServices(options, opened.summary);\n\t\t\tconst interactiveMode = new InteractiveMode({\n\t\t\t\tagentConnection: opened.connection,\n\t\t\t\tdaemonSocketPath: options.socketPath,\n\t\t\t\tuiServices,\n\t\t\t\tpromptStashStore,\n\t\t\t\tpromptStashSessionId: opened.summary.sessionId,\n\t\t\t\tbindLocalSessionExtensions: false,\n\t\t\t\tmigratedProviders: options.migratedProviders,\n\t\t\t\tmodelFallbackMessage: resolveAttachModelFallbackMessage(opened.summary, options.modelFallbackMessage),\n\t\t\t\tstartupNotice: combineAgentsViewStartupNotices(result.statusMessage, opened.cwdFallbackNotice),\n\t\t\t\tverbose: options.verbose,\n\t\t\t\treturnToAgentsView: true,\n\t\t\t\tforceFullscreen: true,\n\t\t\t\t// The agents view renders the global notices itself, so suppress them in-session.\n\t\t\t\tagentsViewOwnsStartupNotices: true,\n\t\t\t\tsessionDepth: opened.summary.rlmDepth,\n\t\t\t\tsessionHasChildren: result.hasChildren,\n\t\t\t});\n\t\t\ttry {\n\t\t\t\tconst interactiveResult = await interactiveMode.run();\n\t\t\t\tconst source = interactiveResult.source;\n\t\t\t\tconst returnedSession: SessionSummary = {\n\t\t\t\t\t...opened.summary,\n\t\t\t\t\t...source,\n\t\t\t\t\tid: source.activeSessionId ?? opened.summary.id,\n\t\t\t\t};\n\t\t\t\t// Preserve an unattachable child's selection while its parent chat was open.\n\t\t\t\tif (selection.sessionId === result.summary.sessionId) {\n\t\t\t\t\tpersistentState.selectedRowIdentity = getSummaryIdentity(returnedSession);\n\t\t\t\t\tpersistentState.selectedSessionKey = getAgentsViewSelectionKey(returnedSession);\n\t\t\t\t}\n\t\t\t\tif (interactiveResult.type === \"scoped_agents_view\") {\n\t\t\t\t\tconst nextScope = { sessionId: source.sessionId, activeSessionId: source.activeSessionId };\n\t\t\t\t\tpersistentState.scopeFrames = transitionAgentsViewScope(persistentState.scopeFrames ?? [], {\n\t\t\t\t\t\ttype: \"push\",\n\t\t\t\t\t\tscope: nextScope,\n\t\t\t\t\t\treturnChat: returnedSession,\n\t\t\t\t\t});\n\t\t\t\t\tconst cachedLiveSummaries = persistentState.lastSuccessfulLiveSummaries ?? [];\n\t\t\t\t\tconst cachedIndex = cachedLiveSummaries.findIndex(\n\t\t\t\t\t\t(summary) => summary.sessionId === returnedSession.sessionId,\n\t\t\t\t\t);\n\t\t\t\t\tpersistentState.lastSuccessfulLiveSummaries =\n\t\t\t\t\t\tcachedIndex === -1\n\t\t\t\t\t\t\t? [...cachedLiveSummaries, returnedSession]\n\t\t\t\t\t\t\t: cachedLiveSummaries.map((summary, index) => (index === cachedIndex ? returnedSession : summary));\n\t\t\t\t\tpersistentState.scopeRootSummary = undefined;\n\t\t\t\t\tpersistentState.query = \"\";\n\t\t\t\t}\n\t\t\t\tpersistentState.backSession = returnedSession;\n\t\t\t} catch (error) {\n\t\t\t\t// The session opened fine and then threw while running; label it as a\n\t\t\t\t// runtime crash so it isn't mixed in with true open failures.\n\t\t\t\tlogClientError(\"Agent session crashed\", error);\n\t\t\t\tpersistentState.statusMessage = formatError(\"Agent session crashed\", error);\n\t\t\t\t// Tear down the session TUI exactly as a normal back-navigation would\n\t\t\t\t// (drain input, stop renderer + theme watcher) so it doesn't fight the\n\t\t\t\t// agents-view UI for the terminal, then drop the daemon connection.\n\t\t\t\tawait interactiveMode.teardownSessionUi({ preserveAltScreen: true });\n\t\t\t\tawait opened.connection.dispose().catch(() => undefined);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tawait opened?.connection.dispose().catch(() => undefined);\n\t\t\tlogClientError(\"Failed to open agent\", error);\n\t\t\tpersistentState.statusMessage = formatError(\"Failed to open agent\", error);\n\t\t}\n\t}\n}\n\nconst AGENTS_VIEW_COMMAND_NAMES = [\"name\", \"kill\"] as const;\nexport type AgentsViewCommandName = (typeof AGENTS_VIEW_COMMAND_NAMES)[number];\nconst AGENTS_VIEW_COMMAND_NAME_SET: ReadonlySet<string> = new Set(AGENTS_VIEW_COMMAND_NAMES);\n\nexport interface AgentsViewCommand {\n\tname: AgentsViewCommandName;\n\targs: string;\n}\n\n/** Row-targeted commands the armed composer maps onto existing RPCs. */\nexport function parseAgentsViewCommand(text: string): AgentsViewCommand | undefined {\n\tconst parsed = parseSlashCommand(text);\n\tif (!parsed) return undefined;\n\tconst name = resolveBuiltinSlashCommandName(parsed.name);\n\tif (!AGENTS_VIEW_COMMAND_NAME_SET.has(name)) return undefined;\n\treturn { name: name as AgentsViewCommandName, args: parsed.args };\n}\n\n/**\n * Reject recognized built-ins that are neither session-owned nor view\n * commands, so they are never sent to the model as plain prompt text.\n */\nexport function getReplyComposerCommandRejection(text: string): string | undefined {\n\tconst parsed = parseSlashCommand(text);\n\tif (!parsed) return undefined;\n\tconst name = resolveBuiltinSlashCommandName(parsed.name);\n\tif (isSessionSlashCommandName(name)) return undefined;\n\tif (AGENTS_VIEW_COMMAND_NAME_SET.has(name)) return undefined;\n\tif (!isBuiltinSlashCommandName(parsed.name)) return undefined;\n\treturn `/${parsed.name} is not available here; open the session to run it`;\n}\n\nconst AGENTS_VIEW_COMMAND_DESCRIPTIONS: Record<AgentsViewCommandName, { description: string; argumentHint?: string }> =\n\t{\n\t\tname: { description: \"Set session display name\", argumentHint: \"<name>\" },\n\t\tkill: { description: \"Stop this agent's runtime (session stays resumable)\" },\n\t};\n\nfunction agentsViewSlashCommands(): {\n\tname: string;\n\taliases?: readonly string[];\n\tdescription: string;\n\targumentHint?: string;\n\ttakesArgument?: boolean;\n}[] {\n\treturn AGENTS_VIEW_COMMAND_NAMES.map((name) => {\n\t\tconst builtin = BUILTIN_SLASH_COMMANDS.find((command) => command.name === name);\n\t\tconst display = AGENTS_VIEW_COMMAND_DESCRIPTIONS[name];\n\t\treturn {\n\t\t\tname,\n\t\t\taliases: builtin?.aliases,\n\t\t\tdescription: display.description,\n\t\t\targumentHint: display.argumentHint ?? builtin?.argumentHint,\n\t\t\ttakesArgument: name === \"name\" ? true : builtin?.takesArgument,\n\t\t};\n\t});\n}\n\n/** Autocomplete for the reply composer: session-owned plus view commands. */\nexport function createReplyComposerAutocompleteProvider(cwd: string, fdPath?: string): AutocompleteProvider {\n\tconst sessionCommands = BUILTIN_SLASH_COMMANDS.filter((command) => isSessionSlashCommandName(command.name)).map(\n\t\t(command) => ({\n\t\t\tname: command.name,\n\t\t\taliases: command.aliases,\n\t\t\tdescription: command.description,\n\t\t\targumentHint: command.argumentHint,\n\t\t\ttakesArgument: command.takesArgument,\n\t\t}),\n\t);\n\treturn new CombinedAutocompleteProvider([...sessionCommands, ...agentsViewSlashCommands()], cwd, fdPath ?? null);\n}\n\nexport function resolveCurrentReplyTargetSummary(\n\trecords: readonly UnifiedSessionRecord[],\n\ttarget: { key: string; summary: SessionSummary },\n\tfindLive: (activeSessionId: string) => SessionSummary | undefined,\n): SessionSummary {\n\tconst identity = getSummaryIdentity(target.summary);\n\tconst current = records.find((record) => record.identity === identity || record.identityAliases.includes(identity));\n\tif (current) return summaryForUnifiedRecord(current);\n\tconst live = target.summary.activeSessionId ? findLive(target.summary.activeSessionId) : undefined;\n\tif (live) return live;\n\t// A persisted target missing from the current live catalog can still be\n\t// resumed from its captured file, but its captured runtime id is stale.\n\tif (target.summary.sessionFile && target.summary.activeSessionId) {\n\t\treturn { ...target.summary, activeSessionId: undefined, lifecycle: \"archived\", activity: \"idle\" };\n\t}\n\treturn target.summary;\n}\n\nexport class AgentsViewMode implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly ui: TUI;\n\tprivate readonly editor: CustomEditor;\n\tprivate readonly splash: BrandSplashHeader;\n\tprivate readonly fullscreenDock: Component;\n\tprivate readonly keybindings: KeybindingsManager;\n\tprivate client: DaemonClient | undefined;\n\tprivate unsubscribeClientClose: (() => void) | undefined;\n\tprivate unsubscribeClientMessage: (() => void) | undefined;\n\tprivate reconnectPromise: Promise<void> | undefined;\n\tprivate reconnectTimedOut = false;\n\tprivate daemonShutdownReceived = false;\n\tprivate resolveRun: ((result: AgentsViewRunResult) => void) | undefined;\n\tprivate pollTimer: NodeJS.Timeout | undefined;\n\tprivate heartbeatPollTimer: NodeJS.Timeout | undefined;\n\tprivate animationTimer: NodeJS.Timeout | undefined;\n\tprivate ctrlCExitHintExpiresAt = 0;\n\tprivate ctrlCExitHintTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate deleteConfirmExpiresAt = 0;\n\tprivate deleteConfirmTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate workingIconFrame = 0;\n\tprivate rows: AgentsViewRow[] = [];\n\tprivate lastListedSummaries: SessionSummary[] = [];\n\tprivate lastVisibleSummaries: SessionSummary[] = [];\n\tprivate savedSessions: AgentConnectionSavedSessionInfo[] = [];\n\tprivate lastSuccessfulSavedSessions: AgentConnectionSavedSessionInfo[] = [];\n\tprivate heartbeats: AgentConnectionHeartbeat[] = [];\n\tprivate unifiedRecords: UnifiedSessionRecord[] = [];\n\tprivate unifiedIndex: UnifiedSessionIndex = buildUnifiedSessionIndex([]);\n\tprivate scopedRecords: UnifiedSessionRecord[] = [];\n\tprivate scopeKey: AgentsViewScopeKey | undefined;\n\tprivate scopeRootSummary: SessionSummary | undefined;\n\tprivate liveCatalogReady = false;\n\tprivate savedCatalogReady = false;\n\tprivate savedCatalogGeneration = 0;\n\tprivate liveCatalogGeneration = 0;\n\tprivate heartbeatCatalogGeneration = 0;\n\tprivate liveCatalogPollPromise: Promise<void> | undefined;\n\tprivate liveCatalogRefreshPending = false;\n\tprivate savedCatalogRefreshPending = false;\n\tprivate expandedSubagentParents = new Set<string>();\n\t// Agent row identities whose full spawn program is currently shown.\n\t// The program key toggles each agent shown ↔ hidden.\n\tprivate programShownParents = new Set<string>();\n\tprivate selectedIndex = 0;\n\tprivate selectedRowIdentity: string | undefined;\n\tprivate selectedActiveSessionId: string | undefined;\n\tprivate selectedSessionKey: AgentsViewSelectionKey | undefined;\n\tprivate selectionAnchorPending = false;\n\t/** Armed reply composer target: a live agent or a saved session to resume on send. */\n\tprivate replyTarget: { key: string; summary: SessionSummary } | undefined;\n\t/** Provider bound to the armed target's cwd for file-path completions. */\n\tprivate replyAutocomplete: AutocompleteProvider | undefined;\n\tprivate fdPath: string | undefined;\n\tprivate creatingNewSession = false;\n\tprivate replyLastAssistantText: string | undefined;\n\tprivate replyLastAssistantTextLoading = false;\n\tprivate replyHeaderTime = \"\";\n\tprivate pendingDeleteAgent: PendingDeleteAgent | undefined;\n\tprivate pendingKillSubagent: PendingKillSubagent | undefined;\n\tprivate renameTarget: { activeSessionId?: string; sessionFile?: string; summary: SessionSummary } | undefined;\n\tprivate actionModeSearchQuery: string | undefined;\n\tprivate readonly inactiveAgentIdentities = new Set<string>();\n\tprivate statusMessage: string | undefined;\n\tprivate statusMessageTone: \"muted\" | \"error\" | \"warning\" = \"muted\";\n\tprivate statusMessageSticky = false;\n\tprivate statusMessageTimer: ReturnType<typeof setTimeout> | undefined;\n\tprivate stopped = false;\n\n\tconstructor(\n\t\tprivate readonly options: AgentsViewModeOptions,\n\t\tprivate readonly persistentState: AgentsViewPersistentState = {},\n\t) {\n\t\tconst initialFrames =\n\t\t\tpersistentState.scopeFrames ??\n\t\t\tcreateInitialAgentsViewScopeFrames(\n\t\t\t\toptions.initialScopeKey,\n\t\t\t\tpersistentState.backSession ?? options.initialSession,\n\t\t\t);\n\t\tpersistentState.scopeFrames = initialFrames;\n\t\tthis.scopeKey = initialFrames.at(-1)?.scope;\n\t\tthis.scopeRootSummary = persistentState.scopeRootSummary;\n\t\tthis.selectedRowIdentity = persistentState.selectedRowIdentity;\n\t\tthis.selectedSessionKey = persistentState.selectedSessionKey;\n\t\tthis.selectedActiveSessionId = persistentState.selectedSessionKey?.activeSessionId;\n\t\tthis.lastListedSummaries = persistentState.lastSuccessfulLiveSummaries ?? [];\n\t\tthis.savedSessions = persistentState.savedSessions ?? [];\n\t\tthis.lastSuccessfulSavedSessions = persistentState.lastSuccessfulSavedSessions ?? this.savedSessions;\n\t\tthis.savedCatalogReady = persistentState.lastSuccessfulSavedSessions !== undefined;\n\t\tthis.heartbeats = persistentState.heartbeats ?? [];\n\t\tthis.savedCatalogGeneration = persistentState.savedCatalogGeneration ?? 0;\n\t\tthis.expandedSubagentParents = persistentState.expandedSubagentParents ?? new Set();\n\t\tpersistentState.expandedSubagentParents = this.expandedSubagentParents;\n\t\tthis.programShownParents = persistentState.programShownParents ?? new Set();\n\t\tpersistentState.programShownParents = this.programShownParents;\n\t\tthis.keybindings = KeybindingsManager.create();\n\t\tsetKeybindings(this.keybindings);\n\t\tsetRegisteredThemes(options.uiServices.getThemes());\n\t\tinitTheme(options.uiServices.settingsManager.getTheme(), true);\n\n\t\tthis.ui = new TUI(new ProcessTerminal(), options.uiServices.settingsManager.getShowHardwareCursor());\n\t\tthis.ui.setClearOnShrink(options.uiServices.settingsManager.getClearOnShrink());\n\t\tthis.ui.terminal.setTitle(`${APP_TITLE} - Agents`);\n\t\tthis.editor = new CustomEditor(this.ui, getEditorTheme(), this.keybindings, {\n\t\t\tpaddingX: options.uiServices.settingsManager.getEditorPaddingX(),\n\t\t\tautocompleteMaxVisible: options.uiServices.settingsManager.getAutocompleteMaxVisible(),\n\t\t\tplaceholder: SEARCH_PROMPT_PLACEHOLDER,\n\t\t\tplaceholderColor: (text) => theme.fg(\"dim\", text),\n\t\t});\n\t\tvoid ensureTool(\"fd\").then((fdPath) => {\n\t\t\tthis.fdPath = fdPath;\n\t\t\t// Rebind an already-armed provider so @-completion picks up fd.\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.replyAutocomplete = createReplyComposerAutocompleteProvider(this.replyTarget.summary.cwd, fdPath);\n\t\t\t}\n\t\t});\n\t\t// Search input never autocompletes; only the armed composer's provider answers.\n\t\tthis.editor.setAutocompleteProvider({\n\t\t\tgetSuggestions: async (lines, cursorLine, cursorCol, suggestOptions) => {\n\t\t\t\tif (!this.replyTarget) return null;\n\t\t\t\treturn this.replyAutocomplete?.getSuggestions(lines, cursorLine, cursorCol, suggestOptions) ?? null;\n\t\t\t},\n\t\t\tapplyCompletion: (lines, cursorLine, cursorCol, item, prefix) =>\n\t\t\t\tthis.replyAutocomplete?.applyCompletion(lines, cursorLine, cursorCol, item, prefix) ?? {\n\t\t\t\t\tlines,\n\t\t\t\t\tcursorLine,\n\t\t\t\t\tcursorCol,\n\t\t\t\t},\n\t\t});\n\t\tthis.editor.focused = true;\n\t\tthis.editor.getHeaderLine = () => this.renderReplyHeaderLine();\n\t\tthis.editor.onSubmit = (value) => {\n\t\t\tvoid this.submit(value);\n\t\t};\n\t\tthis.editor.setText(persistentState.query ?? \"\");\n\t\tthis.editor.onCtrlD = () => {\n\t\t\tthis.finish({ type: \"exit\" });\n\t\t};\n\t\tthis.editor.onAgentsBack = () => {\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\tif (this.editor.getText().length > 0) return false;\n\t\t\tconst ancestors = this.scopeKey\n\t\t\t\t? getUnifiedSessionAncestorSessionIds(this.unifiedRecords, this.scopeKey, this.unifiedIndex)\n\t\t\t\t: [];\n\t\t\tconst scopeRoot = this.scopeRootSummary;\n\t\t\tconst result = resolveAgentsViewLeftResult(\n\t\t\t\tscopeRoot,\n\t\t\t\tancestors,\n\t\t\t\tthis.persistentState.scopeFrames?.at(-1)?.returnChat,\n\t\t\t);\n\t\t\tif (result && scopeRoot) {\n\t\t\t\tthis.finish({\n\t\t\t\t\t...result,\n\t\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\t\tgetAgentsViewSelectionKey(scopeRoot),\n\t\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t\t),\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Global view has no hierarchy parent: consume Left without opening chat.\n\t\t\treturn true;\n\t\t};\n\t\tthis.editor.onEscape = () => {\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t} else if (this.editor.getText().length > 0) {\n\t\t\t\tthis.setSearchQuery(\"\");\n\t\t\t} else {\n\t\t\t\tconst backSession = this.persistentState.backSession;\n\t\t\t\tthis.finish(\n\t\t\t\t\tbackSession\n\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\ttype: \"open\",\n\t\t\t\t\t\t\t\tsummary: backSession,\n\t\t\t\t\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\t\t\t\t\tgetAgentsViewSelectionKey(backSession),\n\t\t\t\t\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t: { type: \"exit\" },\n\t\t\t\t);\n\t\t\t}\n\t\t};\n\t\tthis.fullscreenDock = {\n\t\t\trender: (width) => this.renderDock(width),\n\t\t\tinvalidate: () => {\n\t\t\t\tthis.editor.invalidate();\n\t\t\t},\n\t\t};\n\t\tthis.splash = new BrandSplashHeader(\n\t\t\tVERSION,\n\t\t\t() => this.getSplashModelId(),\n\t\t\t() => this.getSplashCwd(),\n\t\t\tundefined,\n\t\t\t{\n\t\t\t\ttopPadding: true,\n\t\t\t\tgetExtraMetadata: () => {\n\t\t\t\t\tconst root = this.scopeRootSummary;\n\t\t\t\t\treturn [\n\t\t\t\t\t\t{ label: \"agents\", value: this.getAgentCountsText() },\n\t\t\t\t\t\t{ label: \"scope\", value: root ? getAgentsViewSessionTitle(root) : \"global\" },\n\t\t\t\t\t\t{ label: \"depth\", value: String(getAgentsViewDepth(root)) },\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\tasync run(): Promise<AgentsViewRunResult> {\n\t\tthis.client = new DaemonClient(this.requireSocketPath());\n\t\tawait this.client.connect();\n\t\tthis.subscribeToClientClose(this.client);\n\t\tthis.unsubscribeClientMessage = this.client.onMessage((message) => {\n\t\t\tif (message.type === \"heartbeats_changed\") void this.refreshHeartbeats();\n\t\t});\n\n\t\tthis.ui.addChild(this);\n\t\tthis.ui.setFocus(this);\n\t\tthis.ui.start();\n\t\tthis.ui.enterFullscreen({\n\t\t\tscroll: [this],\n\t\t\tdock: this.fullscreenDock,\n\t\t\tmouse: false,\n\t\t\tviewportControls: false,\n\t\t});\n\t\tconst startupStatusMessage = this.persistentState.statusMessage;\n\t\tthis.persistentState.statusMessage = undefined;\n\t\tif (startupStatusMessage) {\n\t\t\tthis.setStatusMessage(startupStatusMessage, { render: false });\n\t\t}\n\t\tthis.ui.requestRender(true);\n\t\tonThemeChange(() => {\n\t\t\tthis.ui.invalidate();\n\t\t\tthis.ui.requestRender();\n\t\t});\n\n\t\tconst runPromise = new Promise<AgentsViewRunResult>((resolve) => {\n\t\t\tthis.resolveRun = resolve;\n\t\t});\n\t\tvoid this.refreshSessions();\n\t\tvoid this.refreshSavedSessions();\n\t\tvoid this.refreshHeartbeats();\n\t\tthis.loadStartupNotices();\n\t\tthis.pollTimer = setInterval(() => this.pollSessions(), POLL_INTERVAL_MS);\n\t\tthis.pollTimer.unref?.();\n\t\tthis.heartbeatPollTimer = setInterval(() => void this.refreshHeartbeats(), HEARTBEAT_POLL_INTERVAL_MS);\n\t\tthis.heartbeatPollTimer.unref?.();\n\t\tthis.animationTimer = setInterval(() => {\n\t\t\tif (!this.rows.some((row) => row.section === \"running\")) return;\n\t\t\tthis.workingIconFrame += 1;\n\t\t\tthis.ui.requestRender();\n\t\t}, WORKING_ICON_INTERVAL_MS);\n\t\tthis.animationTimer.unref?.();\n\n\t\treturn runPromise;\n\t}\n\n\thandleInput(data: string): void {\n\t\tthis.clearStickyStatusMessage();\n\t\tif (this.renameTarget) {\n\t\t\tif (this.keybindings.matches(data, \"tui.select.cancel\")) {\n\t\t\t\tthis.exitRenameMode();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.editor.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"app.clear\")) {\n\t\t\t// The composer hints advertise ctrl+c as cancel; it must not start the exit flow.\n\t\t\tif (this.replyTarget) {\n\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.handleCtrlC();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.rename\")) {\n\t\t\tthis.enterRenameMode();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.delete\")) {\n\t\t\tthis.clearCtrlCExitHint({ render: false });\n\t\t\tvoid this.handleDeleteSelected();\n\t\t\treturn;\n\t\t}\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\tif (this.keybindings.matches(data, \"app.agents.reply\") && this.editor.getText().length === 0) {\n\t\t\tvoid this.toggleReplyTarget();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.replyTarget && this.keybindings.matches(data, \"app.agents.new\")) {\n\t\t\tvoid this.createNewSession();\n\t\t\treturn;\n\t\t}\n\t\tif (this.replyTarget && this.keybindings.matches(data, \"app.message.followUp\")) {\n\t\t\tthis.handleReplyFollowUp();\n\t\t\treturn;\n\t\t}\n\t\tif (this.editor.getText().length === 0 && this.keybindings.matches(data, \"app.agents.program\")) {\n\t\t\tthis.cycleProgramForSelected();\n\t\t\treturn;\n\t\t}\n\t\tif (!this.replyTarget && this.keybindings.matches(data, \"app.agents.open\")) {\n\t\t\tif (this.editor.getText().length === 0 || this.isSearchCursorAtEnd()) {\n\t\t\t\tthis.openSelected();\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\tif (!this.replyTarget && this.handleListNavigation(data)) {\n\t\t\treturn;\n\t\t}\n\t\tconst previous = this.editor.getText();\n\t\tthis.editor.handleInput(data);\n\t\tif (!this.replyTarget && this.editor.getText() !== previous) {\n\t\t\tthis.queryChanged();\n\t\t}\n\t}\n\n\tprivate isSearchCursorAtEnd(): boolean {\n\t\tconst lines = this.editor.getLines();\n\t\tconst cursor = this.editor.getCursor();\n\t\treturn cursor.line === lines.length - 1 && cursor.col === (lines[cursor.line]?.length ?? 0);\n\t}\n\n\trender(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\tconst height = this.contentHeight(safeWidth);\n\t\tconst lines = this.renderContent(safeWidth, height).slice(0, height);\n\t\twhile (lines.length < height) {\n\t\t\tlines.push(\"\");\n\t\t}\n\t\treturn lines.slice(0, height).map((line) => this.finalizeRenderedLine(line, safeWidth));\n\t}\n\n\tinvalidate(): void {\n\t\tthis.editor.invalidate();\n\t\tthis.splash.invalidate();\n\t}\n\n\tprivate renderContent(width: number, height: number): string[] {\n\t\tif (height <= 0) {\n\t\t\treturn [];\n\t\t}\n\t\tconst headerLines = this.splash.render(width);\n\t\tconst noticeLines = this.renderStartupNotices(width);\n\t\tif (noticeLines.length > 0) {\n\t\t\theaderLines.push(\"\", ...noticeLines);\n\t\t}\n\t\theaderLines.push(\"\");\n\n\t\t// The prompt belongs to the scroll pane rather than the fullscreen dock, but\n\t\t// it must remain usable when a short viewport or wrapped notices exhaust the\n\t\t// header. Trim optional header chrome first and reserve one session-list row.\n\t\tconst promptLines = this.renderPrompt(width);\n\t\tconst listGap = height >= promptLines.length + 2 ? 1 : 0;\n\t\tconst headerRows = Math.max(0, height - promptLines.length - listGap - 1);\n\t\tconst lines = headerLines.slice(0, headerRows);\n\t\tlines.push(...promptLines);\n\t\tif (listGap > 0) lines.push(\"\");\n\t\tconst listRows = Math.max(0, height - lines.length);\n\t\tlines.push(...this.renderSessionRows(width, listRows));\n\t\treturn lines;\n\t}\n\n\tprivate loadStartupNotices(): void {\n\t\t// Notices live on persistentState (read directly in renderStartupNotices), so they\n\t\t// survive leaving and re-entering the agents view regardless of which instance's\n\t\t// gather resolved. Already have them? Nothing to do.\n\t\tif (this.persistentState.startupNotices) {\n\t\t\treturn;\n\t\t}\n\t\t// Reuse an in-flight gather from an earlier agents-view instance so re-entry does\n\t\t// not re-run the checks or lose a result that resolved meanwhile.\n\t\tconst promise =\n\t\t\tthis.persistentState.startupNoticesPromise ??\n\t\t\tgatherStartupNotices({\n\t\t\t\tversion: VERSION,\n\t\t\t\tcwd: this.options.uiServices.getInitialCwd(),\n\t\t\t\tagentDir: getAgentDir(),\n\t\t\t\tsettingsManager: this.options.uiServices.settingsManager,\n\t\t\t});\n\t\tthis.persistentState.startupNoticesPromise = promise;\n\t\tvoid promise\n\t\t\t.then((notices) => {\n\t\t\t\tthis.persistentState.startupNotices = notices;\n\t\t\t\t// Best-effort immediate paint; a re-entered instance also picks this up on\n\t\t\t\t// its next poll tick since render reads persistentState directly.\n\t\t\t\tthis.ui.requestRender();\n\t\t\t})\n\t\t\t.catch(() => {});\n\t}\n\n\tprivate renderStartupNotices(width: number): string[] {\n\t\tconst notices = this.persistentState.startupNotices;\n\t\tif (!notices) {\n\t\t\treturn [];\n\t\t}\n\t\tconst formatted: string[] = [];\n\t\tif (notices.newVersion) {\n\t\t\tformatted.push(formatUpdateAvailableNotice(notices.newVersion));\n\t\t}\n\t\tif (notices.packageUpdates.length > 0) {\n\t\t\tformatted.push(formatPackageUpdateNotice(notices.packageUpdates));\n\t\t}\n\t\tif (notices.tmuxWarning) {\n\t\t\tformatted.push(formatTmuxWarningNotice(notices.tmuxWarning));\n\t\t}\n\t\t// Match the splash header's one-column gutter and wrap so long notices\n\t\t// (e.g. the tmux fix instructions) stay readable instead of truncating.\n\t\tconst wrapWidth = Math.max(1, width - 1);\n\t\treturn formatted.flatMap((line) => wrapTextWithAnsi(line, wrapWidth).map((wrapped) => ` ${wrapped}`));\n\t}\n\n\tprivate handleListNavigation(data: string): boolean {\n\t\tif (this.keybindings.matches(data, \"tui.select.up\")) {\n\t\t\tthis.moveSelection(-1);\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.down\")) {\n\t\t\tthis.moveSelection(1);\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.pageUp\")) {\n\t\t\tthis.moveSelection(-Math.max(1, this.visibleListRows()));\n\t\t\treturn true;\n\t\t}\n\t\tif (this.keybindings.matches(data, \"tui.select.pageDown\")) {\n\t\t\tthis.moveSelection(Math.max(1, this.visibleListRows()));\n\t\t\treturn true;\n\t\t}\n\t\treturn false;\n\t}\n\n\tprivate handleCtrlC(): void {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tthis.finish({ type: \"exit\" });\n\t\t\treturn;\n\t\t}\n\t\tthis.showCtrlCExitHint();\n\t}\n\n\tprivate showCtrlCExitHint(): void {\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = Date.now() + EXIT_HINT_DURATION_MS;\n\t\tthis.ctrlCExitHintTimer = setTimeout(() => {\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t\tif (!this.isCtrlCExitHintVisible()) {\n\t\t\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, EXIT_HINT_DURATION_MS);\n\t\tthis.ctrlCExitHintTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearCtrlCExitHint(options: { render?: boolean } = {}): void {\n\t\tif (!this.ctrlCExitHintTimer && this.ctrlCExitHintExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.ctrlCExitHintTimer) {\n\t\t\tclearTimeout(this.ctrlCExitHintTimer);\n\t\t\tthis.ctrlCExitHintTimer = undefined;\n\t\t}\n\t\tthis.ctrlCExitHintExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isCtrlCExitHintVisible(): boolean {\n\t\treturn this.ctrlCExitHintExpiresAt > Date.now();\n\t}\n\n\tprivate showDeleteConfirmation(): void {\n\t\tif (this.deleteConfirmTimer) {\n\t\t\tclearTimeout(this.deleteConfirmTimer);\n\t\t}\n\t\tthis.deleteConfirmExpiresAt = Date.now() + DELETE_CONFIRM_DURATION_MS;\n\t\tthis.deleteConfirmTimer = setTimeout(() => {\n\t\t\tthis.deleteConfirmTimer = undefined;\n\t\t\tif (!this.isDeleteConfirmationVisible()) {\n\t\t\t\tthis.deleteConfirmExpiresAt = 0;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t}, DELETE_CONFIRM_DURATION_MS);\n\t\tthis.deleteConfirmTimer.unref?.();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate clearDeleteConfirmation(options: { render?: boolean } = {}): void {\n\t\tthis.pendingKillSubagent = undefined;\n\t\tif (!this.deleteConfirmTimer && this.deleteConfirmExpiresAt === 0) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.deleteConfirmTimer) {\n\t\t\tclearTimeout(this.deleteConfirmTimer);\n\t\t\tthis.deleteConfirmTimer = undefined;\n\t\t}\n\t\tthis.deleteConfirmExpiresAt = 0;\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\tprivate isDeleteConfirmationVisible(): boolean {\n\t\treturn this.deleteConfirmExpiresAt > Date.now();\n\t}\n\n\tprivate setStatusMessage(\n\t\tmessage: string | undefined,\n\t\toptions: { render?: boolean; tone?: \"muted\" | \"error\" | \"warning\"; sticky?: boolean } = {},\n\t): void {\n\t\tconst statusLine = message === undefined ? undefined : formatAgentsViewStatusLine(message);\n\t\tif (this.statusMessageTimer) {\n\t\t\tclearTimeout(this.statusMessageTimer);\n\t\t\tthis.statusMessageTimer = undefined;\n\t\t}\n\t\tthis.statusMessage = statusLine;\n\t\t// Errors come both from explicit tones and from formatError-style messages.\n\t\tthis.statusMessageTone = options.tone ?? (statusLine?.startsWith(\"Failed\") ? \"error\" : \"muted\");\n\t\t// Sticky messages stay up until the next keypress instead of a timer.\n\t\tthis.statusMessageSticky = options.sticky === true && statusLine !== undefined;\n\t\tif (statusLine && !this.statusMessageSticky) {\n\t\t\tthis.statusMessageTimer = setTimeout(() => {\n\t\t\t\tthis.statusMessageTimer = undefined;\n\t\t\t\tif (this.statusMessage === statusLine) {\n\t\t\t\t\tthis.statusMessage = undefined;\n\t\t\t\t\tthis.ui.requestRender();\n\t\t\t\t}\n\t\t\t}, STATUS_MESSAGE_DURATION_MS);\n\t\t\tthis.statusMessageTimer.unref?.();\n\t\t}\n\t\tif (options.render !== false) {\n\t\t\tthis.ui.requestRender();\n\t\t}\n\t}\n\n\t/** Sticky messages (e.g. billing warnings) stay until the user acknowledges them with any keypress. */\n\tprivate clearStickyStatusMessage(): void {\n\t\tif (!this.statusMessageSticky || this.daemonShutdownReceived || this.reconnectPromise) {\n\t\t\treturn;\n\t\t}\n\t\tthis.statusMessageSticky = false;\n\t\tthis.statusMessage = undefined;\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate moveSelection(delta: number): void {\n\t\tconst selectableIndexes = this.getSelectableRowIndexes();\n\t\tif (selectableIndexes.length === 0) {\n\t\t\treturn;\n\t\t}\n\t\tconst currentPosition = selectableIndexes.includes(this.selectedIndex)\n\t\t\t? selectableIndexes.indexOf(this.selectedIndex)\n\t\t\t: 0;\n\t\tconst nextPosition = Math.max(0, Math.min(selectableIndexes.length - 1, currentPosition + delta));\n\t\tthis.selectedIndex = selectableIndexes[nextPosition] ?? 0;\n\t\tthis.syncSelectedRowState();\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t// Reply stays armed only while the selection sits on the agent row it\n\t\t// targets; nested rows share the parent's session id but are read-only.\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\tif (\n\t\t\tthis.replyTarget &&\n\t\t\t(selectedRow?.kind !== \"agent\" || this.replyTarget.key !== this.selectedActiveSessionId)\n\t\t) {\n\t\t\tthis.setReplyTarget(undefined);\n\t\t}\n\t\tthis.ui.requestRender();\n\t}\n\n\t// Resolve the persisted sessionId breadcrumb to live row identities once, so\n\t// the rest of the expansion lifecycle stays uniformly identity-keyed.\n\tprivate applyPendingAncestorExpansion(): void {\n\t\tconst sessionIds = this.persistentState.pendingExpandedAncestorSessionIds;\n\t\tif (!sessionIds || sessionIds.length === 0) {\n\t\t\tthis.persistentState.pendingExpandedAncestorSessionIds = undefined;\n\t\t\treturn;\n\t\t}\n\t\tthis.persistentState.pendingExpandedAncestorSessionIds = undefined;\n\t\tconst wanted = new Set(sessionIds);\n\t\t// A nested ancestor's row only appears once its own parent is expanded, so\n\t\t// expand-and-rebuild until a pass reveals nothing new.\n\t\tlet added = true;\n\t\twhile (added) {\n\t\t\tadded = false;\n\t\t\tfor (const row of this.rows) {\n\t\t\t\tif (wanted.has(row.summary.sessionId) && !this.expandedSubagentParents.has(row.identity)) {\n\t\t\t\t\tthis.expandedSubagentParents.add(row.identity);\n\t\t\t\t\tadded = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (added) {\n\t\t\t\tthis.rebuildRows();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setSearchQuery(query: string): void {\n\t\tthis.editor.setText(query);\n\t\tthis.queryChanged();\n\t}\n\n\tprivate queryChanged(): void {\n\t\tthis.persistentState.query = this.editor.getText();\n\t\tthis.rebuildRows();\n\t\t// Typing must not claim the visible fallback row while the restored\n\t\t// anchor is still waiting for its catalog row.\n\t\tif (!this.selectionAnchorPending) this.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate getFilteredRecords(): UnifiedSessionRecord[] {\n\t\tconst query = this.replyTarget || this.renameTarget ? (this.actionModeSearchQuery ?? \"\") : this.editor.getText();\n\t\treturn filterUnifiedSessions(this.scopedRecords, (text) => matchesSearchText(text, query));\n\t}\n\n\t/** Rebuild rows from the last fetched summaries, keeping selection on the same row. */\n\tprivate rebuildRows(): void {\n\t\tconst selectedIdentity = this.rows[this.selectedIndex]?.identity;\n\t\tthis.rows = buildAgentsViewRows(\n\t\t\tthis.getFilteredRecords(),\n\t\t\tthis.expandedSubagentParents,\n\t\t\tthis.programShownParents,\n\t\t\tthis.scopeKey,\n\t\t);\n\t\tconst index =\n\t\t\tselectedIdentity === undefined ? -1 : this.rows.findIndex((row) => row.identity === selectedIdentity);\n\t\tif (index >= 0) {\n\t\t\tthis.selectedIndex = index;\n\t\t} else {\n\t\t\tthis.restoreSelection();\n\t\t}\n\t}\n\n\tprivate async submit(value: string, delivery: \"steer\" | \"followUp\" = \"steer\"): Promise<void> {\n\t\tif (this.renameTarget) {\n\t\t\tawait this.confirmRename(value);\n\t\t\treturn;\n\t\t}\n\t\tif (this.replyTarget) {\n\t\t\tconst target = this.replyTarget;\n\t\t\tconst text = value.trim();\n\t\t\tconst viewCommand = parseAgentsViewCommand(text);\n\t\t\tif (viewCommand) {\n\t\t\t\t// Stale summaries mis-route the RPCs after a runtime replacement.\n\t\t\t\tconst currentSummary = resolveCurrentReplyTargetSummary(\n\t\t\t\t\tthis.unifiedRecords ?? [],\n\t\t\t\t\ttarget,\n\t\t\t\t\t(activeSessionId) => this.findSummaryByActiveSessionId(activeSessionId),\n\t\t\t\t);\n\t\t\t\tconst succeeded = await this.runAgentsViewCommand(viewCommand, currentSummary);\n\t\t\t\tif (!succeeded && this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\tthis.editor.setText(value);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst rejection = getReplyComposerCommandRejection(text);\n\t\t\tif (rejection) {\n\t\t\t\t// submitValue cleared the buffer before onSubmit; keep the draft.\n\t\t\t\tif (this.editor.getText().length === 0) this.editor.setText(value);\n\t\t\t\tthis.setStatusMessage(rejection, { tone: \"warning\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (text) {\n\t\t\t\tthis.editor.setText(\"\");\n\t\t\t\tconst sent = await this.sendReply(target, text, delivery);\n\t\t\t\tif (sent) {\n\t\t\t\t\tif (this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\t\tthis.setReplyTarget(undefined);\n\t\t\t\t\t}\n\t\t\t\t\t// Keep the send outcome (or sticky cwd notice) that sendReply just surfaced.\n\t\t\t\t\tawait this.refreshSessions({ preserveStatusOnError: true });\n\t\t\t\t} else if (this.replyTarget === target && this.editor.getText().length === 0) {\n\t\t\t\t\tthis.editor.setText(value);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\t// Search text is never a prompt or a command; Enter opens the selection.\n\t\tthis.openSelected();\n\t}\n\n\t/** Alt+Enter in the reply composer queues the reply as a follow-up. */\n\tprivate handleReplyFollowUp(): void {\n\t\tif (!this.replyTarget) return;\n\t\t// Unlike Enter, this path skips submitValue, so expand paste markers here.\n\t\tconst text = this.editor.getExpandedText();\n\t\tif (!text.trim()) return;\n\t\tvoid this.submit(text, \"followUp\");\n\t}\n\n\tprivate getSavedSessionCwd(): string {\n\t\treturn this.options.config.cwd ?? this.options.uiServices.getInitialCwd();\n\t}\n\n\tprivate getSavedSessionCatalogContext(): DaemonSavedSessionCatalogContext {\n\t\treturn { cwd: this.getSavedSessionCwd(), sessionDir: this.options.config.sessionDir };\n\t}\n\n\tprivate openSelected(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row?.selectable || this.isPendingDeleteRow(row)) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.selectionAnchorPending) {\n\t\t\tthis.setStatusMessage(\"Waiting for the selected session to load\");\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent-summary\") {\n\t\t\tthis.toggleSubagentList(row);\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent\") {\n\t\t\tthis.openSelectedSubagent(row);\n\t\t\treturn;\n\t\t}\n\t\tif (!row.summary.activeSessionId && !row.summary.sessionFile) {\n\t\t\tthis.setStatusMessage(\"Cannot open agent without an active runtime or saved session file\");\n\t\t\treturn;\n\t\t}\n\t\tthis.finish({\n\t\t\ttype: \"open\",\n\t\t\tsummary: row.summary,\n\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\tthis.unifiedRecords,\n\t\t\t\tgetAgentsViewSelectionKey(row.summary),\n\t\t\t\tthis.unifiedIndex,\n\t\t\t),\n\t\t});\n\t}\n\n\tprivate toggleSubagentList(row: AgentsViewRow): void {\n\t\tif (!row.parentIdentity) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.expandedSubagentParents.has(row.parentIdentity)) {\n\t\t\tthis.expandedSubagentParents.delete(row.parentIdentity);\n\t\t\t// A collapsed agent's revealed program collapses with it, so reopening\n\t\t\t// starts from the hidden state rather than a stale reveal.\n\t\t\tthis.programShownParents.delete(row.parentIdentity);\n\t\t} else {\n\t\t\tthis.expandedSubagentParents.add(row.parentIdentity);\n\t\t}\n\t\tthis.rebuildRows();\n\t\tthis.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/**\n\t * Toggle the full spawn program for the agent owning the selected row:\n\t * one press shows it, another hides it. The subagent list is expanded as\n\t * needed so the code sits directly above the subagents it launched.\n\t */\n\tprivate cycleProgramForSelected(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row) {\n\t\t\treturn;\n\t\t}\n\t\tconst target = row.kind === \"agent\" ? row.identity : row.parentIdentity;\n\t\tif (!target) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.targetHasSpawnCode(target)) {\n\t\t\tthis.setStatusMessage(\"No program recorded for these subagents\");\n\t\t\treturn;\n\t\t}\n\t\t// Code only renders inside an expanded subagent list, so reveal it too.\n\t\tthis.expandedSubagentParents.add(target);\n\t\tif (this.programShownParents.has(target)) {\n\t\t\tthis.programShownParents.delete(target);\n\t\t} else {\n\t\t\tthis.programShownParents.add(target);\n\t\t}\n\t\tthis.rebuildRows();\n\t\tthis.syncSelectedRowState();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** Whether any subagent under the given agent identity carries spawn code. */\n\tprivate targetHasSpawnCode(target: string): boolean {\n\t\tfor (const row of this.rows) {\n\t\t\tif (row.parentIdentity !== target) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (row.kind === \"subagent-summary\") {\n\t\t\t\treturn row.hasSpawnCode === true;\n\t\t\t}\n\t\t\tif (row.kind === \"subagent\" && rowHasSpawnCode(row)) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\t\treturn false;\n\t}\n\n\t/** True when the selected row exposes the \"show program\" affordance. */\n\tprivate selectedRowCanShowProgram(): boolean {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row) {\n\t\t\treturn false;\n\t\t}\n\t\tconst target = row.kind === \"agent\" ? row.identity : row.parentIdentity;\n\t\treturn target !== undefined && this.targetHasSpawnCode(target);\n\t}\n\n\tprivate openSelectedSubagent(row: AgentsViewRow): void {\n\t\tconst expandedAncestorSessionIds = this.collectSubagentAncestorSessionIds(row);\n\t\tif (row.summary.activeSessionId || row.summary.sessionFile) {\n\t\t\tthis.finish({\n\t\t\t\ttype: \"open\",\n\t\t\t\tsummary: row.summary,\n\t\t\t\texpandedAncestorSessionIds,\n\t\t\t\thasChildren: hasUnifiedSessionChildren(\n\t\t\t\t\tthis.unifiedRecords,\n\t\t\t\t\tgetAgentsViewSelectionKey(row.summary),\n\t\t\t\t\tthis.unifiedIndex,\n\t\t\t\t),\n\t\t\t});\n\t\t\treturn;\n\t\t}\n\t\tconst root = this.findSubagentRootRow(row);\n\t\tif (!root || !(root.summary.activeSessionId || root.summary.sessionFile)) {\n\t\t\tthis.setStatusMessage(\"Cannot open agent without an active runtime or saved session file\");\n\t\t\treturn;\n\t\t}\n\t\tthis.finish(\n\t\t\tcreateUnattachableChildOpenResult(\n\t\t\t\trow.summary,\n\t\t\t\troot.summary,\n\t\t\t\texpandedAncestorSessionIds,\n\t\t\t\thasUnifiedSessionChildren(this.unifiedRecords, getAgentsViewSelectionKey(root.summary), this.unifiedIndex),\n\t\t\t),\n\t\t);\n\t}\n\n\t/** Session ids of every ancestor of a subagent row, root-most first. */\n\tprivate collectSubagentAncestorSessionIds(row: AgentsViewRow): string[] {\n\t\tconst ancestors: string[] = [];\n\t\tlet parentIdentity = row.parentIdentity;\n\t\twhile (parentIdentity !== undefined) {\n\t\t\tconst parent = this.rows.find((candidate) => candidate.identity === parentIdentity);\n\t\t\tif (!parent) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tancestors.unshift(parent.summary.sessionId);\n\t\t\tparentIdentity = parent.parentIdentity;\n\t\t}\n\t\treturn ancestors;\n\t}\n\n\t/**\n\t * The whole subagent tree belongs to the root agent's session, so nested\n\t * subagents also resolve to their top-level ancestor.\n\t */\n\tprivate findSubagentRootRow(row: AgentsViewRow): AgentsViewRow | undefined {\n\t\tlet root = this.rows.find((candidate) => candidate.identity === row.parentIdentity);\n\t\twhile (root && root.kind !== \"agent\") {\n\t\t\tconst parentIdentity = root.parentIdentity;\n\t\t\troot = this.rows.find((candidate) => candidate.identity === parentIdentity);\n\t\t}\n\t\treturn root;\n\t}\n\n\tprivate async toggleReplyTarget(): Promise<void> {\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\t// Subagents are read-only; replying is reserved for top-level agents.\n\t\tif (selectedRow?.kind !== \"agent\") {\n\t\t\treturn;\n\t\t}\n\t\tconst summary = selectedRow.summary;\n\t\t// Live agents reply directly; saved sessions are resumed when the reply is\n\t\t// sent. Rows with neither runtime nor file have nothing to receive a prompt.\n\t\tif (!summary.activeSessionId && !summary.sessionFile) {\n\t\t\treturn;\n\t\t}\n\t\tif (this.pendingDeleteAgent?.identity === getSelectedRowIdentity(selectedRow)) {\n\t\t\treturn;\n\t\t}\n\t\tconst key = summary.activeSessionId ?? summary.id;\n\t\tif (this.replyTarget?.key === key) {\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\treturn;\n\t\t}\n\t\tthis.setReplyTarget({ key, summary });\n\t\tif (!summary.activeSessionId) {\n\t\t\t// Inactive sessions have no live transcript endpoint; the persisted recap\n\t\t\t// (or opener) is the best preview and needs no daemon round-trip.\n\t\t\tthis.replyLastAssistantText = summary.summary ?? summary.firstMessage;\n\t\t\tthis.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\t\tconst activeSessionId = summary.activeSessionId;\n\t\tthis.replyLastAssistantTextLoading = true;\n\t\ttry {\n\t\t\tconst latestAssistantText = await this.getLastAssistantText(activeSessionId);\n\t\t\tif (this.replyTarget?.key === key) {\n\t\t\t\tthis.replyLastAssistantText = latestAssistantText;\n\t\t\t\tthis.replyLastAssistantTextLoading = false;\n\t\t\t\tthis.ui.requestRender();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (this.replyTarget?.key === key) {\n\t\t\t\tthis.replyLastAssistantTextLoading = false;\n\t\t\t\tthis.setStatusMessage(formatError(\"Failed to load latest response\", error));\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate setReplyTarget(target: { key: string; summary: SessionSummary } | undefined): void {\n\t\tif (target && !this.replyTarget) {\n\t\t\tthis.actionModeSearchQuery = this.editor.getText();\n\t\t\tthis.editor.setText(\"\");\n\t\t} else if (!target && this.replyTarget) {\n\t\t\tthis.editor.setText(this.actionModeSearchQuery ?? this.persistentState.query ?? \"\");\n\t\t\tthis.actionModeSearchQuery = undefined;\n\t\t}\n\t\tthis.replyTarget = target;\n\t\tthis.replyAutocomplete = target\n\t\t\t? createReplyComposerAutocompleteProvider(target.summary.cwd, this.fdPath)\n\t\t\t: undefined;\n\t\tthis.replyLastAssistantText = undefined;\n\t\tthis.replyLastAssistantTextLoading = false;\n\t\tthis.replyHeaderTime = target\n\t\t\t? formatAgentsViewRelativeTime(target.summary.modified ?? target.summary.created)\n\t\t\t: \"\";\n\t\tthis.editor.setPlaceholder(\n\t\t\ttarget\n\t\t\t\t? target.summary.activeSessionId\n\t\t\t\t\t? REPLY_PROMPT_FALLBACK_PLACEHOLDER\n\t\t\t\t\t: RESUME_PROMPT_PLACEHOLDER\n\t\t\t\t: SEARCH_PROMPT_PLACEHOLDER,\n\t\t);\n\t\tif (!target) this.rebuildRows();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate enterRenameMode(): void {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\t// Only top-level agents carry a renameable session; subagents do not.\n\t\tif (row?.kind !== \"agent\" || !row.selectable) {\n\t\t\treturn;\n\t\t}\n\t\tconst activeSessionId = row.summary.activeSessionId;\n\t\tconst sessionFile = row.summary.sessionFile;\n\t\tif (!activeSessionId && !sessionFile) {\n\t\t\tthis.setStatusMessage(\"This session cannot be renamed\");\n\t\t\treturn;\n\t\t}\n\t\tthis.setReplyTarget(undefined);\n\t\tthis.actionModeSearchQuery = this.editor.getText();\n\t\tthis.pendingDeleteAgent = undefined;\n\t\tthis.pendingKillSubagent = undefined;\n\t\tthis.renameTarget = { activeSessionId, sessionFile, summary: row.summary };\n\t\tthis.editor.setPlaceholder(\"Name this agent session\");\n\t\tthis.editor.setText(row.summary.sessionName ?? \"\");\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate exitRenameMode(): void {\n\t\tthis.renameTarget = undefined;\n\t\tthis.editor.setText(this.actionModeSearchQuery ?? this.persistentState.query ?? \"\");\n\t\tthis.actionModeSearchQuery = undefined;\n\t\tthis.editor.setPlaceholder(SEARCH_PROMPT_PLACEHOLDER);\n\t\tthis.rebuildRows();\n\t\tthis.ui.requestRender();\n\t}\n\n\tprivate async confirmRename(value: string): Promise<void> {\n\t\tconst target = this.renameTarget;\n\t\tif (!target) {\n\t\t\treturn;\n\t\t}\n\t\tconst name = value.trim();\n\t\tif (!name) {\n\t\t\tthis.exitRenameMode();\n\t\t\treturn;\n\t\t}\n\t\tthis.exitRenameMode();\n\t\tawait this.renameSession(target.summary, name);\n\t}\n\n\t/** Shared by rename mode and /name: rename, refresh both catalogs, report. */\n\tprivate async renameSession(summary: SessionSummary, name: string): Promise<boolean> {\n\t\tthis.setStatusMessage(\"Renaming agent...\");\n\t\ttry {\n\t\t\tif (summary.activeSessionId) {\n\t\t\t\trequireDaemonData(\n\t\t\t\t\tawait this.requireClient().request({ type: \"rename\", activeSessionId: summary.activeSessionId, name }),\n\t\t\t\t);\n\t\t\t} else if (summary.sessionFile) {\n\t\t\t\tawait renameDaemonSavedSession(\n\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\tsummary.sessionFile,\n\t\t\t\t\tname,\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tthis.setStatusMessage(\"This session cannot be renamed\", { tone: \"warning\" });\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst refreshed = await this.refreshBothCatalogs();\n\t\t\tthis.setStatusMessage(refreshed ? `Renamed to ${name}` : `Renamed to ${name}; refresh failed`);\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(\n\t\t\t\tisUnknownDaemonCommandError(error, \"rename\")\n\t\t\t\t\t? \"Failed to rename: the daemon is running an older build; restart the daemon and try again\"\n\t\t\t\t\t: formatError(\"Failed to rename agent\", error),\n\t\t\t);\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate findSummaryByActiveSessionId(activeSessionId: string): SessionSummary | undefined {\n\t\treturn this.rows.find((row) => (row.summary.activeSessionId ?? row.summary.id) === activeSessionId)?.summary;\n\t}\n\n\tprivate renderReplyHeaderLine(): string | undefined {\n\t\tif (this.renameTarget) {\n\t\t\treturn theme.fg(\"warning\", \"Rename agent session\");\n\t\t}\n\t\tif (!this.replyTarget) {\n\t\t\treturn undefined;\n\t\t}\n\t\tconst headline =\n\t\t\tcreateAgentsViewReplyHeadline(this.replyLastAssistantText) ??\n\t\t\ttheme.fg(\"dim\", this.replyLastAssistantTextLoading ? \"Loading last response...\" : \"No response yet\");\n\t\treturn this.replyHeaderTime ? `${theme.fg(\"warning\", this.replyHeaderTime)} ${headline}` : headline;\n\t}\n\n\tprivate async getLastAssistantText(activeSessionId: string): Promise<string | undefined> {\n\t\tconst response = await this.requireClient().request({\n\t\t\ttype: \"get_last_assistant_text\",\n\t\t\tactiveSessionId,\n\t\t});\n\t\tconst data = requireDaemonData(response);\n\t\tif (!isRecord(data)) {\n\t\t\tthrow new Error(\"Daemon returned an invalid last assistant response\");\n\t\t}\n\t\tif (data.text === null || data.text === undefined) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (typeof data.text !== \"string\") {\n\t\t\tthrow new Error(\"Daemon returned an invalid last assistant response\");\n\t\t}\n\t\treturn data.text;\n\t}\n\n\tprivate async sendReply(\n\t\ttarget: { key: string; summary: SessionSummary },\n\t\ttext: string,\n\t\tdelivery: \"steer\" | \"followUp\" = \"steer\",\n\t): Promise<boolean> {\n\t\tconst currentSummary = resolveCurrentReplyTargetSummary(this.unifiedRecords ?? [], target, (activeSessionId) =>\n\t\t\tthis.findSummaryByActiveSessionId(activeSessionId),\n\t\t);\n\t\tlet activeSessionId = currentSummary.activeSessionId;\n\t\tlet liveSummary = activeSessionId ? currentSummary : undefined;\n\t\tlet cwdFallbackNotice: string | undefined;\n\t\tlet didResume = false;\n\t\ttry {\n\t\t\tif (!activeSessionId) {\n\t\t\t\t// Saved session: resume it into the daemon first, then deliver the\n\t\t\t\t// prompt through the same path as a live reply.\n\t\t\t\tthis.setStatusMessage(\"Resuming session...\");\n\t\t\t\tconst resumed = await resumeSavedAgentsViewSession(\n\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\tthis.options.config,\n\t\t\t\t\tcurrentSummary,\n\t\t\t\t);\n\t\t\t\tactiveSessionId = resumed.activeSessionId;\n\t\t\t\tdidResume = true;\n\t\t\t\t// The rows are still pre-resume; the fresh summary is the authoritative\n\t\t\t\t// streaming state for scheduling the prompt.\n\t\t\t\tliveSummary = resumed.summary;\n\t\t\t\tcwdFallbackNotice = resumed.cwdFallbackNotice;\n\t\t\t\tthis.inactiveAgentIdentities.delete(getSummaryIdentity(target.summary));\n\t\t\t\t// The resume and delivery still belong to this submission, but selection\n\t\t\t\t// belongs to the current composer. Do not steal it after cancellation.\n\t\t\t\tif (this.replyTarget === target) this.selectSummary(resumed.summary);\n\t\t\t}\n\t\t\tconst behavior = delivery === \"followUp\" ? \"followUp\" : liveSummary?.isStreaming ? \"steer\" : undefined;\n\t\t\tthis.setStatusMessage(\"Sending reply...\");\n\t\t\tawait this.sendPrompt(activeSessionId, text, behavior);\n\t\t\t// The fallback-directory notice must outlive the transient send statuses.\n\t\t\tif (cwdFallbackNotice) this.setStatusMessage(cwdFallbackNotice, { sticky: true });\n\t\t\telse this.setStatusMessage(\"Reply sent\");\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to send reply\", error));\n\t\t\tif (didResume) await this.refreshSessions({ preserveStatusOnError: true });\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/** Create a fresh daemon session and open it in the chat view. */\n\tprivate async createNewSession(): Promise<boolean> {\n\t\tif (this.creatingNewSession || this.stopped) return false;\n\t\tthis.creatingNewSession = true;\n\t\ttry {\n\t\t\tconst client = await this.connectDedicatedClient();\n\t\t\ttry {\n\t\t\t\tthis.setStatusMessage(\"Creating session...\");\n\t\t\t\tconst response = await client.request({\n\t\t\t\t\ttype: \"create\",\n\t\t\t\t\tconfig: this.options.config,\n\t\t\t\t\tenv: collectDaemonClientEnv(),\n\t\t\t\t});\n\t\t\t\tconst created = expectSessionSummary(requireDaemonData(response));\n\t\t\t\t// The view can finish mid-create; kill the fresh session instead of orphaning it.\n\t\t\t\tif (this.stopped) {\n\t\t\t\t\tif (created.activeSessionId) {\n\t\t\t\t\t\tawait client\n\t\t\t\t\t\t\t.request({ type: \"kill\", activeSessionId: created.activeSessionId })\n\t\t\t\t\t\t\t.catch(() => undefined);\n\t\t\t\t\t}\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tthis.selectSummary(created);\n\t\t\t\tthis.finish({ type: \"open\", summary: created });\n\t\t\t\treturn true;\n\t\t\t} finally {\n\t\t\t\tclient.close();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (!this.stopped) this.setStatusMessage(formatError(\"Failed to create session\", error));\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tthis.creatingNewSession = false;\n\t\t}\n\t}\n\n\t/**\n\t * Run a view command against the armed composer's target. Returns whether\n\t * it completed so callers can restore the draft; disarms are guarded\n\t * against a composer re-armed during the awaited RPCs.\n\t */\n\tprivate async runAgentsViewCommand(command: AgentsViewCommand, target: SessionSummary): Promise<boolean> {\n\t\tconst armedAtStart = this.replyTarget;\n\t\tconst disarmIfUnchanged = () => {\n\t\t\tif (armedAtStart && this.replyTarget === armedAtStart) this.setReplyTarget(undefined);\n\t\t};\n\t\ttry {\n\t\t\tswitch (command.name) {\n\t\t\t\tcase \"name\": {\n\t\t\t\t\tconst name = command.args.trim();\n\t\t\t\t\tif (!name) {\n\t\t\t\t\t\tthis.setStatusMessage(\"Usage: /name <session name>\", { tone: \"warning\" });\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\treturn await this.renameSession(target, name);\n\t\t\t\t}\n\t\t\t\tcase \"kill\": {\n\t\t\t\t\tif (!target.activeSessionId) {\n\t\t\t\t\t\tthis.setStatusMessage(\"/kill needs a running agent; this session is inactive\", { tone: \"warning\" });\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t\ttry {\n\t\t\t\t\t\trequireDaemonData(\n\t\t\t\t\t\t\tawait this.requireClient().request({ type: \"kill\", activeSessionId: target.activeSessionId }),\n\t\t\t\t\t\t);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t// As in deactivatePendingAgent: an agent that already finished counts as stopped.\n\t\t\t\t\t\tif (!isUnknownActiveSessionError(error)) throw error;\n\t\t\t\t\t}\n\t\t\t\t\tdisarmIfUnchanged();\n\t\t\t\t\tthis.setStatusMessage(\"Agent stopped\");\n\t\t\t\t\tawait this.refreshBothCatalogs();\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(`Failed to run /${command.name}`, error));\n\t\t\treturn false;\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Outlives finish(), which closes the shared client and would reject an\n\t * in-flight create while the daemon still materializes the session.\n\t */\n\tprivate async connectDedicatedClient(): Promise<DaemonClient> {\n\t\treturn connectAgentsViewDaemonClient(this.requireSocketPath());\n\t}\n\n\t/** Point selection (and its persisted key) at a freshly resumed session row. */\n\tprivate selectSummary(summary: SessionSummary): void {\n\t\tthis.selectedRowIdentity = getSummaryIdentity(summary);\n\t\tthis.selectedActiveSessionId = summary.activeSessionId ?? summary.id;\n\t\tthis.selectedSessionKey = getAgentsViewSelectionKey(summary);\n\t\tthis.persistentState.selectedRowIdentity = this.selectedRowIdentity;\n\t\tthis.persistentState.selectedSessionKey = this.selectedSessionKey;\n\t}\n\n\tprivate async handleDeleteSelected(): Promise<void> {\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tif (!row?.selectable) {\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind === \"subagent\") {\n\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\tawait this.handleKillSubagentSelected(row);\n\t\t\treturn;\n\t\t}\n\t\tif (row.kind !== \"agent\") {\n\t\t\treturn;\n\t\t}\n\t\tthis.pendingKillSubagent = undefined;\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tif (!row.summary.activeSessionId && row.summary.sessionFile) {\n\t\t\tif (this.pendingDeleteAgent?.identity === identity && this.isDeleteConfirmationVisible()) {\n\t\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\t\ttry {\n\t\t\t\t\tconst latest = expectSessionList(\n\t\t\t\t\t\trequireDaemonData(await this.requireClient().request(createAgentsViewListCommand())),\n\t\t\t\t\t);\n\t\t\t\t\tthis.lastListedSummaries = latest;\n\t\t\t\t\tconst active = resolveAgentsViewActiveSummaryForPath(row.summary.sessionFile, latest);\n\t\t\t\t\tif (active) {\n\t\t\t\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\t\t\t\tthis.setStatusMessage(\"Session became active; stop it before deleting\", { tone: \"warning\" });\n\t\t\t\t\t\tawait this.refreshSessions();\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tconst result = await deleteDaemonSavedSession(\n\t\t\t\t\t\tthis.requireClient(),\n\t\t\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\t\trow.summary.sessionFile,\n\t\t\t\t\t);\n\t\t\t\t\tif (!result.ok) {\n\t\t\t\t\t\tthis.setStatusMessage(`Failed to delete session: ${result.error ?? \"Unknown error\"}`, {\n\t\t\t\t\t\t\ttone: \"error\",\n\t\t\t\t\t\t});\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\t\t\tconst refreshed = await this.refreshSavedSessions({ preserveStatusOnError: true });\n\t\t\t\t\tconst success = result.method === \"trash\" ? \"Session moved to trash\" : \"Session deleted\";\n\t\t\t\t\tthis.setStatusMessage(refreshed ? success : `${success}; refresh failed`);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to delete session\", error));\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tif (this.pendingDeleteAgent?.identity === identity) {\n\t\t\tif (this.isDeleteConfirmationVisible()) {\n\t\t\t\tawait this.deactivatePendingAgent();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tawait this.stopAgentForDeletion(row);\n\t}\n\n\tprivate async handleKillSubagentSelected(row: AgentsViewRow): Promise<void> {\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tif (this.pendingKillSubagent?.identity === identity && this.isDeleteConfirmationVisible()) {\n\t\t\tconst pending = this.pendingKillSubagent;\n\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\tawait this.killSubagent(pending, row);\n\t\t\treturn;\n\t\t}\n\t\tconst childId = row.summary.rlmChildId;\n\t\tconst rootActiveSessionId = this.findSubagentRootRow(row)?.summary.activeSessionId;\n\t\tif (!childId || !rootActiveSessionId) {\n\t\t\tthis.setStatusMessage(\"Cannot stop subagent without its parent agent\");\n\t\t\treturn;\n\t\t}\n\t\tthis.pendingKillSubagent = { identity, rootActiveSessionId, childId };\n\t\tthis.showDeleteConfirmation();\n\t}\n\n\tprivate async killSubagent(pending: PendingKillSubagent, currentRow: AgentsViewRow): Promise<void> {\n\t\tconst running = currentRow.section === \"running\";\n\t\tconst client = this.requireClient();\n\t\tthis.setStatusMessage(running ? \"Stopping subagent...\" : \"Deleting subagent...\");\n\t\ttry {\n\t\t\tif (!running && client.supportsServerCapability(\"delete_rlm_subagent\")) {\n\t\t\t\tconst data = requireDaemonData(\n\t\t\t\t\tawait client.request({\n\t\t\t\t\t\ttype: \"delete_rlm_subagent\",\n\t\t\t\t\t\tactiveSessionId: pending.rootActiveSessionId,\n\t\t\t\t\t\tchildId: pending.childId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tconst deleted = isRecord(data) && data.deleted === true;\n\t\t\t\tconst stillRunning = isRecord(data) && data.reason === \"running\";\n\t\t\t\tthis.setStatusMessage(\n\t\t\t\t\tdeleted\n\t\t\t\t\t\t? \"Subagent deleted\"\n\t\t\t\t\t\t: stillRunning\n\t\t\t\t\t\t\t? \"Subagent is running; stop it first\"\n\t\t\t\t\t\t\t: \"Subagent already removed\",\n\t\t\t\t\t{ render: false },\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst data = requireDaemonData(\n\t\t\t\t\tawait client.request({\n\t\t\t\t\t\ttype: \"cancel_rlm_child\",\n\t\t\t\t\t\tactiveSessionId: pending.rootActiveSessionId,\n\t\t\t\t\t\tchildId: pending.childId,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t\tconst cancelled = isRecord(data) && data.cancelled === true;\n\t\t\t\tthis.setStatusMessage(\n\t\t\t\t\trunning\n\t\t\t\t\t\t? cancelled\n\t\t\t\t\t\t\t? \"Subagent stopped\"\n\t\t\t\t\t\t\t: \"Subagent already finished\"\n\t\t\t\t\t\t: \"The daemon cannot delete subagents; it was left unchanged\",\n\t\t\t\t\t{ render: false, ...(running ? {} : { tone: \"warning\" as const }) },\n\t\t\t\t);\n\t\t\t}\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tconst command = running ? \"cancel_rlm_child\" : \"delete_rlm_subagent\";\n\t\t\tthis.setStatusMessage(\n\t\t\t\tisUnknownDaemonCommandError(error, command)\n\t\t\t\t\t? \"Failed to update subagent: the daemon is running an older build; restart the daemon and try again\"\n\t\t\t\t\t: formatError(\"Failed to update subagent\", error),\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async stopAgentForDeletion(row: AgentsViewRow): Promise<void> {\n\t\tconst identity = getSummaryIdentity(row.summary);\n\t\tconst activeSessionId = row.summary.activeSessionId;\n\t\tif (!activeSessionId) {\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tif (!isRunningSessionSummary(row.summary)) {\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tactiveSessionId,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: false,\n\t\t\t};\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.showDeleteConfirmation();\n\t\t\treturn;\n\t\t}\n\t\tthis.setStatusMessage(\"Stopping agent...\");\n\t\ttry {\n\t\t\tconst response = await this.requireClient().request({\n\t\t\t\ttype: \"kill\",\n\t\t\t\tactiveSessionId,\n\t\t\t});\n\t\t\trequireDaemonData(response);\n\t\t\tthis.pendingDeleteAgent = {\n\t\t\t\tidentity,\n\t\t\t\tactiveSessionId,\n\t\t\t\tsessionFile: row.summary.sessionFile,\n\t\t\t\tsummary: row.summary,\n\t\t\t\tstopped: true,\n\t\t\t};\n\t\t\tthis.selectedActiveSessionId = activeSessionId;\n\t\t\tthis.setReplyTarget(undefined);\n\t\t\tthis.setStatusMessage(undefined, { render: false });\n\t\t\tthis.showDeleteConfirmation();\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to stop agent\", error));\n\t\t}\n\t}\n\n\tprivate async deactivatePendingAgent(): Promise<void> {\n\t\tconst pending = this.pendingDeleteAgent;\n\t\tif (!pending) {\n\t\t\treturn;\n\t\t}\n\t\tthis.setStatusMessage(\"Deactivating agent...\");\n\t\ttry {\n\t\t\tif (pending.activeSessionId) {\n\t\t\t\ttry {\n\t\t\t\t\tconst killResponse = await this.requireClient().request({\n\t\t\t\t\t\ttype: \"kill\",\n\t\t\t\t\t\tactiveSessionId: pending.activeSessionId,\n\t\t\t\t\t});\n\t\t\t\t\trequireDaemonData(killResponse);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (!isUnknownActiveSessionError(error)) {\n\t\t\t\t\t\tthrow error;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Skip a file deleted between listing and now: SessionManager.open would\n\t\t\t// recreate a stub at the old path instead of loading it.\n\t\t\tif (pending.sessionFile && existsSync(pending.sessionFile)) {\n\t\t\t\t// Persist archived unless it already is: sessions with no prior\n\t\t\t\t// session_state entry would otherwise resurface on the next scan.\n\t\t\t\tconst sessionManager = SessionManager.open(pending.sessionFile, this.options.config.sessionDir);\n\t\t\t\tif (sessionManager.getSessionState()?.status !== \"archived\") {\n\t\t\t\t\tsessionManager.appendSessionState({ status: \"archived\" });\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.inactiveAgentIdentities.add(pending.identity);\n\t\t\tthis.pendingDeleteAgent = undefined;\n\t\t\tthis.clearDeleteConfirmation({ render: false });\n\t\t\tthis.selectedActiveSessionId = undefined;\n\t\t\tthis.setStatusMessage(\"Agent inactive\", { render: false });\n\t\t\tawait this.refreshSessions();\n\t\t} catch (error) {\n\t\t\tthis.setStatusMessage(formatError(\"Failed to deactivate agent\", error));\n\t\t}\n\t}\n\n\tprivate async sendPrompt(\n\t\tactiveSessionId: string,\n\t\tmessage: string,\n\t\tstreamingBehavior?: \"steer\" | \"followUp\",\n\t): Promise<void> {\n\t\tconst command: PromptCommand = { type: \"prompt\", activeSessionId, message };\n\t\tif (streamingBehavior) command.streamingBehavior = streamingBehavior;\n\t\tconst response = await this.requireClient().request(command);\n\t\trequireDaemonData(response);\n\t}\n\n\tprivate pollSessions(): void {\n\t\tif (this.liveCatalogPollPromise) return;\n\t\tconst poll = this.refreshSessions().then(() => undefined);\n\t\tthis.liveCatalogPollPromise = poll;\n\t\tvoid poll.finally(() => {\n\t\t\tif (this.liveCatalogPollPromise === poll) this.liveCatalogPollPromise = undefined;\n\t\t});\n\t}\n\n\tprivate async refreshSessions(options: { preserveStatusOnError?: boolean } = {}): Promise<boolean> {\n\t\tif (this.reconnectPromise || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.liveCatalogGeneration;\n\t\tthis.liveCatalogRefreshPending = true;\n\t\ttry {\n\t\t\tconst client = this.requireClient();\n\t\t\ttry {\n\t\t\t\tconst response = await client.request(createAgentsViewListCommand());\n\t\t\t\tif (generation !== this.liveCatalogGeneration) return false;\n\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\tthis.applySessionList(expectSessionList(requireDaemonData(response)), true);\n\t\t\t\treturn true;\n\t\t\t} catch (error) {\n\t\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\t\t// A completed failed attempt is still catalog-settled: otherwise a\n\t\t\t\t\t// vanished scope can permanently trap an empty view.\n\t\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\t\tif (!options.preserveStatusOnError && !this.reconnectPromise) {\n\t\t\t\t\t\tif (client.isConnected) this.setStatusMessage(formatError(\"Failed to refresh agents\", error));\n\t\t\t\t\t\telse this.startClientReconnect(client, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn false;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\tthis.liveCatalogReady = true;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\tif (!options.preserveStatusOnError) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to refresh current session\", error));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tif (generation === this.liveCatalogGeneration) {\n\t\t\t\tthis.liveCatalogRefreshPending = false;\n\t\t\t\tthis.resolveMissingSelectionAnchor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate applySessionList(sessions: SessionSummary[], successful = false): void {\n\t\tthis.lastListedSummaries = sessions;\n\t\tif (successful) this.persistentState.lastSuccessfulLiveSummaries = sessions;\n\t\tthis.reconcileCatalogs();\n\t}\n\n\tprivate reconcileCatalogs(): void {\n\t\tconst visibleSessions = this.lastListedSummaries.filter((summary) =>\n\t\t\tshouldShowAgentsViewSession(summary, this.inactiveAgentIdentities.has(getSummaryIdentity(summary))),\n\t\t);\n\t\tthis.lastVisibleSummaries = this.withPendingDeleteSession(visibleSessions);\n\t\tthis.unifiedRecords = reconcileUnifiedSessions(this.lastVisibleSummaries, this.savedSessions, this.heartbeats);\n\t\tthis.unifiedIndex = buildUnifiedSessionIndex(this.unifiedRecords);\n\t\tmigrateAgentsViewIdentitySet(this.expandedSubagentParents, this.unifiedIndex.byKey);\n\t\tmigrateAgentsViewIdentitySet(this.programShownParents, this.unifiedIndex.byKey);\n\n\t\tconst frames = this.persistentState.scopeFrames ?? [];\n\t\tconst resolution = resolveAgentsViewScopeFrames(this.unifiedRecords, frames, this.unifiedIndex);\n\t\tif (shouldApplyScopeResolution(resolution.droppedFrames, this.liveCatalogReady, this.savedCatalogReady)) {\n\t\t\tthis.persistentState.scopeFrames = resolution.frames;\n\t\t\tthis.scopeKey = resolution.frames.at(-1)?.scope;\n\t\t\tthis.scopeRootSummary = resolution.root ? summaryForUnifiedRecord(resolution.root) : undefined;\n\t\t\tthis.persistentState.scopeRootSummary = this.scopeRootSummary;\n\t\t\tif (resolution.droppedFrames > 0) {\n\t\t\t\tconst destination = resolution.root ? \"the nearest available parent\" : \"the global view\";\n\t\t\t\tthis.setStatusMessage(`Scope is no longer available; returned to ${destination}`, { render: false });\n\t\t\t}\n\t\t}\n\t\tthis.scopedRecords = scopeToSessionSubtree(this.unifiedRecords, this.scopeKey, this.unifiedIndex);\n\t\tthis.rows = buildAgentsViewRows(\n\t\t\tthis.getFilteredRecords(),\n\t\t\tthis.expandedSubagentParents,\n\t\t\tthis.programShownParents,\n\t\t\tthis.scopeKey,\n\t\t);\n\t\tthis.applyPendingAncestorExpansion();\n\t\tthis.restoreSelection();\n\t\tthis.ui.requestRender();\n\t}\n\n\t/** A session appears in both catalogs; mutations must refresh both. */\n\tprivate async refreshBothCatalogs(): Promise<boolean> {\n\t\tconst results = await Promise.all([\n\t\t\tthis.refreshSessions({ preserveStatusOnError: true }),\n\t\t\tthis.refreshSavedSessions({ preserveStatusOnError: true }),\n\t\t]);\n\t\treturn results.every(Boolean);\n\t}\n\n\tprivate async refreshSavedSessions(\n\t\toptions: { duringReconnect?: boolean; preserveStatusOnError?: boolean } = {},\n\t): Promise<boolean> {\n\t\tif ((!options.duringReconnect && this.reconnectPromise) || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.savedCatalogGeneration;\n\t\tthis.persistentState.savedCatalogGeneration = generation;\n\t\tthis.savedCatalogRefreshPending = true;\n\t\tthis.savedCatalogReady = false;\n\t\tconst successfulSessions = this.lastSuccessfulSavedSessions;\n\t\tconst progressiveSessions = new Map(\n\t\t\tsuccessfulSessions.map((session) => [resolvePath(canonicalizePath(session.path)), session]),\n\t\t);\n\t\ttry {\n\t\t\tconst onSession = (session: AgentConnectionSavedSessionInfo) => {\n\t\t\t\tif (generation !== this.savedCatalogGeneration) return;\n\t\t\t\tprogressiveSessions.set(resolvePath(canonicalizePath(session.path)), session);\n\t\t\t\tthis.savedSessions = [...progressiveSessions.values()];\n\t\t\t\tthis.persistentState.savedSessions = this.savedSessions;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t};\n\t\t\tconst sessions = await listDaemonSavedSessions(\n\t\t\t\tthis.requireClient(),\n\t\t\t\tthis.getSavedSessionCatalogContext(),\n\t\t\t\t\"all\",\n\t\t\t\t{\n\t\t\t\t\tonSession,\n\t\t\t\t},\n\t\t\t);\n\t\t\tif (generation !== this.savedCatalogGeneration) return false;\n\t\t\tthis.savedSessions = sessions;\n\t\t\tthis.lastSuccessfulSavedSessions = sessions;\n\t\t\tthis.savedCatalogReady = true;\n\t\t\tthis.persistentState.lastSuccessfulSavedSessions = sessions;\n\t\t\tthis.persistentState.savedSessions = sessions;\n\t\t\tthis.reconcileCatalogs();\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (generation === this.savedCatalogGeneration) {\n\t\t\t\tthis.savedSessions = successfulSessions;\n\t\t\t\tthis.persistentState.savedSessions = successfulSessions;\n\t\t\t\t// Treat a terminal failure as settled so scope fallback cannot soft-lock.\n\t\t\t\tthis.savedCatalogReady = true;\n\t\t\t\tthis.reconcileCatalogs();\n\t\t\t\tif (!options.preserveStatusOnError && !this.reconnectPromise && !this.daemonShutdownReceived) {\n\t\t\t\t\tthis.setStatusMessage(formatError(\"Failed to load saved sessions\", error));\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn false;\n\t\t} finally {\n\t\t\tif (generation === this.savedCatalogGeneration) {\n\t\t\t\tthis.savedCatalogRefreshPending = false;\n\t\t\t\tthis.resolveMissingSelectionAnchor();\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate async refreshHeartbeats(options: { duringReconnect?: boolean } = {}): Promise<boolean> {\n\t\tif ((!options.duringReconnect && this.reconnectPromise) || this.daemonShutdownReceived) return false;\n\t\tconst generation = ++this.heartbeatCatalogGeneration;\n\t\ttry {\n\t\t\tconst heartbeats = await listDaemonHeartbeats(this.requireClient());\n\t\t\tif (generation !== this.heartbeatCatalogGeneration) return false;\n\t\t\tthis.heartbeats = heartbeats;\n\t\t\tthis.persistentState.heartbeats = heartbeats;\n\t\t\tthis.reconcileCatalogs();\n\t\t\treturn true;\n\t\t} catch (error) {\n\t\t\tif (generation === this.heartbeatCatalogGeneration && !this.reconnectPromise) {\n\t\t\t\tthis.setStatusMessage(formatError(\"Failed to refresh heartbeats\", error));\n\t\t\t}\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate withPendingDeleteSession(sessions: readonly SessionSummary[]): SessionSummary[] {\n\t\tconst pending = this.pendingDeleteAgent;\n\t\t// Saved-only rows already come from the durable catalog. Injecting their\n\t\t// synthetic archived summary as a daemon record would move confirmation\n\t\t// from Inactive to Idle.\n\t\tif (!pending || pending.summary.lifecycle !== \"live\") {\n\t\t\treturn [...sessions];\n\t\t}\n\t\tif (!this.isDeleteConfirmationVisible()) {\n\t\t\treturn [...sessions];\n\t\t}\n\t\tlet replaced = false;\n\t\tconst merged = sessions.map((summary) => {\n\t\t\tif (getSummaryIdentity(summary) !== pending.identity) {\n\t\t\t\treturn summary;\n\t\t\t}\n\t\t\treplaced = true;\n\t\t\treturn pending.summary;\n\t\t});\n\t\treturn replaced ? merged : [...merged, pending.summary];\n\t}\n\n\tprivate resolveMissingSelectionAnchor(): void {\n\t\tif (!this.selectionAnchorPending || this.liveCatalogRefreshPending || this.savedCatalogRefreshPending) {\n\t\t\treturn;\n\t\t}\n\t\t// Unblock the fallback row, but keep the anchor identities: a later poll\n\t\t// can still deliver the intended session and re-anchor selection to it.\n\t\tthis.selectionAnchorPending = false;\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = row?.selectable ? (row.summary.activeSessionId ?? row.summary.id) : undefined;\n\t}\n\n\tprivate restoreSelection(): void {\n\t\tif (this.rows.length === 0) {\n\t\t\tthis.selectedIndex = 0;\n\t\t\tthis.selectedActiveSessionId = undefined;\n\t\t\treturn;\n\t\t}\n\t\tconst resolution = resolveAgentsViewSelectionState(\n\t\t\tthis.rows,\n\t\t\tthis.selectedIndex,\n\t\t\tthis.selectedRowIdentity ?? this.persistentState.selectedRowIdentity,\n\t\t\tthis.selectedSessionKey ?? this.persistentState.selectedSessionKey,\n\t\t);\n\t\tthis.selectedIndex = resolution.index;\n\t\tif (resolution.resolved) {\n\t\t\tthis.syncSelectedRowState();\n\t\t\treturn;\n\t\t}\n\t\tthis.selectionAnchorPending = Boolean(\n\t\t\tthis.selectedRowIdentity ??\n\t\t\t\tthis.persistentState.selectedRowIdentity ??\n\t\t\t\tthis.selectedSessionKey ??\n\t\t\t\tthis.persistentState.selectedSessionKey,\n\t\t);\n\t\t// Catalogs stream independently. Show a temporary fallback row without\n\t\t// replacing the source-session anchor before its daemon row arrives.\n\t\tconst fallback = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = fallback?.selectable\n\t\t\t? (fallback.summary.activeSessionId ?? fallback.summary.id)\n\t\t\t: undefined;\n\t}\n\n\tprivate getSelectableRowIndexes(): number[] {\n\t\treturn this.rows.flatMap((row, index) => (row.selectable ? [index] : []));\n\t}\n\n\tprivate syncSelectedRowState(): void {\n\t\tthis.selectionAnchorPending = false;\n\t\tconst row = this.rows[this.selectedIndex];\n\t\tthis.selectedActiveSessionId = row?.selectable ? (row.summary.activeSessionId ?? row.summary.id) : undefined;\n\t\tthis.selectedRowIdentity = getSelectedRowIdentity(row);\n\t\tthis.selectedSessionKey = row?.selectable ? getAgentsViewSelectionKey(row.summary) : undefined;\n\t\tthis.persistentState.selectedRowIdentity = this.selectedRowIdentity;\n\t\tthis.persistentState.selectedSessionKey = this.selectedSessionKey;\n\t}\n\n\tprivate finish(result: AgentsViewRunResult): void {\n\t\tif (this.stopped) {\n\t\t\treturn;\n\t\t}\n\t\tthis.stopped = true;\n\t\tthis.savedCatalogGeneration += 1;\n\t\tthis.liveCatalogGeneration += 1;\n\t\tthis.heartbeatCatalogGeneration += 1;\n\t\tif (this.pollTimer) {\n\t\t\tclearInterval(this.pollTimer);\n\t\t\tthis.pollTimer = undefined;\n\t\t}\n\t\tif (this.heartbeatPollTimer) {\n\t\t\tclearInterval(this.heartbeatPollTimer);\n\t\t\tthis.heartbeatPollTimer = undefined;\n\t\t}\n\t\tif (this.animationTimer) {\n\t\t\tclearInterval(this.animationTimer);\n\t\t\tthis.animationTimer = undefined;\n\t\t}\n\t\tthis.clearCtrlCExitHint({ render: false });\n\t\tthis.clearDeleteConfirmation({ render: false });\n\t\tthis.setStatusMessage(undefined, { render: false });\n\t\tthis.ui.stop({\n\t\t\tpreserveAltScreen: result.type !== \"exit\",\n\t\t\tflushFullscreen: false,\n\t\t});\n\t\tstopThemeWatcher();\n\t\tthis.unsubscribeClientClose?.();\n\t\tthis.unsubscribeClientClose = undefined;\n\t\tthis.unsubscribeClientMessage?.();\n\t\tthis.unsubscribeClientMessage = undefined;\n\t\tthis.client?.close();\n\t\tthis.client = undefined;\n\t\tthis.resolveRun?.(result);\n\t\tthis.resolveRun = undefined;\n\t}\n\n\tprivate subscribeToClientClose(client: DaemonClient): void {\n\t\tthis.unsubscribeClientClose?.();\n\t\tthis.unsubscribeClientClose = client.onClose((error) => {\n\t\t\tif (!shouldReconnectAgentsViewDaemon(getDaemonSocketCloseReason(error))) {\n\t\t\t\tthis.handleDaemonShutdown(client, error);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.startClientReconnect(client, error);\n\t\t});\n\t}\n\n\tprivate handleDaemonShutdown(client: DaemonClient, error: Error): void {\n\t\tif (this.stopped || client !== this.client) {\n\t\t\treturn;\n\t\t}\n\t\tthis.daemonShutdownReceived = true;\n\t\tthis.reconnectTimedOut = false;\n\t\tthis.setStatusMessage(`ASM Agent daemon shut down. Restart ASM Agent to reconnect. ${error.message}`, {\n\t\t\ttone: \"error\",\n\t\t\tsticky: true,\n\t\t});\n\t\tthis.applySessionList([]);\n\t}\n\n\tprivate startClientReconnect(client: DaemonClient, error: unknown): void {\n\t\tif (this.stopped || client !== this.client || this.reconnectPromise || this.daemonShutdownReceived) {\n\t\t\treturn;\n\t\t}\n\t\tif (!this.reconnectTimedOut) {\n\t\t\tthis.setStatusMessage(\"Daemon connection lost; reconnecting…\", { tone: \"warning\", sticky: true });\n\t\t}\n\t\tconst reconnectPromise = this.reconnectClient(client, error).finally(() => {\n\t\t\tif (this.reconnectPromise === reconnectPromise) {\n\t\t\t\tthis.reconnectPromise = undefined;\n\t\t\t}\n\t\t});\n\t\tthis.reconnectPromise = reconnectPromise;\n\t}\n\n\tprivate async reconnectClient(client: DaemonClient, initialError: unknown): Promise<void> {\n\t\tconst deadline = Date.now() + (this.options.reconnectTimeoutMs ?? RECONNECT_TIMEOUT_MS);\n\t\tlet lastError = initialError;\n\t\twhile (!this.stopped && !this.daemonShutdownReceived && client === this.client && Date.now() < deadline) {\n\t\t\ttry {\n\t\t\t\tawait this.options.recoverDaemon?.();\n\t\t\t\tawait client.reconnect(1000);\n\t\t\t\tconst response = await client.request(createAgentsViewListCommand());\n\t\t\t\tconst data = requireDaemonData(response);\n\t\t\t\tconst sessions = expectSessionList(data);\n\t\t\t\tconst heartbeatsRefreshed = await this.refreshHeartbeats({ duringReconnect: true });\n\t\t\t\tif (!heartbeatsRefreshed) throw new Error(\"Heartbeat catalog did not refresh during reconnect\");\n\t\t\t\tthis.daemonShutdownReceived = false;\n\t\t\t\tthis.reconnectTimedOut = false;\n\t\t\t\tthis.setStatusMessage(\"Daemon reconnected\", { render: false });\n\t\t\t\tthis.applySessionList(sessions, true);\n\t\t\t\tvoid this.refreshSavedSessions({ duringReconnect: true, preserveStatusOnError: true });\n\t\t\t\treturn;\n\t\t\t} catch (error) {\n\t\t\t\tlastError = error;\n\t\t\t}\n\t\t\tawait new Promise<void>((resolve) => {\n\t\t\t\tconst retryTimer = setTimeout(resolve, RECONNECT_RETRY_MS);\n\t\t\t\tretryTimer.unref?.();\n\t\t\t});\n\t\t}\n\t\tif (!this.stopped && !this.daemonShutdownReceived && client === this.client) {\n\t\t\tthis.reconnectTimedOut = true;\n\t\t\tthis.setStatusMessage(formatError(\"Daemon unavailable; retrying\", lastError), {\n\t\t\t\ttone: \"error\",\n\t\t\t\tsticky: true,\n\t\t\t\trender: false,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate requireSocketPath(): string {\n\t\tif (!this.options.socketPath) throw new Error(\"Session view daemon socket is not configured\");\n\t\treturn this.options.socketPath;\n\t}\n\n\tprivate requireClient(): DaemonClient {\n\t\tif (!this.client) {\n\t\t\tthrow new Error(\"Agents view daemon client is not connected\");\n\t\t}\n\t\treturn this.client;\n\t}\n\n\tprivate getAgentCountsText(): string {\n\t\tconst counts = countRowsBySection(this.rows);\n\t\treturn `${counts.running} running, ${counts.idle} idle, ${counts.inactive} inactive`;\n\t}\n\n\tprivate renderSessionRows(width: number, maxRows: number): string[] {\n\t\tif (maxRows <= 0) {\n\t\t\treturn [];\n\t\t}\n\t\tif (this.rows.length === 0) {\n\t\t\treturn [theme.bold(sectionTitle(\"running\")), theme.fg(\"dim\", \" No sessions match your search.\")].slice(\n\t\t\t\t0,\n\t\t\t\tmaxRows,\n\t\t\t);\n\t\t}\n\n\t\tconst displayItems = buildDisplayItems(this.rows);\n\t\tconst selectedIdentity = this.rows[this.selectedIndex]?.identity;\n\t\tconst selectedDisplayIndex = displayItems.findIndex(\n\t\t\t(item) => item.type === \"row\" && item.row.identity === selectedIdentity,\n\t\t);\n\t\tconst visibleRows = Math.min(maxRows, this.visibleListRows());\n\t\tconst start = Math.max(\n\t\t\t0,\n\t\t\tMath.min(displayItems.length - visibleRows, selectedDisplayIndex - Math.floor(visibleRows / 2)),\n\t\t);\n\t\tconst showLeadingEllipsis = start > 0;\n\t\tlet showTrailingEllipsis = start + visibleRows < displayItems.length;\n\t\tif ((showLeadingEllipsis ? 1 : 0) + (showTrailingEllipsis ? 1 : 0) >= visibleRows) {\n\t\t\tshowTrailingEllipsis = false;\n\t\t}\n\t\tconst contentVisibleRows = Math.max(\n\t\t\t0,\n\t\t\tvisibleRows - (showLeadingEllipsis ? 1 : 0) - (showTrailingEllipsis ? 1 : 0),\n\t\t);\n\t\tconst visibleItems = displayItems.slice(start, start + contentVisibleRows);\n\t\tconst lines = visibleItems.map((item) => {\n\t\t\tif (item.type === \"spacer\") {\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t\tif (item.type === \"heading\") {\n\t\t\t\treturn theme.bold(sectionTitle(item.section));\n\t\t\t}\n\t\t\tif (item.type === \"empty\") {\n\t\t\t\treturn theme.fg(\"dim\", \" No agents\");\n\t\t\t}\n\t\t\treturn this.renderRow(item.row, width);\n\t\t});\n\t\tif (showLeadingEllipsis) {\n\t\t\tlines.unshift(theme.fg(\"dim\", \" ...\"));\n\t\t}\n\t\tif (showTrailingEllipsis) {\n\t\t\tlines.push(theme.fg(\"dim\", \" ...\"));\n\t\t}\n\t\treturn lines;\n\t}\n\n\tprivate renderRow(row: AgentsViewRow, width: number): string {\n\t\tconst selected = row.selectable && row.identity === this.rows[this.selectedIndex]?.identity;\n\t\tif (row.kind === \"subagent-code\") {\n\t\t\treturn this.renderCodeRow(row);\n\t\t}\n\t\tif (row.kind === \"subagent-summary\") {\n\t\t\tconst indent = \" \".repeat(row.depth);\n\t\t\tconst hint = row.hasSpawnCode ? theme.fg(\"dim\", ` · ${keyText(\"app.agents.program\")} show program`) : \"\";\n\t\t\tconst label = `${theme.fg(\"dim\", `${row.expanded ? \"▾\" : \"▸\"} ${row.title}`)}${hint}`;\n\t\t\tconst line = padLine(truncateToWidth(`${indent}${label}`, width, \"\"), width);\n\t\t\treturn selected ? `${SELECTED_ROW_MARKER}${line}` : line;\n\t\t}\n\t\tconst pendingDelete = row.kind === \"agent\" && this.isPendingDeleteRow(row);\n\t\tconst pendingKill = row.kind === \"subagent\" && this.isPendingKillSubagentRow(row);\n\t\tconst rawIcon = this.getRowIcon(row.section);\n\t\tconst icon = this.formatRowIcon(row.section, rawIcon);\n\t\tconst indent = \" \".repeat(row.depth);\n\t\tconst age = formatSessionDuration(row.summary);\n\t\tconst details = row.section === \"inactive\" ? `${row.summary.messageCount} · ${age}` : age;\n\t\tconst detailsWidth = row.section === \"inactive\" ? Math.max(10, visibleWidth(details)) : 10;\n\t\tconst heartbeatBadge = !pendingDelete && !pendingKill ? formatHeartbeatBadge(row.heartbeat) : \"\";\n\t\tconst heartbeatCell = heartbeatBadge ? theme.fg(\"error\", heartbeatBadge) : \"\";\n\t\tconst heartbeatWidth = visibleWidth(heartbeatBadge);\n\t\tconst titleWidth = Math.max(\n\t\t\t0,\n\t\t\twidth -\n\t\t\t\tvisibleWidth(indent) -\n\t\t\t\tvisibleWidth(rawIcon) -\n\t\t\t\tdetailsWidth -\n\t\t\t\t2 -\n\t\t\t\t(heartbeatWidth > 0 ? heartbeatWidth + 1 : 0),\n\t\t);\n\t\tconst title = pendingDelete\n\t\t\t? this.getPendingDeleteTitle()\n\t\t\t: pendingKill\n\t\t\t\t? `${keyText(\"app.agents.delete\")} again to ${row.section === \"running\" ? \"stop\" : \"delete\"}`\n\t\t\t\t: styleRowTitle(row);\n\t\t// Keep stable model information ahead of the variable summary so narrow rows truncate the summary first.\n\t\tconst summaryText = !pendingDelete && !pendingKill ? row.summary.summary : undefined;\n\t\tconst modelLabel =\n\t\t\tisSubagentSummary(row.summary) && !pendingDelete && !pendingKill && row.summary.model\n\t\t\t\t? `${row.summary.model.provider}/${row.summary.model.id}${row.summary.thinkingLevel && row.summary.thinkingLevel !== \"off\" ? `:${row.summary.thinkingLevel}` : \"\"}`\n\t\t\t\t: undefined;\n\t\tconst suffixes = [modelLabel, summaryText].filter(\n\t\t\t(suffix): suffix is string => suffix !== undefined && suffix.length > 0,\n\t\t);\n\t\tconst titleContent = suffixes.length > 0 ? `${title} ${theme.fg(\"dim\", `· ${suffixes.join(\" · \")}`)}` : title;\n\t\tconst titleCell = formatTableCell(titleContent, titleWidth);\n\t\tconst cells = [\n\t\t\ticon,\n\t\t\tpendingDelete || pendingKill ? theme.fg(\"error\", titleCell) : titleCell,\n\t\t\tformatRightTableCell(details, detailsWidth),\n\t\t];\n\t\tconst base = `${indent}${cells[0]} ${heartbeatCell ? `${heartbeatCell} ` : \"\"}${cells[1]} ${cells[2]}`;\n\t\tconst line = padLine(truncateToWidth(base, width, \"\"), width);\n\t\treturn selected ? `${SELECTED_ROW_MARKER}${line}` : line;\n\t}\n\n\t// Spawn-code rows are read-only context. They render deemphasized — muted\n\t// text on a panel background (applied in finalizeRenderedLine) so the program\n\t// reads as one quiet segmented block rather than competing with agent rows.\n\tprivate renderCodeRow(row: AgentsViewRow): string {\n\t\tconst indent = \" \".repeat(row.depth);\n\t\tconst body = theme.fg(\"muted\", row.code || \" \");\n\t\treturn `${CODE_ROW_MARKER}${indent} ${body}`;\n\t}\n\n\tprivate finalizeRenderedLine(line: string, width: number): string {\n\t\tconst code = line.startsWith(CODE_ROW_MARKER);\n\t\tconst selected = !code && line.startsWith(SELECTED_ROW_MARKER);\n\t\tlet content = code\n\t\t\t? line.slice(CODE_ROW_MARKER.length)\n\t\t\t: selected\n\t\t\t\t? line.slice(SELECTED_ROW_MARKER.length)\n\t\t\t\t: line;\n\t\t// Each rendered line must occupy exactly one terminal row; a stray\n\t\t// newline would shift every line below it and overlap the editor.\n\t\tif (content.includes(\"\\n\") || content.includes(\"\\r\")) {\n\t\t\tcontent = content.replace(/[\\r\\n]+/g, \" \");\n\t\t}\n\t\tconst padded = padLine(truncateToWidth(content, width), width);\n\t\tif (code) {\n\t\t\treturn theme.bg(\"toolPanelBg\", padded);\n\t\t}\n\t\tif (!selected) {\n\t\t\treturn padded;\n\t\t}\n\t\t// Truncating styled cells embeds full \\x1b[0m resets; re-open the\n\t\t// selection background after each so the highlight spans the whole row.\n\t\tconst applySelectionBg = theme.getSelectionBackgroundColor();\n\t\treturn padded.split(\"\\x1b[0m\").map(applySelectionBg).join(\"\\x1b[0m\");\n\t}\n\n\tprivate isPendingDeleteRow(row: AgentsViewRow): boolean {\n\t\treturn (\n\t\t\tgetSummaryIdentity(row.summary) === this.pendingDeleteAgent?.identity && this.isDeleteConfirmationVisible()\n\t\t);\n\t}\n\n\tprivate isPendingKillSubagentRow(row: AgentsViewRow): boolean {\n\t\treturn (\n\t\t\tgetSummaryIdentity(row.summary) === this.pendingKillSubagent?.identity && this.isDeleteConfirmationVisible()\n\t\t);\n\t}\n\n\tprivate getPendingDeleteTitle(): string {\n\t\tconst deleteKey = keyText(\"app.agents.delete\");\n\t\treturn this.pendingDeleteAgent?.stopped\n\t\t\t? `stopped - ${deleteKey} again to remove`\n\t\t\t: `${deleteKey} again to remove`;\n\t}\n\n\tprivate renderPrompt(width: number): string[] {\n\t\treturn this.editor.render(width);\n\t}\n\n\tprivate renderDock(width: number): string[] {\n\t\tconst safeWidth = Math.max(1, width);\n\t\treturn [this.renderHints(safeWidth)].map((line) => this.finalizeRenderedLine(line, safeWidth));\n\t}\n\n\tprivate renderHints(width: number): string {\n\t\tif (this.isCtrlCExitHintVisible()) {\n\t\t\tconst clearKey = keyText(\"app.clear\");\n\t\t\tconst hint = clearKey ? `Press ${clearKey} again to exit` : \"Press again to exit\";\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", hint), width);\n\t\t}\n\t\tif (this.statusMessage) {\n\t\t\treturn truncateToWidth(theme.fg(this.statusMessageTone, this.statusMessage), width);\n\t\t}\n\t\tif (this.renameTarget) {\n\t\t\tconst hint = `${keyText(\"tui.select.confirm\")} save ${keyText(\"tui.select.cancel\")} cancel`;\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", hint), width);\n\t\t}\n\t\tif (this.replyTarget) {\n\t\t\treturn truncateToWidth(theme.fg(\"muted\", this.renderReplyComposerHints()), width);\n\t\t}\n\t\t// Replying is reserved for top-level agents; subagents can be stopped or deleted.\n\t\tconst selectedRow = this.rows[this.selectedIndex];\n\t\tconst selectedAgent = selectedRow?.kind === \"agent\";\n\t\tconst selectedSubagent = selectedRow?.kind === \"subagent\";\n\t\tconst selectedSummary = selectedRow?.kind === \"subagent-summary\";\n\t\tconst hints = [\n\t\t\t`${keyText(\"tui.select.up\")}/${keyText(\"tui.select.down\")} move`,\n\t\t\tselectedSummary\n\t\t\t\t? `${keyText(\"tui.select.confirm\")} ${selectedRow?.expanded ? \"collapse\" : \"expand\"}`\n\t\t\t\t: `${keyText(\"tui.select.confirm\")} open`,\n\t\t\tselectedSummary ? undefined : `${keyText(\"app.agents.open\")} open`,\n\t\t\tselectedAgent\n\t\t\t\t? `${keyText(\"app.agents.reply\")} ${selectedRow?.section === \"inactive\" ? \"resume\" : \"reply\"}`\n\t\t\t\t: undefined,\n\t\t\t`${keyText(\"app.agents.new\")} new`,\n\t\t\tselectedAgent ? `${keyText(\"app.agents.rename\")} rename` : undefined,\n\t\t\tselectedAgent\n\t\t\t\t? `${keyText(\"app.agents.delete\")} ${selectedRow?.section === \"inactive\" ? \"delete\" : \"stop/deactivate\"}`\n\t\t\t\t: undefined,\n\t\t\tselectedSubagent\n\t\t\t\t? `${keyText(\"app.agents.delete\")} ${selectedRow.section === \"running\" ? \"stop\" : \"delete\"}`\n\t\t\t\t: undefined,\n\t\t\tthis.selectedRowCanShowProgram() ? `${keyText(\"app.agents.program\")} program` : undefined,\n\t\t]\n\t\t\t.filter((hint): hint is string => hint !== undefined)\n\t\t\t.join(\" \");\n\t\treturn truncateToWidth(theme.fg(\"muted\", hints), width);\n\t}\n\n\tprivate renderReplyComposerHints(): string {\n\t\tconst target = this.replyTarget!;\n\t\tconst current = resolveCurrentReplyTargetSummary(this.unifiedRecords ?? [], target, (activeSessionId) =>\n\t\t\tthis.findSummaryByActiveSessionId(activeSessionId),\n\t\t);\n\t\tconst streaming = current.activeSessionId !== undefined && current.isStreaming;\n\t\tconst hasText = this.editor.getText().trim().length > 0;\n\t\treturn [\n\t\t\t`${keyText(\"tui.select.confirm\")} ${streaming ? \"steer\" : current.activeSessionId ? \"send\" : \"resume & send\"}`,\n\t\t\thasText ? `${keyText(\"app.message.followUp\")} queue` : undefined,\n\t\t\t`${keyText(\"tui.select.cancel\")} cancel`,\n\t\t]\n\t\t\t.filter((hint): hint is string => hint !== undefined)\n\t\t\t.join(\" \");\n\t}\n\n\tprivate visibleListRows(): number {\n\t\treturn Math.max(4, this.ui.terminal.rows - 9);\n\t}\n\n\tprivate contentHeight(width: number): number {\n\t\tconst rows = this.ui.terminal.rows;\n\t\tconst dockHeight = clippedFullscreenDockHeight(this.renderDock(width).length, rows);\n\t\treturn Math.max(0, rows - dockHeight);\n\t}\n\n\tprivate getSplashModelId(): string | undefined {\n\t\treturn this.rows[this.selectedIndex]?.summary.model?.id ?? this.options.startupModelId;\n\t}\n\n\tprivate getSplashCwd(): string {\n\t\treturn this.rows[this.selectedIndex]?.summary.cwd ?? this.options.uiServices.getInitialCwd();\n\t}\n\n\tprivate getRowIcon(section: AgentsViewSection): string {\n\t\tswitch (section) {\n\t\t\tcase \"running\":\n\t\t\t\treturn workingIconFrame(this.workingIconFrame);\n\t\t\tcase \"idle\":\n\t\t\t\treturn NEEDS_INPUT_ROW_ICON;\n\t\t\tcase \"inactive\":\n\t\t\t\treturn COMPLETED_ROW_ICON;\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = section;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate formatRowIcon(section: AgentsViewSection, icon: string): string {\n\t\tswitch (section) {\n\t\t\tcase \"running\":\n\t\t\t\treturn theme.bold(icon);\n\t\t\tcase \"idle\":\n\t\t\t\treturn theme.fg(\"warning\", icon);\n\t\t\tcase \"inactive\":\n\t\t\t\treturn theme.fg(\"dim\", icon);\n\t\t\tdefault: {\n\t\t\t\tconst _exhaustive: never = section;\n\t\t\t\treturn _exhaustive;\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype DisplayItem =\n\t| { type: \"spacer\" }\n\t| { type: \"heading\"; section: AgentsViewSection }\n\t| { type: \"empty\"; section: AgentsViewSection }\n\t| { type: \"row\"; row: AgentsViewRow };\n\nfunction buildDisplayItems(rows: readonly AgentsViewRow[]): DisplayItem[] {\n\tconst items: DisplayItem[] = [];\n\tconst sections: AgentsViewSection[] = [\"running\", \"idle\", \"inactive\"];\n\tfor (const [index, section] of sections.entries()) {\n\t\tif (index > 0) {\n\t\t\titems.push({ type: \"spacer\" });\n\t\t}\n\t\titems.push({ type: \"heading\", section });\n\t\tconst sectionRows = getDisplayRowsForSection(rows, section);\n\t\tif (sectionRows.length === 0) {\n\t\t\titems.push({ type: \"empty\", section });\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const row of sectionRows) {\n\t\t\titems.push({ type: \"row\", row });\n\t\t}\n\t}\n\treturn items;\n}\n\n// Nested rows (subagent summaries and expanded subagents) always render in\n// their top-level agent's section block, regardless of their own section.\nfunction getDisplayRowsForSection(rows: readonly AgentsViewRow[], section: AgentsViewSection): AgentsViewRow[] {\n\tconst result: AgentsViewRow[] = [];\n\tlet include = false;\n\tfor (const row of rows) {\n\t\tif (row.depth === 0) {\n\t\t\tinclude = row.section === section;\n\t\t}\n\t\tif (include) {\n\t\t\tresult.push(row);\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction countRowsBySection(rows: readonly AgentsViewRow[]): Record<AgentsViewSection, number> {\n\tconst agents = rows.filter((row) => row.kind === \"agent\");\n\treturn {\n\t\trunning: agents.filter((row) => row.section === \"running\").length,\n\t\tidle: agents.filter((row) => row.section === \"idle\").length,\n\t\tinactive: agents.filter((row) => row.section === \"inactive\").length,\n\t};\n}\n\nfunction getSelectedRowIdentity(row: AgentsViewRow | undefined): string | undefined {\n\treturn row?.identity;\n}\n\nfunction rowHasSpawnCode(row: AgentsViewRow): boolean {\n\tconst code = row.summary.spawnCode;\n\treturn typeof code === \"string\" && code.trim().length > 0;\n}\n\nfunction isRunningSessionSummary(summary: SessionSummary): boolean {\n\treturn summary.activity === \"working\";\n}\n\n// Explicit session names read bold so they stand out from fallback titles\n// (first prompt, cwd, ids); the \"(no messages)\" placeholder reads italic.\nfunction styleRowTitle(row: AgentsViewRow): string {\n\tif (row.summary.sessionName?.replace(/\\s+/g, \" \").trim()) {\n\t\treturn theme.bold(row.title);\n\t}\n\tif (row.title === \"(no messages)\") {\n\t\treturn theme.italic(row.title);\n\t}\n\treturn row.title;\n}\n\nfunction formatTableCell(value: string, width: number): string {\n\tconst truncated = truncateToWidth(value, width, \"\");\n\treturn truncated + \" \".repeat(Math.max(0, width - visibleWidth(truncated)));\n}\n\nfunction formatRightTableCell(value: string, width: number): string {\n\tconst truncated = truncateToWidth(value, width, \"\");\n\treturn \" \".repeat(Math.max(0, width - visibleWidth(truncated))) + truncated;\n}\n\nfunction formatSessionDuration(summary: SessionSummary): string {\n\treturn formatAgentsViewRelativeTime(\n\t\tsummary.activeSessionId ? (summary.created ?? summary.modified) : (summary.modified ?? summary.created),\n\t);\n}\n\nexport function formatAgentsViewRelativeTime(value: string | undefined, now: number = Date.now()): string {\n\tconst timestamp = parseSessionTimestamp(value);\n\tif (!timestamp) {\n\t\treturn \"\";\n\t}\n\tconst seconds = Math.max(0, Math.floor((now - timestamp) / 1000));\n\tif (seconds < 60) {\n\t\treturn `${seconds}s`;\n\t}\n\tconst minutes = Math.floor(seconds / 60);\n\tif (minutes < 60) {\n\t\treturn `${minutes}m`;\n\t}\n\tconst hours = Math.floor(minutes / 60);\n\tif (hours < 24) {\n\t\treturn `${hours}h`;\n\t}\n\tconst days = Math.floor(hours / 24);\n\treturn `${days}d`;\n}\n\nfunction parseSessionTimestamp(value: string | undefined): number | undefined {\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\tconst timestamp = Date.parse(value);\n\treturn Number.isNaN(timestamp) ? undefined : timestamp;\n}\n\nfunction requireDaemonData(response: DaemonResponse): unknown {\n\tif (!response.success) {\n\t\tthrow new Error(response.error);\n\t}\n\treturn response.data;\n}\n\nfunction expectSessionList(value: unknown): SessionSummary[] {\n\tif (!isRecord(value) || !Array.isArray(value.sessions)) {\n\t\tthrow new Error(\"Daemon returned an invalid session list response\");\n\t}\n\tif (!value.sessions.every(isSessionSummary)) {\n\t\tthrow new Error(\"Daemon returned an invalid session summary\");\n\t}\n\treturn value.sessions;\n}\n\nfunction expectSessionSummary(value: unknown): SessionSummary {\n\tif (!isSessionSummary(value)) {\n\t\tthrow new Error(\"Daemon returned an invalid session summary\");\n\t}\n\treturn value;\n}\n\nfunction isSessionSummary(value: unknown): value is SessionSummary {\n\treturn isRecord(value) && typeof value.id === \"string\" && typeof value.sessionId === \"string\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction formatError(prefix: string, error: unknown): string {\n\tconst message = error instanceof Error ? error.message : String(error);\n\treturn formatAgentsViewStatusLine(`${prefix}: ${message}`);\n}\n\n// The agents view shows open failures as a one-line status only, so a client-side\n// crash (e.g. \"Maximum call stack size exceeded\") leaves no stack to debug from.\n// Persist the full stack to a file — the TUI owns stdout/stderr, so a log file is\n// the only safe sink.\nfunction logClientError(prefix: string, error: unknown): void {\n\tconst detail = error instanceof Error ? (error.stack ?? error.message) : String(error);\n\tappendRotatingLog(getClientErrorLogPath(), `[${new Date().toISOString()}] ${prefix}: ${detail}`);\n}\n\nfunction padLine(line: string, width: number): string {\n\treturn line + \" \".repeat(Math.max(0, width - visibleWidth(line)));\n}\n"]}