@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.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 (277) hide show
  1. package/README.md +171 -124
  2. package/bin/dsh-tui.js +748 -22
  3. package/cordis.patch.yml +38 -10
  4. package/cordis.yml +2 -0
  5. package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
  6. package/lib/types/adapter/channel/session-projection.js +2 -0
  7. package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
  8. package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
  9. package/lib/types/adapter/channel/ui-policy.js +5 -1
  10. package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
  11. package/lib/types/adapter/kernel/host-facade.js +1 -0
  12. package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
  13. package/lib/types/adapter/kernel/replay.js +3 -0
  14. package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
  15. package/lib/types/adapter/kernel/runtime.js +2 -0
  16. package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
  17. package/lib/types/adapter/kernel/slices/workspace.js +1 -0
  18. package/lib/types/adapter/ports/channel-ui.d.ts +22 -2
  19. package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
  20. package/lib/types/adapter/ports/channel-view.d.ts +66 -0
  21. package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
  22. package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
  23. package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
  24. package/lib/types/adapter/ports/workspace.d.ts +2 -0
  25. package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
  26. package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
  27. package/lib/types/adapter/upstream/workspace-driver.js +10 -2
  28. package/lib/types/commands.d.ts.map +1 -1
  29. package/lib/types/commands.js +1 -0
  30. package/lib/types/components/ContextBarView.d.ts +15 -7
  31. package/lib/types/components/ContextBarView.d.ts.map +1 -1
  32. package/lib/types/components/ContextBarView.js +26 -22
  33. package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
  34. package/lib/types/components/ImagePreviewOverlay.js +11 -5
  35. package/lib/types/components/Markdown.d.ts +11 -1
  36. package/lib/types/components/Markdown.d.ts.map +1 -1
  37. package/lib/types/components/Markdown.js +20 -2
  38. package/lib/types/components/MermaidDiagram.d.ts +13 -0
  39. package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
  40. package/lib/types/components/MermaidDiagram.js +61 -0
  41. package/lib/types/components/MessageList.d.ts +26 -1
  42. package/lib/types/components/MessageList.d.ts.map +1 -1
  43. package/lib/types/components/MessageList.js +72 -7
  44. package/lib/types/components/PageMargin.d.ts +1 -1
  45. package/lib/types/components/PromptInput.d.ts +37 -1
  46. package/lib/types/components/PromptInput.d.ts.map +1 -1
  47. package/lib/types/components/PromptInput.js +243 -28
  48. package/lib/types/components/ScrollbarGutter.d.ts +20 -5
  49. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
  50. package/lib/types/components/ScrollbarGutter.js +88 -10
  51. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  52. package/lib/types/components/StreamingMarkdown.js +30 -28
  53. package/lib/types/components/Tooltip.d.ts.map +1 -1
  54. package/lib/types/components/Tooltip.js +45 -7
  55. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  56. package/lib/types/components/messages/UserPromptMessage.js +5 -7
  57. package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
  58. package/lib/types/components/messages/useImageInspection.js +19 -9
  59. package/lib/types/components/promptDraftCache.d.ts +102 -0
  60. package/lib/types/components/promptDraftCache.d.ts.map +1 -0
  61. package/lib/types/components/promptDraftCache.js +76 -0
  62. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
  63. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  64. package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
  65. package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
  66. package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
  67. package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
  68. package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
  69. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  70. package/lib/types/components/sessions/SessionListRow.js +52 -5
  71. package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
  72. package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
  73. package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
  74. package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
  75. package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
  76. package/lib/types/dsh-adapter/bundled-presets.js +80 -0
  77. package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
  78. package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
  79. package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
  80. package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
  81. package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
  82. package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
  83. package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
  84. package/lib/types/dsh-adapter/channel/background-action.js +52 -29
  85. package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
  86. package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
  87. package/lib/types/dsh-adapter/channel/binding.js +78 -18
  88. package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
  89. package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
  90. package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
  91. package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
  92. package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
  93. package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
  94. package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
  95. package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
  96. package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
  97. package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
  98. package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
  99. package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
  100. package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
  101. package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
  102. package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
  103. package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
  104. package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
  105. package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
  106. package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
  107. package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
  108. package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
  109. package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
  110. package/lib/types/dsh-adapter/channel/projection.js +29 -19
  111. package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
  112. package/lib/types/dsh-adapter/channel/reports.js +4 -12
  113. package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
  114. package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
  115. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +3 -0
  116. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
  117. package/lib/types/dsh-adapter/channel/session-live-adoption.js +1 -0
  118. package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
  119. package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
  120. package/lib/types/dsh-adapter/channel/session-metadata.js +2 -2
  121. package/lib/types/dsh-adapter/channel/session-resume.d.ts +11 -2
  122. package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
  123. package/lib/types/dsh-adapter/channel/session-resume.js +291 -94
  124. package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
  125. package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
  126. package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
  127. package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
  128. package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
  129. package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
  130. package/lib/types/dsh-adapter/channel/state.d.ts +1 -1
  131. package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
  132. package/lib/types/dsh-adapter/channel/state.js +1 -1
  133. package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
  134. package/lib/types/dsh-adapter/channel/transcript.js +6 -12
  135. package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
  136. package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
  137. package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
  138. package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
  139. package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
  140. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  141. package/lib/types/dsh-adapter/channel.js +67 -6
  142. package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
  143. package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
  144. package/lib/types/dsh-adapter/compat/messages.js +15 -0
  145. package/lib/types/dsh-adapter/compat/settings.d.ts +33 -0
  146. package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
  147. package/lib/types/dsh-adapter/compat/settings.js +43 -0
  148. package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
  149. package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
  150. package/lib/types/dsh-adapter/compat/shell.js +11 -0
  151. package/lib/types/dsh-adapter/contract.d.ts +4 -4
  152. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  153. package/lib/types/dsh-adapter/contract.js +15 -6
  154. package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
  155. package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
  156. package/lib/types/dsh-adapter/ide-channel.js +556 -0
  157. package/lib/types/dsh-adapter/index.d.ts +18 -3
  158. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  159. package/lib/types/dsh-adapter/index.js +32 -5
  160. package/lib/types/dsh-adapter/plugin.d.ts +5 -2
  161. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  162. package/lib/types/dsh-adapter/plugin.js +173 -42
  163. package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
  164. package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
  165. package/lib/types/dsh-adapter/preset-resolution.js +10 -0
  166. package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
  167. package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
  168. package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
  169. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
  170. package/lib/types/dsh-adapter/sessionTree.js +3 -2
  171. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  172. package/lib/types/dsh-adapter/subagents.js +6 -5
  173. package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
  174. package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
  175. package/lib/types/dsh-adapter/workspaces.js +116 -0
  176. package/lib/types/effortPrefs.d.ts +15 -3
  177. package/lib/types/effortPrefs.d.ts.map +1 -1
  178. package/lib/types/effortPrefs.js +37 -3
  179. package/lib/types/homePrefs.d.ts +15 -0
  180. package/lib/types/homePrefs.d.ts.map +1 -0
  181. package/lib/types/homePrefs.js +53 -0
  182. package/lib/types/i18n.d.ts +257 -296
  183. package/lib/types/i18n.d.ts.map +1 -1
  184. package/lib/types/i18n.js +78 -84
  185. package/lib/types/ink/colorize.d.ts +10 -0
  186. package/lib/types/ink/colorize.d.ts.map +1 -1
  187. package/lib/types/ink/colorize.js +21 -0
  188. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  189. package/lib/types/ink/components/ScrollBox.js +12 -1
  190. package/lib/types/ink/dom.d.ts +1 -0
  191. package/lib/types/ink/dom.d.ts.map +1 -1
  192. package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
  193. package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
  194. package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
  195. package/lib/types/ink/ink.d.ts.map +1 -1
  196. package/lib/types/ink/ink.js +36 -1
  197. package/lib/types/ink/output.d.ts +10 -1
  198. package/lib/types/ink/output.d.ts.map +1 -1
  199. package/lib/types/ink/output.js +109 -17
  200. package/lib/types/ink/render-border.d.ts +6 -1
  201. package/lib/types/ink/render-border.d.ts.map +1 -1
  202. package/lib/types/ink/render-border.js +25 -9
  203. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  204. package/lib/types/ink/render-node-to-output.js +48 -6
  205. package/lib/types/ink/renderer.d.ts +6 -0
  206. package/lib/types/ink/renderer.d.ts.map +1 -1
  207. package/lib/types/ink/renderer.js +2 -1
  208. package/lib/types/ink/selection.d.ts +60 -0
  209. package/lib/types/ink/selection.d.ts.map +1 -1
  210. package/lib/types/ink/selection.js +129 -0
  211. package/lib/types/ink/sixel-graphics.d.ts +19 -1
  212. package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
  213. package/lib/types/ink/sixel-graphics.js +124 -12
  214. package/lib/types/ink/terminal-image.d.ts +18 -1
  215. package/lib/types/ink/terminal-image.d.ts.map +1 -1
  216. package/lib/types/ink/terminal.d.ts +17 -0
  217. package/lib/types/ink/terminal.d.ts.map +1 -1
  218. package/lib/types/ink/terminal.js +20 -1
  219. package/lib/types/screens/Chat.d.ts +17 -1
  220. package/lib/types/screens/Chat.d.ts.map +1 -1
  221. package/lib/types/screens/Chat.js +284 -72
  222. package/lib/types/screens/SessionSupervisor.d.ts +84 -0
  223. package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
  224. package/lib/types/screens/SessionSupervisor.js +350 -0
  225. package/lib/types/screens/SessionTree.js +2 -2
  226. package/lib/types/screens/Settings.d.ts.map +1 -1
  227. package/lib/types/screens/Settings.js +32 -9
  228. package/lib/types/screens/StatusLine.d.ts +13 -0
  229. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  230. package/lib/types/screens/StatusLine.js +36 -17
  231. package/lib/types/screens/StatusMetrics.d.ts +79 -22
  232. package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
  233. package/lib/types/screens/StatusMetrics.js +153 -88
  234. package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
  235. package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
  236. package/lib/types/screens/sessionSupervisor/model.js +69 -0
  237. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
  238. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
  239. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
  240. package/lib/types/sessionMounts.d.ts +272 -0
  241. package/lib/types/sessionMounts.d.ts.map +1 -0
  242. package/lib/types/sessionMounts.js +706 -0
  243. package/lib/types/sessions/format.d.ts.map +1 -1
  244. package/lib/types/sessions/format.js +4 -1
  245. package/lib/types/sessions/resumeFailure.d.ts +38 -0
  246. package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
  247. package/lib/types/sessions/resumeFailure.js +58 -0
  248. package/lib/types/terminal-utils/mermaid.d.ts +44 -0
  249. package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
  250. package/lib/types/terminal-utils/mermaid.js +83 -0
  251. package/lib/types/tips.d.ts.map +1 -1
  252. package/lib/types/tips.js +175 -19
  253. package/lib/types/tuiDisplayPrefs.d.ts +7 -9
  254. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  255. package/lib/types/tuiDisplayPrefs.js +31 -28
  256. package/lib/types/update.d.ts +61 -0
  257. package/lib/types/update.d.ts.map +1 -1
  258. package/lib/types/update.js +194 -0
  259. package/lib/types/utils/imageResize.d.ts +90 -0
  260. package/lib/types/utils/imageResize.d.ts.map +1 -0
  261. package/lib/types/utils/imageResize.js +272 -0
  262. package/lib/types/utils/keymap.d.ts +5 -1
  263. package/lib/types/utils/keymap.d.ts.map +1 -1
  264. package/lib/types/utils/keymap.js +23 -0
  265. package/lib/types/workspaces.d.ts +2 -1
  266. package/lib/types/workspaces.d.ts.map +1 -1
  267. package/lib/types/workspaces.js +1 -1
  268. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +10 -10
  269. package/package.json +120 -103
  270. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  271. package/presets/liangshen/agent.cordis.yml +15 -0
  272. package/lib/types/screens/AgentView.d.ts +0 -23
  273. package/lib/types/screens/AgentView.d.ts.map +0 -1
  274. package/lib/types/screens/AgentView.js +0 -546
  275. package/lib/types/screens/SessionBrowser.d.ts +0 -42
  276. package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
  277. package/lib/types/screens/SessionBrowser.js +0 -911
