@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
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Data, derived state and actions for the unified session screen.
3
+ *
4
+ * The screen keeps only assembly and key routing; everything that answers "what
5
+ * is on screen and what does an action do" lives here. That is what makes the
6
+ * two panes presentational and the derivations reachable from a regression.
7
+ *
8
+ * Two ownership rules must not be duplicated anywhere else:
9
+ * - the cursor is ONE fact; every index is derived from it, so movement,
10
+ * rendering and Enter can never disagree;
11
+ * - occupancy is re-read from the ledger on this screen own pulse and never
12
+ * captured from the parent render.
13
+ */
14
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
15
+ import { basename } from 'node:path';
16
+ import { t } from '../../i18n.js';
17
+ import { normalizeWorkspaceCwd } from '../../sessions/view.js';
18
+ import { readSessionPins, setSessionPinned } from '../../sessionPins.js';
19
+ import { readSessionOwners } from '../../sessionMounts.js';
20
+ import { RAIL_CHROME_ROWS, WORKSPACE_ROW_LINES, RAIL_MIN_TOTAL_COLUMNS, RAIL_WIDTH_MIN, RAIL_WIDTH_MAX, SESSION_ROW_LINES, SESSION_PANE_CHROME_ROWS, MENU_ACTIONS, UNREGISTERED_RAIL_ID, message, samePath, sessionMatchesQuery } from './model.js';
21
+ /**
22
+ * Derive the whole screen model.
23
+ * @param input - Channel, home, the opening/stop actions and the live lookup.
24
+ * @returns Every value the screen renders from, plus the action callbacks.
25
+ */
26
+ export function useSessionSupervisor(input) {
27
+ const { channel, home, onOpenSession, onNewSession, onStopSession, liveStateOf, columns, rows } = input;
28
+ const [entries, setEntries] = useState([]);
29
+ const [sessions, setSessions] = useState([]);
30
+ const [loading, setLoading] = useState(true);
31
+ const [notice, setNotice] = useState(undefined);
32
+ /** Live status and occupancy are re-read on their own clock, not the listing's. */
33
+ const [pulse, setPulse] = useState(0);
34
+ const [query, setQuery] = useState('');
35
+ /**
36
+ * Cross-process occupancy, re-read on the SAME tick as the live state.
37
+ *
38
+ * This has to be read here, not captured by the host: the ledger is a file
39
+ * another process writes, so a snapshot taken during the parent's render went
40
+ * stale the moment it was taken and nothing re-took it — the parent does not
41
+ * re-render on this screen's 2s pulse, so a foreign terminal that acquired or
42
+ * released a session left the row red (and unclickable) until some unrelated
43
+ * channel event happened to repaint. Reading behind a pulse-keyed ref keeps
44
+ * it to one read per tick while the rows and the click guard always see the
45
+ * current holder.
46
+ */
47
+ const occupancyRef = useRef(new Map());
48
+ const occupancyPulseRef = useRef(-1);
49
+ if (occupancyPulseRef.current !== pulse) {
50
+ occupancyPulseRef.current = pulse;
51
+ occupancyRef.current = readSessionOwners();
52
+ }
53
+ const holderOf = useCallback((sessionId) => {
54
+ const owner = occupancyRef.current.get(sessionId);
55
+ return owner === undefined || owner.pid === process.pid ? undefined : owner.pid;
56
+ }, []);
57
+ /**
58
+ * Sessions eligible for this screen, computed ONCE per listing so the rail's
59
+ * per-workspace counts and the pane's rows always agree.
60
+ *
61
+ * Three things are hidden, and the third is the one that is easy to lose:
62
+ * a delegated run (its own row belongs to the agent-run folding, not to a
63
+ * workspace listing), a log holding no conversation, and the CURRENT
64
+ * session's fork ANCESTORS. The last one matters because a `/resume` fork
65
+ * records `parentSession` exactly like a delegated run does — listing the
66
+ * chain makes one conversation look like several, with no way to tell which
67
+ * row continues what the user is looking at.
68
+ *
69
+ * The current session itself stays listed (marked `current` by the live
70
+ * state): this screen exists to show what the terminal hosts, and "the one
71
+ * you are in" is the row the user is most likely looking for. Only the
72
+ * ancestors go — they are the same conversation at an earlier point, which
73
+ * the current row already represents. `buildView` hides the current id as
74
+ * well because its list has no live-state column to mark it with.
75
+ */
76
+ const listedSessions = useMemo(() => {
77
+ const byId = new Map(sessions.map(session => [session.id, session]));
78
+ const ancestors = new Set();
79
+ let cursor = byId.get(channel.agentId);
80
+ while (cursor?.kind.kind === 'fork') {
81
+ const parent = cursor.kind.parent;
82
+ if (parent === undefined || ancestors.has(parent))
83
+ break;
84
+ ancestors.add(parent);
85
+ cursor = byId.get(parent);
86
+ }
87
+ return sessions.filter(session => session.hasPrompt && session.kind.kind !== 'subagent' && !ancestors.has(session.id));
88
+ }, [sessions, channel.agentId]);
89
+ /**
90
+ * The rail's rows: the durable registry, plus rows for sessions that live in
91
+ * a directory the registry does not know (or while it cannot be read at all).
92
+ *
93
+ * The registry is the sidebar's own ledger, and it is genuinely OPTIONAL:
94
+ * `createLocalWorkspaceRuntime()` supports compositions with no workspace
95
+ * stack and returns an empty one, a registration can be removed while its
96
+ * session logs stay on disk, and the service itself can reject. Sessions in
97
+ * every one of those cases are still resumable — the persistence store, not
98
+ * the registry, is what holds them — so an empty rail must not render "no
99
+ * history" and leave them unreachable.
100
+ *
101
+ * Each unregistered directory gets its OWN row (titled by its basename), so
102
+ * the rail keeps telling the user WHERE a session ran; when even the session
103
+ * paths are unavailable they all land in one synthetic group. These rows live
104
+ * only inside this screen and are never written back to the ledger.
105
+ */
106
+ const railEntries = useMemo(() => {
107
+ const orphans = listedSessions.filter(session => !entries.some(entry => samePath(entry.path, session.cwd)));
108
+ if (orphans.length === 0)
109
+ return entries;
110
+ const byCwd = new Map();
111
+ for (const session of orphans) {
112
+ const key = normalizeWorkspaceCwd(session.cwd);
113
+ const existing = byCwd.get(key);
114
+ if (existing === undefined)
115
+ byCwd.set(key, { path: session.cwd, count: 1 });
116
+ else
117
+ existing.count += 1;
118
+ }
119
+ return [...entries, ...[...byCwd.entries()].map(([key, group]) => ({
120
+ id: `${UNREGISTERED_RAIL_ID}:${key === '' ? 'unknown' : key}`,
121
+ path: group.path === '' ? UNREGISTERED_RAIL_ID : group.path,
122
+ title: basename(group.path) || t('supervisor-unregistered'),
123
+ present: true,
124
+ sessionCount: group.count,
125
+ from: 'unregistered',
126
+ }))];
127
+ }, [entries, listedSessions]);
128
+ const groupedEntries = useMemo(() => {
129
+ const groups = new Map();
130
+ for (const session of listedSessions) {
131
+ const path = railEntries.find(entry => entry.from === 'registry' && samePath(entry.path, session.cwd))?.path
132
+ ?? (session.cwd === '' ? UNREGISTERED_RAIL_ID : session.cwd);
133
+ const bucket = groups.get(path);
134
+ if (bucket === undefined)
135
+ groups.set(path, [session]);
136
+ else
137
+ bucket.push(session);
138
+ }
139
+ return groups;
140
+ }, [listedSessions, railEntries]);
141
+ /** Session count for one rail row, from the same grouping the pane uses. */
142
+ const countOf = useCallback((entry) => (groupedEntries.get(entry.path) ?? []).length, [groupedEntries]);
143
+ const [railFocus, setRailFocus] = useState(0);
144
+ /**
145
+ * A rail entry the user picked by hand that is NOT registered — the fallback
146
+ * group for unregistered sessions. It exists only for this screen's lifetime:
147
+ * selecting a group is a way to SEE those sessions, never a way to register a
148
+ * directory, so it must not create a ledger record.
149
+ */
150
+ const [selectedUnregistered, setSelectedUnregistered] = useState(false);
151
+ const [selectedPath, setSelectedPath] = useState(undefined);
152
+ /** True once the user picked a rail row by hand; see the selection effect. */
153
+ const [selectionManual, setSelectionManual] = useState(false);
154
+ /**
155
+ * The session column's cursor, as ONE fact.
156
+ *
157
+ * It used to be three: a `sessionFocus` index, a `sessionFocusRef` mirror and
158
+ * a `focusSessionId`, with the render deriving the index from the id while
159
+ * Enter read the ref. The filter and the live-state re-sort move rows, so the
160
+ * ref went stale and Enter opened a row other than the one under `❯`. The id
161
+ * (or the card) is stored here and the index is always DERIVED from it.
162
+ */
163
+ /**
164
+ * The cursor of the session column, as ONE fact: the session id it is on, or
165
+ * undefined for the new-session card.
166
+ *
167
+ * The screen used to keep an index, a ref mirror and an id beside each other,
168
+ * with the render deriving an index from the id while Enter read the ref. The
169
+ * filter and the live-state re-sort move rows, so the stored index kept
170
+ * pointing at the offset it had held while `❯` was drawn from the id — Enter
171
+ * then acted on a row the user had never selected. There is one fact now, and
172
+ * {@link focusIndex} is derived from it for the render, for movement and for
173
+ * Enter alike.
174
+ */
175
+ const [focusSessionId, setFocusSessionId] = useState(undefined);
176
+ /**
177
+ * Which column owns the keyboard, and therefore which column draws the `❯`
178
+ * cursor. Exactly one at a time: two cursors mean "where does Enter go?" has
179
+ * no answer, and ←/→ is how this screen answers it.
180
+ */
181
+ const [activePane, setActivePane] = useState('rail');
182
+ const [pins, setPins] = useState(() => readSessionPins());
183
+ const [menu, setMenu] = useState(undefined);
184
+ const [rename, setRename] = useState(undefined);
185
+ const [confirmRemove, setConfirmRemove] = useState(undefined);
186
+ const railRef = useRef(railFocus);
187
+ railRef.current = railFocus;
188
+ const menuRef = useRef(menu);
189
+ menuRef.current = menu;
190
+ const queryRef = useRef(query);
191
+ queryRef.current = query;
192
+ const now = Date.now();
193
+ /**
194
+ * One cheap tick that re-derives live status and cross-process occupancy.
195
+ *
196
+ * Deliberately NOT a re-listing: the listing is the expensive part (a stat
197
+ * per session and a revision-keyed digest), while status and occupancy are
198
+ * two in-memory reads over data the process already holds. So the tick is
199
+ * affordable at a rate that keeps `/resume` honest about a sibling
200
+ * terminal — the user sees another TUI take or release a session while
201
+ * looking at the screen, without this screen re-reading the session store.
202
+ */
203
+ React.useEffect(() => {
204
+ const timer = setInterval(() => setPulse(value => value + 1), 2000);
205
+ return () => clearInterval(timer);
206
+ }, []);
207
+ /**
208
+ * Reload the ledger and the session listing together.
209
+ *
210
+ * One `listSessions()` pass feeds every workspace: re-reading the whole
211
+ * store per workspace click would be both slower and inconsistent between
212
+ * the two panes.
213
+ *
214
+ * The ledger read degrades to an EMPTY rail when the host does not expose it,
215
+ * instead of failing the whole reload: `/bg` opens this screen, and a host
216
+ * written before the workspace ledger existed (the older in-repo regressions
217
+ * compose exactly that) would otherwise get "failed to read sessions" on
218
+ * screen and lose the session listing with it. The session list is the half
219
+ * this screen cannot work without, so it must survive a missing ledger.
220
+ */
221
+ const reload = useCallback(async () => {
222
+ // The two reads are independent, and the session listing is the half this
223
+ // screen cannot work without: a registry that rejects (bare composition,
224
+ // unmounted service, a provider throwing) must not take the history down
225
+ // with it. So the listing is settled on its own, and a registry failure
226
+ // degrades to the empty rail the cwd-derived fallback groups already cover.
227
+ await Promise.all([
228
+ (async () => {
229
+ try {
230
+ setSessions(await channel.listSessions());
231
+ setNotice(current => (current?.tone === 'error' ? undefined : current));
232
+ }
233
+ catch (error) {
234
+ setNotice({ text: t('home-sessions-failed', { err: message(error) }), tone: 'error' });
235
+ }
236
+ })(),
237
+ (async () => {
238
+ try {
239
+ const registry = typeof channel.listWorkspaceRegistry === 'function'
240
+ ? await channel.listWorkspaceRegistry()
241
+ : [];
242
+ setEntries(registry.map(entry => ({ ...entry, from: 'registry' })));
243
+ }
244
+ catch {
245
+ // An unreadable registry is not an empty history: the sessions stay
246
+ // listed (and resumable) under the cwd-derived fallback groups.
247
+ setEntries([]);
248
+ }
249
+ })(),
250
+ ]);
251
+ setLoading(false);
252
+ }, [channel]);
253
+ React.useEffect(() => {
254
+ void reload();
255
+ }, [reload]);
256
+ // Selection follows the terminal's own directory, then the ledger: the rail
257
+ // must open on the workspace this terminal is IN, not on whichever record
258
+ // sorts first — otherwise launching in a workspace you have never opened
259
+ // lands the marker on some unrelated project.
260
+ //
261
+ // It deliberately does NOT latch the first default it computes. The listing
262
+ // arrives asynchronously, so the very first pass runs against an EMPTY ledger;
263
+ // latching there would pin the rail to whatever record arrives first and never
264
+ // reconsider. While the selection is still ours to make, every ledger update
265
+ // re-derives it (`entries[0]` remains the fallback when the terminal's own
266
+ // directory is not registered); once the user picks a row by hand, that choice
267
+ // wins until its entry disappears.
268
+ //
269
+ // `channel.cwd` is a dependency, not just a first read: resuming a session
270
+ // from another workspace moves this terminal to that workspace, and the rail
271
+ // follows the session the pane is showing rather than the launch directory.
272
+ React.useEffect(() => {
273
+ if (railEntries.length === 0) {
274
+ if (selectedPath !== undefined && !selectionManual)
275
+ setSelectedPath(undefined);
276
+ return;
277
+ }
278
+ if (selectionManual
279
+ && selectedPath !== undefined
280
+ && railEntries.some(entry => entry.from === 'registry' && samePath(entry.path, selectedPath)))
281
+ return;
282
+ // A hand-picked fallback group stays picked while it is still on the rail.
283
+ // Without this the group would be dropped on the very next listing pass and
284
+ // the sessions it was showing would vanish again.
285
+ if (selectionManual && selectedUnregistered
286
+ && railEntries.some(entry => entry.from === 'unregistered'))
287
+ return;
288
+ const here = railEntries.find(entry => entry.from === 'registry' && samePath(entry.path, channel.cwd));
289
+ const next = here ?? railEntries[0];
290
+ setSelectedPath(next.from === 'registry' ? next.path : undefined);
291
+ setSelectedUnregistered(next.from === 'unregistered');
292
+ // The cursor travels with an automatic selection. It starts at 0, so
293
+ // leaving it there while the selection lands elsewhere paints two green
294
+ // rows — `❯` on the first record and the marker on the selected one — until
295
+ // some input moves it. The two are one position on this screen, and the
296
+ // very first frame has to render that way. A pick the user made by hand is
297
+ // left alone (it already moved the cursor itself).
298
+ setRailFocus(current => {
299
+ const index = railEntries.findIndex(entry => entry.id === next.id);
300
+ return index < 0 || current === index ? current : index;
301
+ });
302
+ }, [railEntries, selectedPath, selectedUnregistered, selectionManual, channel.cwd]);
303
+ // The cursor indexes the entry list directly (there is no `+` row in front of
304
+ // it), so a shrinking ledger has to pull it back inside or the last row would
305
+ // highlight nothing.
306
+ React.useEffect(() => {
307
+ setRailFocus(current => Math.min(current, Math.max(0, railEntries.length - 1)));
308
+ }, [railEntries.length]);
309
+ /** The rail row whose sessions the pane is showing (or the fallback group). */
310
+ const selected = useMemo(() => {
311
+ const registered = railEntries.find(entry => entry.from === 'registry' && selectedPath !== undefined && samePath(entry.path, selectedPath));
312
+ if (registered !== undefined)
313
+ return registered;
314
+ if (selectedUnregistered)
315
+ return railEntries.find(entry => entry.from === 'unregistered');
316
+ return railEntries[0];
317
+ }, [railEntries, selectedPath, selectedUnregistered]);
318
+ /**
319
+ * Sessions whose recorded cwd is the selected workspace, minus the search
320
+ * filter. Live sessions in this workspace sort above stopped ones, then by
321
+ * recency: what this terminal is currently running is what the user is
322
+ * most likely switching between.
323
+ */
324
+ const visibleSessions = useMemo(() => {
325
+ if (selected === undefined)
326
+ return [];
327
+ const needle = query.trim().toLowerCase();
328
+ void pulse;
329
+ return (groupedEntries.get(selected.path) ?? [])
330
+ .filter(session => sessionMatchesQuery(session, needle))
331
+ .slice()
332
+ .sort((left, right) => {
333
+ const leftLive = liveStateOf(left.id)?.live === true ? 1 : 0;
334
+ const rightLive = liveStateOf(right.id)?.live === true ? 1 : 0;
335
+ return rightLive - leftLive || right.updatedAt - left.updatedAt;
336
+ });
337
+ }, [groupedEntries, selected, query, liveStateOf, pulse]);
338
+ /**
339
+ * Cursor identity: rows reorder on every reload and filter, so the cursor
340
+ * follows an ID rather than an index — and the cursor space includes the
341
+ * new-session card as row 0. The card is not decoration: it has to be
342
+ * selectable like every other card, or the keyboard loses a path to the one
343
+ * action that still works when the list is empty.
344
+ */
345
+ const sessionIndex = useMemo(() => {
346
+ // No focused session means the card (row 0) holds the cursor.
347
+ if (focusSessionId === undefined)
348
+ return 0;
349
+ const byId = visibleSessions.findIndex(session => session.id === focusSessionId);
350
+ if (byId >= 0)
351
+ return byId + 1;
352
+ // The focused session is not on screen (the filter removed it). The cursor
353
+ // must still stand on a REAL row, because Enter acts on whatever it stands
354
+ // on: landing on the card would turn "search, then Enter" into "start a new
355
+ // session" — an action the user never asked for. With an empty match set the
356
+ // card is the only row there is, so it keeps the cursor.
357
+ return visibleSessions.length === 0 ? 0 : 1;
358
+ }, [visibleSessions, focusSessionId]);
359
+ /** True while the new-session card holds the cursor; the render says why. */
360
+ const cardFocused = activePane === 'list' && sessionIndex === 0;
361
+ /** The new-session card is the list's row 0; sessions start at 1. */
362
+ const sessionAt = useCallback((index) => visibleSessions[index - 1], [visibleSessions]);
363
+ /**
364
+ * Enter the session column: land the cursor on the session this terminal is
365
+ * attached to (that is the one the user most likely means), else on the top
366
+ * row — which is the new-session card when the list sorted its live rows
367
+ * lower. A cursor that stayed put while the list scrolled elsewhere would act
368
+ * on a row the user never looked at.
369
+ */
370
+ const activateList = useCallback(() => {
371
+ setActivePane('list');
372
+ const current = visibleSessions.find(session => liveStateOf(session.id)?.current === true);
373
+ setFocusSessionId(current?.id);
374
+ }, [liveStateOf, visibleSessions]);
375
+ /** Enter the workspace column. */
376
+ const activateRail = useCallback(() => {
377
+ setActivePane('rail');
378
+ }, []);
379
+ const railWidth = columns >= RAIL_MIN_TOTAL_COLUMNS
380
+ ? Math.min(RAIL_WIDTH_MAX, Math.max(RAIL_WIDTH_MIN, Math.floor(columns * 0.3)))
381
+ : columns;
382
+ const railVisible = columns >= RAIL_MIN_TOTAL_COLUMNS;
383
+ const sessionWidth = Math.max(20, columns - (railVisible ? railWidth + 1 : 0));
384
+ const railListHeight = Math.max(1, rows - RAIL_CHROME_ROWS);
385
+ /**
386
+ * How many WHOLE workspaces the rail can show. `HomeWorkspaceRow` is always
387
+ * {@link WORKSPACE_ROW_LINES} rows, so the row budget has to be divided by
388
+ * that before it can be used as a window size; handing the row count to the
389
+ * window math directly let the list render twice as many entries as fit and
390
+ * clipped the focused one out of the viewport.
391
+ */
392
+ const railEntryCapacity = Math.max(1, Math.floor(railListHeight / WORKSPACE_ROW_LINES));
393
+ const sessionListHeight = Math.max(SESSION_ROW_LINES, rows - SESSION_PANE_CHROME_ROWS);
394
+ const report = useCallback((text, tone) => {
395
+ setNotice({ text, tone });
396
+ }, []);
397
+ const persistPin = useCallback((id, pinned) => {
398
+ const result = setSessionPinned(id, pinned);
399
+ if (!result.ok) {
400
+ report(t('resume-pin-save-failed'), 'error');
401
+ return;
402
+ }
403
+ setPins(result.pins);
404
+ }, [report]);
405
+ const selectEntry = useCallback((entry) => {
406
+ setSelectedPath(entry.from === 'registry' ? entry.path : undefined);
407
+ setSelectedUnregistered(entry.from === 'unregistered');
408
+ setSelectionManual(true);
409
+ setFocusSessionId(undefined);
410
+ }, []);
411
+ /**
412
+ * Mount a session, refusing one another terminal holds.
413
+ *
414
+ * The occupancy test runs here as well as in the adapter: the adapter's is
415
+ * the authority (it must hold for every caller), but checking first lets the
416
+ * screen say WHICH terminal owns the session instead of reporting a generic
417
+ * failure, and keeps a refused row from looking like a broken one.
418
+ */
419
+ const openSession = useCallback((session) => {
420
+ const holder = holderOf(session.id);
421
+ if (holder !== undefined) {
422
+ report(t('supervisor-occupied', { pid: holder }), 'error');
423
+ return;
424
+ }
425
+ setNotice(undefined);
426
+ void onOpenSession(session.id)
427
+ .then((ok) => {
428
+ // The host owns the REASON: it is the layer that saw the mount result
429
+ // (Chat renders the real refusal through `resumeFailureText` and a
430
+ // notification). This screen only names WHICH session could not be
431
+ // entered — a notice that restated the generic failure would compete
432
+ // with, and read worse than, the host's own sentence.
433
+ if (!ok)
434
+ report(t('supervisor-open-failed', { name: session.title.text }), 'error');
435
+ })
436
+ .catch(error => report(t('session-resume-failed', { err: message(error) }), 'error'));
437
+ }, [holderOf, onOpenSession, report]);
438
+ const newSessionIn = useCallback((entry) => {
439
+ setNotice(undefined);
440
+ void channel.resolveWorkspace(entry.path)
441
+ .then((target) => {
442
+ if (target === undefined) {
443
+ report(t('workspace-open-invalid', { target: entry.path }), 'error');
444
+ return undefined;
445
+ }
446
+ return onNewSession(target).then((ok) => {
447
+ if (!ok)
448
+ report(t('new-session-failed', { err: '' }), 'error');
449
+ return ok;
450
+ });
451
+ })
452
+ .catch(error => report(t('new-session-failed', { err: message(error) }), 'error'));
453
+ }, [channel, onNewSession, report]);
454
+ const renameEntry = useCallback((path, title) => {
455
+ const next = title.trim();
456
+ if (next === '') {
457
+ report(t('home-rename-empty'), 'error');
458
+ return;
459
+ }
460
+ void channel.renameWorkspaceAt(path, next)
461
+ .then((ok) => {
462
+ if (ok)
463
+ return reload();
464
+ report(t('home-rename-failed', { err: '' }), 'error');
465
+ return undefined;
466
+ })
467
+ .catch(error => report(t('home-rename-failed', { err: message(error) }), 'error'));
468
+ }, [channel, reload, report]);
469
+ const removeEntry = useCallback((path) => {
470
+ void channel.removeWorkspace(path)
471
+ .then((ok) => {
472
+ if (ok)
473
+ return reload();
474
+ report(t('workspace-remove-unknown', { target: path }), 'error');
475
+ return undefined;
476
+ })
477
+ .catch(error => report(t('workspace-remove-failed', { err: message(error) }), 'error'));
478
+ }, [channel, reload, report]);
479
+ /** Stop a parked background session; the attached one is not stoppable. */
480
+ const stopSession = useCallback((session) => {
481
+ const state = liveStateOf(session.id);
482
+ if (state?.current === true) {
483
+ report(t('supervisor-stop-current'), 'error');
484
+ return;
485
+ }
486
+ if (state?.live !== true)
487
+ return;
488
+ void onStopSession(session.id)
489
+ .then((stopped) => {
490
+ report(stopped ? t('supervisor-stopped', { name: session.title.text }) : t('supervisor-stop-failed'), stopped ? 'info' : 'error');
491
+ })
492
+ .catch((error) => report(t('supervisor-stop-failed') + ` · ${message(error)}`, 'error'));
493
+ }, [liveStateOf, onStopSession, report]);
494
+ const closeMenu = useCallback(() => {
495
+ menuRef.current = undefined;
496
+ setMenu(undefined);
497
+ }, []);
498
+ const activateMenu = useCallback((entry, item) => {
499
+ closeMenu();
500
+ const action = MENU_ACTIONS[item] ?? 'edit';
501
+ if (action === 'edit')
502
+ selectEntry(entry);
503
+ else if (action === 'new')
504
+ newSessionIn(entry);
505
+ // The fallback group is not a registration, so there is no ledger row to
506
+ // rename or drop: those two actions would either fail or (worse) try to
507
+ // mutate a record that does not exist.
508
+ else if (entry.from === 'unregistered')
509
+ return;
510
+ else if (action === 'rename')
511
+ setRename({ path: entry.path, draft: entry.title });
512
+ else
513
+ setConfirmRemove(entry.path);
514
+ }, [closeMenu, newSessionIn, selectEntry]);
515
+ const moveRail = useCallback((by) => {
516
+ // The rail is the ledger (plus the unregistered fallback group), so every
517
+ // row is a workspace and the cursor is an entry index over that list alone.
518
+ const total = Math.max(1, railEntries.length);
519
+ const next = (railRef.current + by + total) % total;
520
+ railRef.current = next;
521
+ setRailFocus(next);
522
+ const entry = railEntries[next];
523
+ if (entry !== undefined)
524
+ selectEntry(entry);
525
+ }, [railEntries, selectEntry]);
526
+ const moveSession = useCallback((by) => {
527
+ // +1: the cursor space includes the new-session card as row 0, and with an
528
+ // EMPTY list the card is still a row the user can stand on. Clamping the
529
+ // index at 0 instead would have made ↓/↑ do nothing at all there.
530
+ const total = visibleSessions.length + 1;
531
+ const next = Math.min(total - 1, Math.max(0, sessionIndex + by));
532
+ // Landing on the card CLEARS the id: leaving the last session's id in place
533
+ // made the derived index resolve back to that session's row, which is what
534
+ // put `❯` on the first session while the user had selected the card.
535
+ const landed = sessionAt(next);
536
+ setFocusSessionId(landed?.id);
537
+ }, [visibleSessions, sessionAt, sessionIndex]);
538
+ /** The session under the cursor, or undefined while the card (row 0) holds it. */
539
+ const focusedSession = sessionAt(sessionIndex);
540
+ return {
541
+ entries,
542
+ sessions,
543
+ loading,
544
+ notice,
545
+ setNotice,
546
+ query,
547
+ setQuery,
548
+ holderOf,
549
+ listedSessions,
550
+ railEntries,
551
+ countOf,
552
+ railFocus,
553
+ setRailFocus,
554
+ setFocusSessionId,
555
+ activePane,
556
+ pins,
557
+ menu,
558
+ setMenu,
559
+ rename,
560
+ setRename,
561
+ confirmRemove,
562
+ setConfirmRemove,
563
+ railRef,
564
+ menuRef,
565
+ queryRef,
566
+ now,
567
+ reload,
568
+ selected,
569
+ visibleSessions,
570
+ sessionIndex,
571
+ cardFocused,
572
+ sessionAt,
573
+ activateList,
574
+ activateRail,
575
+ railWidth,
576
+ railVisible,
577
+ sessionWidth,
578
+ railEntryCapacity,
579
+ sessionListHeight,
580
+ persistPin,
581
+ selectEntry,
582
+ openSession,
583
+ newSessionIn,
584
+ renameEntry,
585
+ removeEntry,
586
+ stopSession,
587
+ closeMenu,
588
+ activateMenu,
589
+ moveRail,
590
+ moveSession,
591
+ focusedSession,
592
+ };
593
+ }