@@ -63,6 +63,16 @@ export function isProgressReportingAvailable() {
63
63
  export function isJetBrainsIdeTerminal() {
64
64
  return process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
65
65
  }
66
+ /**
67
+ * Detects zellij, which multiplexes the outer terminal instead of emulating
68
+ * it. Capability probes therefore describe the *outer* terminal — zellij even
69
+ * forwards KITTY_WINDOW_ID into the pane, so kitty detection matches — while
70
+ * zellij's own grid decides how control sequences actually apply.
71
+ * @returns true when the process runs inside a zellij pane.
72
+ */
73
+ export function isZellij() {
74
+ return !!process.env.ZELLIJ;
75
+ }
66
76
  /**
67
77
  * Checks if the terminal supports DEC mode 2026 (synchronized output).
68
78
  * When supported, BSU/ESU sequences prevent visible flicker during redraws.
@@ -272,10 +282,19 @@ export const SYNC_OUTPUT_SUPPORTED = isSynchronizedOutputSupported();
272
282
  * as content scrolls (the "JetBrains terminal slowly garbles" bug class).
273
283
  * The diff engine falls back to repainting the shifted rows cell-by-cell,
274
284
  * which every terminal renders identically. Disable DECSTBM on JetBrains terminals.
285
+ *
286
+ * zellij is excluded for the same class of reason. Its `CSI T`
287
+ * (grid.rs rotate_scroll_region_up) shifts rows only while the cursor sits
288
+ * inside the scroll region, and its `CSI r` does not home the cursor outside
289
+ * origin mode — but the renderer parks the cursor on the last screen row,
290
+ * below every ScrollBox. The shift is silently dropped while the diff assumes
291
+ * it happened, so stale rows survive every scroll-up frame (tearing, leftover
292
+ * transcript lines). Only DECSTBM is withdrawn: zellij does implement DEC
293
+ * 2026, so BSU/ESU stay on and frames remain atomic.
275
294
  * @returns true when DECSTBM scroll optimization is safe on this terminal.
276
295
  */
277
296
  export function isDecstbmSafe() {
278
- return SYNC_OUTPUT_SUPPORTED && !isJetBrainsIdeTerminal();
297
+ return SYNC_OUTPUT_SUPPORTED && !isJetBrainsIdeTerminal() && !isZellij();
279
298
  }
280
299
  /**
281
300
  * Render forensics: when DSH_TUI_RENDER_LOG names a file path, every painted
@@ -6,8 +6,9 @@ import { TuiStatusStore } from '../dsh-adapter/status.js';
6
6
  import type { TuiShortcutHost } from '../dsh-adapter/shortcuts.js';
7
7
  import type { TuiThemeHost } from '../dsh-adapter/themes.js';
8
8
  import { ApprovalStore } from '../dsh-adapter/approvals.js';
9
+ import { type PromptController } from '../components/PromptInput.js';
9
10
  import type { InjectController } from '../dsh-adapter/inject-channel.js';
10
- export declare function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen, trajectorySeen: trajectorySeenProp, injectControllerRef, renderScene, }: {
11
+ export declare function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, }: {
11
12
  channel: Channel;
12
13
  renderScene?: (id: string, channel: Channel) => React.ReactNode;
13
14
  questionStore: QuestionStore;
@@ -59,5 +60,20 @@ export declare function Chat({ channel, questionStore, approvalStore, extensionD
59
60
  * open the channel (headless scripts, bare embeds).
60
61
  */
61
62
  injectControllerRef?: React.RefObject<InjectController | null>;
63
+ /**
64
+ * Show the workspace home screen as this session's first frame.
65
+ *
66
+ * A prop rather than a filesystem read inside the component: the host knows
67
+ * whether this launch was an ordinary one (no `--resume`, no workspace
68
+ * target) and whether the home screen has already been shown on this
69
+ * installation, and tests need it deterministic.
70
+ */
71
+ openHomeOnBoot?: boolean;
72
+ /**
73
+ * The composer's live controller, published every render. Exposed as a prop
74
+ * so a regression can read the draft the composer HOLDS — the ownership
75
+ * question (does a screen swap lose it?) is about state, not about pixels.
76
+ */
77
+ promptControllerRef?: React.RefObject<PromptController | null>;
62
78
  }): React.JSX.Element | null;
63
79
  //# sourceMappingURL=Chat.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAE/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAmB3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AA+MxE,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,UAAkB,EAClB,cAAc,EAAE,kBAAkB,EAClC,mBAAmB,EACnB,WAAW,GACZ,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAC/D,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,yDAAyD;IACzD,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,uEAAuE;IACvE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CAC/D,4BAy+HA"}
1
+ {"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAE/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAkB3D,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AAgNxE,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,UAAkB,EAClB,cAAc,EAAE,kBAAkB,EAClC,mBAAmB,EACnB,mBAAmB,EAAE,uBAAuB,EAC5C,WAAW,EACX,cAAc,GACf,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAC/D,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,yDAAyD;IACzD,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,uEAAuE;IACvE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAC9D;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CAC/D,4BA2rIA"}
@@ -18,7 +18,6 @@ import { homeDir } from '../utils/paths.js';
18
18
  import { cleanRenderText, cleanScalarText } from '../dsh-adapter/sanitize.js';
19
19
  import { deriveModelGroups, modelPickerLanding, recentCatalogModels, RECENTS_GROUP_PROVIDER, } from '../modelGroups.js';
20
20
  import { readModelRecents, recordModelUse } from '../modelRecents.js';
21
- import { sessionCwdMatches } from '../dsh-adapter/channel.js';
22
21
  import { TuiDialogStore } from '../dsh-adapter/dialogs.js';
23
22
  import { TuiStatusStore } from '../dsh-adapter/status.js';
24
23
  import { runProviderWizard } from '../dsh-adapter/providerWizard.js';
@@ -52,7 +51,7 @@ import { PluginSceneBoundary } from '../components/PluginSceneBoundary.js';
52
51
  import { PluginStatusViewBoundary } from '../components/PluginStatusViewBoundary.js';
53
52
  import { ImagePreviewOverlay } from '../components/ImagePreviewOverlay.js';
54
53
  import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js';
55
- import { SessionBrowser } from './SessionBrowser.js';
54
+ import { SessionSupervisor } from './SessionSupervisor.js';
56
55
  import { SessionTree } from './SessionTree.js';
57
56
  import { Settings } from './Settings.js';
58
57
  import { WorkspacePicker } from '../components/WorkspacePicker.js';
@@ -89,7 +88,8 @@ import instances from '../ink/instances.js';
89
88
  import { useAnimationFrame } from '../ink/hooks/use-animation-frame.js';
90
89
  import { useExternalVersion } from '../hooks/useExternalVersion.js';
91
90
  import { TrajectoryScene } from './TrajectoryScene.js';
92
- import { AgentView } from './AgentView.js';
91
+ import { resumeFailureText } from '../sessions/resumeFailure.js';
92
+ import { markHomeSeen } from '../homePrefs.js';
93
93
  import { extendTrajectory, projectWave } from '../dsh-adapter/trajectory/index.js';
94
94
  import { miniWakeWidth } from '../components/trajectory/MiniWake.js';
95
95
  import { readTrajectorySeen, writeTrajectorySeen } from '../trajectoryPrefs.js';
@@ -204,7 +204,7 @@ let fallbackStatusStore;
204
204
  function peekKey(image, title) {
205
205
  return `${title ?? ''} ${image.id}`;
206
206
  }
207
- export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, renderScene, }) {
207
+ export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, }) {
208
208
  const writeRaw = React.useContext(TerminalWriteContext);
209
209
  // Re-render whenever the channel mutates; rows/status are read fresh below.
210
210
  // DEFAULT lane on purpose (useExternalVersion): the channel version bumps
@@ -358,18 +358,33 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
358
358
  };
359
359
  /** `/skills` 技能目录(issue #204):null = 注册表快照在途。 */
360
360
  const [skillsList, setSkillsList] = React.useState(null);
361
- /** `/resume` opens the session browser, a screen rather than a panel. It
362
- * owns its own selection, filters and keyboard — Chat only opens it. */
363
- const [browserOpen, setBrowserOpen] = React.useState(false);
361
+ /**
362
+ * The session supervisor — the ONE screen behind `/resume`, `/agentview`,
363
+ * `/home`, `/bg` and the composer's 🏠 button.
364
+ *
365
+ * Those were three screens over one domain (a workspace rail here, a
366
+ * search surface there, a live-status overview somewhere else), which is why
367
+ * each new session feature needed patching into all three and why the
368
+ * three disagreed about what switching a session even does. There is now a
369
+ * single surface and a single runtime behind every entry point: this
370
+ * terminal hosts several sessions, leaving one parks it rather than ending
371
+ * it, and a session another terminal holds is visible but not enterable.
372
+ *
373
+ * Seeded from the host's one-shot landing decision (`openHomeOnBoot`): the
374
+ * first ordinary launch of an installation lands here instead of on a blank
375
+ * conversation, because that is the launch where "which project am I working
376
+ * on" has not been answered yet. Every later launch starts on the chat
377
+ * screen, and the screen stays reachable.
378
+ */
379
+ const [supervisorOpen, setSupervisorOpen] = React.useState(openHomeOnBoot === true);
364
380
  /** `/tree` opens the session family tree (pi's Session Tree): every rewind
365
381
  * fork stitched back onto the message it diverged from, hover previews,
366
- * and per-node rewind/fork/adopt actions. Like the browser, a screen. */
382
+ * and per-node rewind/fork/adopt actions. Like the supervisor, a screen. */
367
383
  const [treeOpen, setTreeOpen] = React.useState(false);
368
- /** `/agentview` and `/bg` open the agent view — a screen like the browser:
369
- * it owns selection, the dispatch input and every key while up. */
370
- const [agentViewOpen, setAgentViewOpen] = React.useState(false);
371
- /** The session backgrounded when the view opened via ←/`/bg` (the "Esc
372
- * returns to that conversation" return target), cleared on close. */
384
+ /**
385
+ * The session backgrounded when the screen opened via ←/`/bg` (the "Esc
386
+ * returns to that conversation" return target), cleared on close.
387
+ */
373
388
  const [agentViewReturnId, setAgentViewReturnId] = React.useState(undefined);
374
389
  /** Live agent-view rows: the prompt footer's "← N agents" hint reads the
375
390
  * needs-input count from here (cached snapshot in the channel). The
@@ -378,16 +393,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
378
393
  const EMPTY_AGENT_VIEW_ROWS = [];
379
394
  const agentViewRows = React.useSyncExternalStore(listener => channel.subscribeAgentView?.(listener) ?? (() => { }), () => channel.agentViewRows?.() ?? EMPTY_AGENT_VIEW_ROWS);
380
395
  const backgroundAgentsNeedingInput = agentViewRows.filter(row => row.status === 'needs-input' && !row.current).length;
381
- /** Background the attached session and open the agent view
396
+ /** Background the attached session and open the supervisor
382
397
  * (`/bg`, `/background`, and ← on an empty prompt all land here). The
383
- * backgrounded session becomes the view's return target (final Esc
398
+ * backgrounded session becomes the screen's return target (final Esc
384
399
  * attaches back to it). */
385
400
  const backgroundToAgentView = React.useCallback(() => {
386
401
  void channel.backgroundCurrent().then((result) => {
387
402
  if (result.ok) {
388
403
  setAgentViewReturnId(result.backgroundedSessionId);
389
404
  agentViewOpenSessionRef.current = channel.agentId;
390
- setAgentViewOpen(true);
405
+ setSupervisorOpen(true);
391
406
  }
392
407
  });
393
408
  }, [channel]);
@@ -405,6 +420,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
405
420
  const [themeName, setTheme] = useTheme();
406
421
  const { rows: terminalRows } = useTerminalSize();
407
422
  const [showAllMessages, setShowAllMessages] = React.useState(false);
423
+ /** Scope the fold to this question: an aborted ask can promote its queued
424
+ * successor without ever publishing an idle (null) snapshot. */
425
+ const [minimizedQuestionKey, setMinimizedQuestionKey] = React.useState(null);
426
+ const questionMinimized = questionSnapshot !== null && minimizedQuestionKey === questionSnapshot.key;
408
427
  /** Fold state for the GoalTodoPanel todo section (ctrl/cmd+q or click). */
409
428
  const [todoCollapsed, setTodoCollapsed] = React.useState(false);
410
429
  const [thinkingVisible, setThinkingVisible] = React.useState(true);
@@ -615,6 +634,34 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
615
634
  });
616
635
  setSceneOpen(true);
617
636
  }, []);
637
+ /**
638
+ * Leave the session supervisor for the conversation.
639
+ *
640
+ * The one-shot landing preference is written here rather than at boot: a
641
+ * process that dies before the user ever sees the screen (a config error, a
642
+ * crash during the first render) must not burn the installation's only
643
+ * first-launch landing. Writing on the way OUT means "the user has seen it".
644
+ *
645
+ * Leaving also honours `/bg`'s return target. `/background` moved the
646
+ * session the user was in to the background and opened this screen; a plain
647
+ * Esc out of it re-attaches to that session instead of silently leaving them
648
+ * on the fresh one, which is what "go back to what I was doing" means. Any
649
+ * explicit mount inside the screen clears the target first, so this can
650
+ * never undo a choice the user just made.
651
+ */
652
+ const closeHome = React.useCallback(() => {
653
+ suppressLogoIntroRef.current = true;
654
+ markHomeSeen();
655
+ const returnTo = agentViewReturnId;
656
+ setAgentViewReturnId(undefined);
657
+ setSupervisorOpen(false);
658
+ if (returnTo !== undefined && returnTo !== channel.agentId) {
659
+ void channel.resumeTo(returnTo).then((result) => {
660
+ if (result.ok)
661
+ repaintTranscript();
662
+ }).catch(() => undefined);
663
+ }
664
+ }, [agentViewReturnId, channel, repaintTranscript]);
618
665
  /** The startup summary gives way to transcript rows after the first local command or message. */
619
666
  const loadedContextVisible = channel.rows.length === 0 && channel.loadedContext !== undefined;
620
667
  /** Startup context panel: collapsed by default, toggled with Ctrl+P. */
@@ -851,12 +898,121 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
851
898
  const exitTimerRef = React.useRef(null);
852
899
  // Live view into the prompt's text for the Ctrl+C rule (clears text when
853
900
  // non-empty; the double-press exit only arms on an empty input).
854
- const promptControllerRef = React.useRef(null);
901
+ const ownPromptControllerRef = React.useRef(null);
902
+ const promptControllerRef = promptControllerRefProp ?? ownPromptControllerRef;
903
+ /**
904
+ * Owner of the unsent draft. Every screen this component renders INSTEAD of
905
+ * the conversation (the session screen, the tree, settings, the jobs and
906
+ * subagent panels, the trajectory scene) unmounts the composer, and the
907
+ * composer keeps its text in local state — so without this the half-written
908
+ * prompt died on the way in. The slot lives here, outlives that unmount, and
909
+ * is dropped the moment the attached session changes so no draft can follow
910
+ * the user into a different conversation.
911
+ */
912
+ const promptDraftRef = React.useRef({ current: null });
913
+ /**
914
+ * Latest channel for the unmount release below: that effect must not re-run
915
+ * on a channel identity change, yet its cleanup must release against the
916
+ * channel of the last render.
917
+ */
918
+ const channelRef = React.useRef(channel);
919
+ channelRef.current = channel;
920
+ /**
921
+ * Release the staged images a WAITING snapshot alone owns.
922
+ *
923
+ * While a draft waits in the slot for the composer to remount, the snapshot
924
+ * is the only owner of the capabilities behind its `[Image #N]` tokens. If
925
+ * Chat itself goes away first (leaving an early-return screen by exiting the
926
+ * TUI), nothing would ever restore or discard them — the session's
927
+ * 128-entry FIFO would evict live entries instead. The `hasStagedImage`
928
+ * guard keeps a capability the channel already recycled a no-op; both calls
929
+ * are idempotent.
930
+ *
931
+ * Scope, deliberately narrow (review round 7): a capability a QUEUED message
932
+ * still references (`channel.pending`) is never revoked here. The real
933
+ * double-hold path is paste an image → queue the draft with Tab while the
934
+ * model works → recall that line from input history with ↑ (same stageId
935
+ * rebound to the draft) → park the composer. Delivery resolves its refs from
936
+ * the enqueue-time capture, so a late revoke would only bite a host that
937
+ * re-resolves them afterwards — this keeps the rule identical to
938
+ * `stageIdIsRetained` instead of relying on that.
939
+ *
940
+ * A composer that is still MOUNTED when Chat unmounts is NOT covered: React
941
+ * runs this parent cleanup BEFORE the child's, so the child then writes its
942
+ * draft into the now-dead ref and those ids ride the channel's lifetime out
943
+ * (the #942 review's remaining P2). Neither unmount path loses anything
944
+ * user-visible — the channel dies with them.
945
+ */
946
+ React.useEffect(() => {
947
+ return () => {
948
+ const snapshot = promptDraftRef.current.current;
949
+ promptDraftRef.current.current = null;
950
+ if (snapshot === null)
951
+ return;
952
+ const queued = new Set();
953
+ for (const item of channelRef.current.pending) {
954
+ // `?? []`: a foreign/embedded host may hand us a pending entry without
955
+ // images, and a throw inside an unmount cleanup escapes into the exit
956
+ // path — every other reader of this field guards it the same way.
957
+ for (const image of item.images ?? [])
958
+ queued.add(image.stageId);
959
+ }
960
+ for (const [, stageId] of snapshot.images) {
961
+ if (queued.has(stageId))
962
+ continue;
963
+ if (channelRef.current.hasStagedImage?.(stageId) === true) {
964
+ channelRef.current.discardStagedImage(stageId);
965
+ }
966
+ }
967
+ };
968
+ }, []);
969
+ const draftSessionId = channel.agentId;
970
+ /** Session the effect below last reconciled against; a change is a switch. */
971
+ const draftSessionRef = React.useRef(draftSessionId);
972
+ /**
973
+ * The session a fill Chat itself requested belongs to, if one is in flight.
974
+ *
975
+ * A rewind's restored message arrives in the same commit that replaces the
976
+ * session, and it belongs to the NEW binding — the user picked it. The
977
+ * composer cannot tell, so Chat says so here, at the two call sites that ask
978
+ * for a fill.
979
+ *
980
+ * Keyed by the session id rather than a bare flag, so it can only ever excuse
981
+ * the switch it was written for. Do NOT clear it when the composer consumes
982
+ * the fill: a child's layout effects run before the parent's, so the fill is
983
+ * consumed in the very commit this effect judges, and clearing it there would
984
+ * wipe the message the user just got back.
985
+ */
986
+ const pendingFillRef = React.useRef(null);
987
+ /**
988
+ * Drop the composer's text when the session underneath it is replaced.
989
+ *
990
+ * A LAYOUT effect, not a passive one: the clear has to land in the commit
991
+ * that swaps the session. A passive effect is flushed later, and anything
992
+ * typed in between (the tree's hand-off, a fast user) would be wiped with the
993
+ * old conversation's text. Which DRAFT the slot keeps is a separate question,
994
+ * answered by the snapshot's owner fields.
995
+ */
996
+ React.useLayoutEffect(() => {
997
+ if (draftSessionRef.current === draftSessionId)
998
+ return;
999
+ draftSessionRef.current = draftSessionId;
1000
+ // A stored draft can only belong to the conversation being replaced: the
1001
+ // composer is the one that writes it, and it writes it on the way out.
1002
+ promptDraftRef.current.current = null;
1003
+ if (pendingFillRef.current === draftSessionId) {
1004
+ pendingFillRef.current = null;
1005
+ return;
1006
+ }
1007
+ promptControllerRef.current?.clear();
1008
+ }, [draftSessionId]);
855
1009
  const previewGallery = activePreview === null ? [] : activePreview.peek
856
1010
  ? promptControllerRef.current?.previewImages?.() ?? [activePreview]
857
1011
  : overlay.kind === 'image-preview' ? overlay.gallery ?? [activePreview] : [];
1012
+ // Peek entries are rebuilt from the prompt every render: match by
1013
+ // attachment id + token title, not facade identity.
858
1014
  const previewIndex = activePreview?.peek
859
- ? previewGallery.findIndex(entry => entry.image === activePreview.image && entry.title === activePreview.title)
1015
+ ? previewGallery.findIndex(entry => entry.image.id === activePreview.image.id && entry.title === activePreview.title)
860
1016
  : overlay.kind === 'image-preview' ? overlay.index ?? 0 : -1;
861
1017
  const stepPreview = (delta) => {
862
1018
  if (!activePreview?.peek) {
@@ -943,7 +1099,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
943
1099
  // fullscreen (<AlternateScreen> supplies mouse tracking); a no-op
944
1100
  // subscription in inline mode, where selection belongs to the terminal.
945
1101
  // The copy clears the highlight and posts a transient notification.
946
- useCopyOnSelect(text => channel.notify(t('copied-chars', { n: text.length }), { timeoutMs: 1500 }));
1102
+ useCopyOnSelect(text => channel.notify(t('copied-chars', { n: text.length }), { timeoutMs: 1500 }),
1103
+ // Stale-selection refusal: the highlighted rows were replaced in place
1104
+ // (streaming overwrite), so nothing was copied — say why instead of
1105
+ // letting the highlight vanish silently.
1106
+ () => channel.notify(t('copy-refused-stale'), { timeoutMs: 2500 }));
947
1107
  const { clearSelection: clearMouseSelection, hasSelection: hasMouseSelection } = useSelection();
948
1108
  React.useEffect(() => {
949
1109
  if (!channel.working || !terminalFocused)
@@ -1611,27 +1771,31 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
1611
1771
  }
1612
1772
  return true;
1613
1773
  }
1614
- case 'resume': {
1615
- setHelpOpen(false);
1616
- // The browser opens immediately and loads its own list. Waiting for
1617
- // the listing here would make `/resume` feel slower the more history
1618
- // a project has, which is exactly backwards.
1619
- setBrowserOpen(true);
1620
- return true;
1621
- }
1774
+ case 'resume':
1775
+ /**
1776
+ * `/resume`, `/home` and `/agentview` are one screen.
1777
+ *
1778
+ * They were three implementations of one domain and drifted apart: the
1779
+ * same session could be listed by all three, each with its own selection
1780
+ * model and its own idea of what opening one does. Keeping the three
1781
+ * commands is about muscle memory, not about three surfaces — every one
1782
+ * of them lands here, on the same runtime.
1783
+ */
1784
+ case 'home':
1622
1785
  case 'agentview': {
1623
- // The agent view shows one screen for every session. It opens
1624
- // immediately; the view reads its own rows (live + persisted).
1625
1786
  setHelpOpen(false);
1787
+ // The screen opens immediately and loads its own list. Waiting for the
1788
+ // listing here would make it feel slower the more history a project
1789
+ // has, which is exactly backwards.
1626
1790
  agentViewOpenSessionRef.current = channel.agentId;
1627
- setAgentViewOpen(true);
1791
+ setSupervisorOpen(true);
1628
1792
  return true;
1629
1793
  }
1630
1794
  case 'bg':
1631
1795
  case 'background': {
1632
1796
  // `/background`: the attached session moves to the background
1633
1797
  // (it keeps running in this process), the terminal lands on a fresh
1634
- // session, and the agent view opens on top.
1798
+ // session, and the supervisor opens on top.
1635
1799
  setHelpOpen(false);
1636
1800
  backgroundToAgentView();
1637
1801
  return true;
@@ -2256,6 +2420,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2256
2420
  const performRewind = async (row, mode = null) => {
2257
2421
  const text = await channel.rewindTo(row, mode);
2258
2422
  if (text !== null) {
2423
+ // The restored message belongs to the binding `rewindTo` just created,
2424
+ // not to the one it replaced: it is the user's choice, and the switch
2425
+ // effect must not treat it as the old conversation's leftovers.
2426
+ pendingFillRef.current = String(channel.agentId);
2259
2427
  // Put the restored message back in the prompt for re-editing.
2260
2428
  setHistoryFill(text);
2261
2429
  channel.notify(t('rewind-done'));
@@ -2460,18 +2628,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2460
2628
  || overlay.kind === 'tips'
2461
2629
  || (recap !== null && (!recap.auto || recap.expanded)))
2462
2630
  return;
2463
- // Same for the session browser: it renders instead of the conversation,
2464
- // so every key belongs to it — including the plain letters that drive its
2465
- // search box, which Chat would otherwise route into the prompt.
2466
- if (browserOpen)
2467
- return;
2468
- // Same for the session tree: plain letters drive its search, clicks and
2469
- // Enter drive its action menu.
2631
+ // The session tree owns the whole terminal while it is up: plain letters
2632
+ // drive its search, clicks and Enter drive its action menu.
2470
2633
  if (treeOpen)
2471
2634
  return;
2472
- // The agent view is another whole-screen surface:
2473
- // its dispatch input owns every printable key.
2474
- if (agentViewOpen)
2635
+ // The session supervisor owns the whole terminal while it is up: its rail
2636
+ // and session list bind ↑/↓/Enter/Tab/Esc, its filter box takes the plain
2637
+ // letters that would otherwise reach the prompt, and the directory picker
2638
+ // and menus it opens are its own modal layers.
2639
+ if (supervisorOpen)
2475
2640
  return;
2476
2641
  // Same for the settings screen: plain letters (s save / d discard) and
2477
2642
  // the field draft editor belong to it alone.
@@ -2575,8 +2740,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
2575
2740
  // keyboard while one is pending (the panel's own useInput handles
2576
2741
  // ↑/↓/Space/Tab/Enter/Esc; the prompt input is suspended, so nothing
2577
2742
  // else should see these keys).
2578
- if (questionSnapshot !== null || approvalSnapshot !== null || dialogSnapshot !== null)
2743
+ if (approvalSnapshot !== null || dialogSnapshot !== null)
2744
+ return;
2745
+ if (questionSnapshot !== null) {
2746
+ // Only transcript navigation belongs here. The mounted questionnaire
2747
+ // owns fold/expand keys, including when it interrupts another screen.
2748
+ if (questionMinimized && !isSticky && (isPlainReturnInput(input, key) || key.end)) {
2749
+ handle?.scrollToBottom();
2750
+ event.stopImmediatePropagation();
2751
+ }
2579
2752
  return;
2753
+ }
2580
2754
  const returnCandidate = isPlainReturnInput(input, key);
2581
2755
  const returnNow = Date.now();
2582
2756
  const plainReturn = returnCandidate && returnNow - lastModalEnterAtRef.current >= 80;
@@ -3365,9 +3539,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3365
3539
  const approvalPanelNode = approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, background: approvalSnapshot.agentId !== channel.agentId, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : null;
3366
3540
  const questionPanelNode = questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, initialDraft: questionSnapshot.draft, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent(), onBack: questionSnapshot.canGoBack
3367
3541
  ? draft => questionStore.backCurrent(draft)
3368
- : undefined }, questionSnapshot.key)) : null;
3542
+ : undefined, collapsed: questionMinimized, onExpand: () => setMinimizedQuestionKey(null), onToggleFold: () => setMinimizedQuestionKey(previous => previous === questionSnapshot.key ? null : questionSnapshot.key), fullscreen: fullscreen }, questionSnapshot.key)) : null;
3369
3543
  const interruptPanel = approvalPanelNode ?? questionPanelNode;
3370
- const screenOpen = channel.pluginScene !== undefined || browserOpen || settingsOpen
3544
+ const screenOpen = channel.pluginScene !== undefined || supervisorOpen || settingsOpen
3371
3545
  || subagentDetailId !== null || subagentDashboardOpen || sceneOpen;
3372
3546
  if (interruptPanel !== null && screenOpen) {
3373
3547
  const node = (_jsx(Box, { flexDirection: "column", width: "100%", paddingX: 1, children: interruptPanel }));
@@ -3394,37 +3568,62 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3394
3568
  }, children: renderScene ? renderScene(pluginScene.id, channel) : _jsxs(Text, { children: ["Scene unavailable: ", pluginScene.id] }) }));
3395
3569
  return fullscreen ? node : _jsx(AlternateScreen, { children: node });
3396
3570
  }
3397
- // The agent view is a screen like the browser: it REPLACES the
3398
- // conversation. Every session keeps running behind it — including the
3399
- // attached one mid-turn.
3400
- if (agentViewOpen) {
3401
- const view = (_jsx(AgentView, { channel: channel, home: homeDir(), approval: approvalSnapshot, onApprove: outcome => approvals.decide(outcome), returnSessionId: agentViewReturnId, onClose: () => {
3402
- // The transcript tree remounts on close: never replay the whale
3403
- // intro there, and when the session changed INSIDE the view
3404
- // (attach / backgrounded dispatch), repaint the fresh transcript
3405
- // from the top like `/new` does.
3571
+ /**
3572
+ * The session supervisor: a screen in the same sense as the tree — an early
3573
+ * return after every hook above has run, so there is no transcript
3574
+ * underneath to repaint or bled through.
3575
+ *
3576
+ * It sits ABOVE the session tree because it is the surface a launch can
3577
+ * start on (`openHomeOnBoot`): a first launch has no conversation to come
3578
+ * back to, and every action it offers either mounts a session (which closes
3579
+ * it) or starts a new one.
3580
+ *
3581
+ * Behind it, every session this terminal hosts keeps running — that is the
3582
+ * runtime the screen describes, not an implementation detail of it. A turn
3583
+ * that was in flight when the user opened this screen is still in flight
3584
+ * while they read the list, which is why closing the screen only repaints
3585
+ * the transcript when the attached session actually changed.
3586
+ */
3587
+ if (supervisorOpen) {
3588
+ /**
3589
+ * Live state per session, from the channel's own agent-view projection.
3590
+ * Reading the projection rather than a parallel source is what keeps this
3591
+ * screen and the attention hints in the composer footer from disagreeing
3592
+ * about which session is waiting for input.
3593
+ */
3594
+ const agentRowOf = (sessionId) => agentViewRows.find(row => row.id === sessionId);
3595
+ const supervisorNode = (_jsx(SessionSupervisor, { channel: channel, home: homeDir(), onClose: closeHome, approval: approvalSnapshot, onApprove: outcome => approvals.decide(outcome), onOpenSession: async (sessionId) => {
3596
+ const result = await channel.resumeTo(sessionId);
3597
+ if (!result.ok) {
3598
+ const text = resumeFailureText(result);
3599
+ if (text !== undefined)
3600
+ channel.notify(text, { color: 'error', timeoutMs: 8000 });
3601
+ return false;
3602
+ }
3603
+ channel.notify(t('resume-resumed'));
3406
3604
  suppressLogoIntroRef.current = true;
3407
- const switched = agentViewOpenSessionRef.current !== undefined &&
3408
- agentViewOpenSessionRef.current !== channel.agentId;
3409
- if (switched)
3410
- repaintTranscript();
3411
3605
  setAgentViewReturnId(undefined);
3412
- setAgentViewOpen(false);
3413
- } }));
3414
- return fullscreen ? view : _jsx(AlternateScreen, { children: view });
3415
- }
3416
- // The browser is a screen, not an overlay: it REPLACES the conversation
3417
- // rather than floating above it. Rendering it as an early return (after
3418
- // every hook above has run) is what makes that literal — there is no
3419
- // transcript underneath to be repainted, scrolled, or bled through.
3420
- if (browserOpen) {
3421
- const browser = (_jsx(SessionBrowser, { channel: channel, home: homeDir(), sameProject: sessionCwdMatches, onClose: () => {
3422
- suppressLogoIntroRef.current = true;
3423
- setBrowserOpen(false);
3606
+ setSupervisorOpen(false);
3607
+ repaintTranscript();
3608
+ return true;
3609
+ }, onNewSession: async (target) => {
3610
+ const ok = await channel.switchWorkspace(target);
3611
+ if (ok) {
3612
+ suppressLogoIntroRef.current = true;
3613
+ setAgentViewReturnId(undefined);
3614
+ setSupervisorOpen(false);
3615
+ repaintTranscript();
3616
+ }
3617
+ return ok;
3618
+ }, onStopSession: async (sessionId) => channel.stopBackgroundAgent?.(sessionId) ?? false, liveStateOf: (sessionId) => {
3619
+ const row = agentRowOf(sessionId);
3620
+ return row === undefined
3621
+ ? undefined
3622
+ : { status: row.status, live: row.live, current: row.current, summary: row.summary };
3424
3623
  } }));
3425
3624
  // Inline hosts enter the alternate screen for the duration; full-screen
3426
3625
  // hosts are already in it and must not nest a second one.
3427
- return fullscreen ? browser : _jsx(AlternateScreen, { children: browser });
3626
+ return fullscreen ? supervisorNode : _jsx(AlternateScreen, { children: supervisorNode });
3428
3627
  }
3429
3628
  // The session tree follows the browser's rule exactly: it REPLACES the
3430
3629
  // conversation (an early return after every hook above has run), so there
@@ -3432,6 +3631,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3432
3631
  // turn's prompt returns through the same fill path a rewind picker uses.
3433
3632
  if (treeOpen) {
3434
3633
  const tree = (_jsx(SessionTree, { channel: channel, currentSessionId: channel.agentId, onClose: () => setTreeOpen(false), onRestoreText: (text) => {
3634
+ // The tree rewound to a node and is handing that turn's prompt back,
3635
+ // exactly like the picker does. It belongs to the binding the tree
3636
+ // action just created.
3637
+ pendingFillRef.current = String(channel.agentId);
3435
3638
  setHistoryFill(text);
3436
3639
  } }));
3437
3640
  return fullscreen ? tree : _jsx(AlternateScreen, { children: tree });
@@ -3568,7 +3771,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3568
3771
  // sessions keep the full intro; restored ones settle instantly.
3569
3772
  // A remount after a whole screen closed also settles instantly
3570
3773
  // (see suppressLogoIntroRef).
3571
- skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
3774
+ skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, sessionCwd: channel.cwd, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
3572
3775
  // Gutter mode (settings `dsh-tui.scrollGutter`): the timeline
3573
3776
  // rail (default), the proportional scrollbar, or nothing. The
3574
3777
  // slot keeps its 2 columns in both rendered modes (Qwen's
@@ -3617,7 +3820,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
3617
3820
  void setClipboard(btw.answer ?? '').then(raw => { if (raw)
3618
3821
  writeRaw?.(raw); });
3619
3822
  channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
3620
- } }) })) : questionPanelNode !== null ? (questionPanelNode) : null, _jsx(PromptInput, { channel: channel, suspended: promptReplacementOpen, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => { setHistoryFill(null); }, onRewindRequest: openRewind, onBackgroundRequest: backgroundToAgentView, backgroundAgentsNeedingInput:
3823
+ } }) })) : questionPanelNode !== null ? (questionPanelNode) : null, _jsx(PromptInput, { channel: channel, suspended: promptReplacementOpen, draftCache: promptDraftRef.current, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => setHistoryFill(null), onRewindRequest: openRewind, onBackgroundRequest: backgroundToAgentView,
3824
+ // The 🏠 at the head of the input row opens the same session screen
3825
+ // `/resume` and `/agentview` open — one surface, three doors. It is
3826
+ // gated on this prop rather than a setting, so hosts that mount the
3827
+ // prompt without a session screen (and the layout regressions that
3828
+ // pin the row's column budget) keep the row they had.
3829
+ onOpenSessions: () => {
3830
+ agentViewOpenSessionRef.current = channel.agentId;
3831
+ setSupervisorOpen(true);
3832
+ }, backgroundAgentsNeedingInput:
3621
3833
  // Only the real channel supplies the seam; pre-agent-view test
3622
3834
  // stubs must not grow the footer row (layout-dependent
3623
3835
  // regressions pin the visible row count). The footer only