@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.1

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 (351) hide show
  1. package/README.md +175 -123
  2. package/bin/dsh-tui.js +748 -22
  3. package/cordis.patch.yml +42 -13
  4. package/cordis.yml +5 -2
  5. package/lib/types/activityPrefs.d.ts +24 -0
  6. package/lib/types/activityPrefs.d.ts.map +1 -1
  7. package/lib/types/activityPrefs.js +50 -1
  8. package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
  9. package/lib/types/adapter/channel/session-projection.js +2 -0
  10. package/lib/types/adapter/channel/state.d.ts.map +1 -1
  11. package/lib/types/adapter/channel/state.js +3 -1
  12. package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
  13. package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
  14. package/lib/types/adapter/channel/ui-policy.js +6 -2
  15. package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
  16. package/lib/types/adapter/kernel/host-facade.js +1 -0
  17. package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
  18. package/lib/types/adapter/kernel/replay.js +3 -0
  19. package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
  20. package/lib/types/adapter/kernel/runtime.js +2 -0
  21. package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
  22. package/lib/types/adapter/kernel/slices/workspace.js +1 -0
  23. package/lib/types/adapter/ports/channel-ui.d.ts +29 -5
  24. package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
  25. package/lib/types/adapter/ports/channel-view.d.ts +66 -12
  26. package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
  27. package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
  28. package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
  29. package/lib/types/adapter/ports/channel.d.ts +4 -1
  30. package/lib/types/adapter/ports/channel.d.ts.map +1 -1
  31. package/lib/types/adapter/ports/workspace.d.ts +2 -0
  32. package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
  33. package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
  34. package/lib/types/adapter/upstream/workspace-driver.js +10 -2
  35. package/lib/types/commands.d.ts.map +1 -1
  36. package/lib/types/commands.js +1 -0
  37. package/lib/types/components/ActivityLine.d.ts +7 -2
  38. package/lib/types/components/ActivityLine.d.ts.map +1 -1
  39. package/lib/types/components/ActivityLine.js +0 -8
  40. package/lib/types/components/ContextBarView.d.ts +15 -7
  41. package/lib/types/components/ContextBarView.d.ts.map +1 -1
  42. package/lib/types/components/ContextBarView.js +26 -22
  43. package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
  44. package/lib/types/components/ImagePreviewOverlay.js +11 -5
  45. package/lib/types/components/Markdown.d.ts +11 -1
  46. package/lib/types/components/Markdown.d.ts.map +1 -1
  47. package/lib/types/components/Markdown.js +20 -2
  48. package/lib/types/components/MermaidDiagram.d.ts +13 -0
  49. package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
  50. package/lib/types/components/MermaidDiagram.js +61 -0
  51. package/lib/types/components/MessageList.d.ts +26 -1
  52. package/lib/types/components/MessageList.d.ts.map +1 -1
  53. package/lib/types/components/MessageList.js +74 -8
  54. package/lib/types/components/PageMargin.d.ts +1 -1
  55. package/lib/types/components/PromptInput.d.ts +37 -1
  56. package/lib/types/components/PromptInput.d.ts.map +1 -1
  57. package/lib/types/components/PromptInput.js +291 -30
  58. package/lib/types/components/ScrollbarGutter.d.ts +20 -5
  59. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
  60. package/lib/types/components/ScrollbarGutter.js +88 -10
  61. package/lib/types/components/SplitDiffView.d.ts.map +1 -1
  62. package/lib/types/components/SplitDiffView.js +2 -1
  63. package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
  64. package/lib/types/components/StreamingMarkdown.js +30 -28
  65. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  66. package/lib/types/components/SubagentCard.js +12 -6
  67. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  68. package/lib/types/components/SubagentDetailScene.js +8 -3
  69. package/lib/types/components/Tooltip.d.ts.map +1 -1
  70. package/lib/types/components/Tooltip.js +45 -7
  71. package/lib/types/components/design-system/HintLine.d.ts +1 -2
  72. package/lib/types/components/design-system/HintLine.d.ts.map +1 -1
  73. package/lib/types/components/design-system/HintLine.js +1 -2
  74. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  75. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  76. package/lib/types/components/messages/AssistantThinkingMessage.js +2 -1
  77. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +2 -2
  78. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  79. package/lib/types/components/messages/AssistantToolUseMessage.js +30 -26
  80. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  81. package/lib/types/components/messages/UserPromptMessage.js +5 -7
  82. package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
  83. package/lib/types/components/messages/useImageInspection.js +19 -9
  84. package/lib/types/components/promptDraftCache.d.ts +102 -0
  85. package/lib/types/components/promptDraftCache.d.ts.map +1 -0
  86. package/lib/types/components/promptDraftCache.js +76 -0
  87. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
  88. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  89. package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
  90. package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
  91. package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
  92. package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
  93. package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
  94. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  95. package/lib/types/components/sessions/SessionListRow.js +52 -5
  96. package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
  97. package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
  98. package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
  99. package/lib/types/dsh-adapter/activity-store.d.ts +221 -0
  100. package/lib/types/dsh-adapter/activity-store.d.ts.map +1 -0
  101. package/lib/types/dsh-adapter/activity-store.js +359 -0
  102. package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
  103. package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
  104. package/lib/types/dsh-adapter/bundled-presets.js +80 -0
  105. package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
  106. package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
  107. package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
  108. package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
  109. package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
  110. package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
  111. package/lib/types/dsh-adapter/channel/background-action.d.ts +1 -1
  112. package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
  113. package/lib/types/dsh-adapter/channel/background-action.js +53 -29
  114. package/lib/types/dsh-adapter/channel/binding-events.d.ts +8 -6
  115. package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
  116. package/lib/types/dsh-adapter/channel/binding-events.js +5 -4
  117. package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
  118. package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
  119. package/lib/types/dsh-adapter/channel/binding.js +78 -18
  120. package/lib/types/dsh-adapter/channel/compaction.d.ts +0 -1
  121. package/lib/types/dsh-adapter/channel/compaction.d.ts.map +1 -1
  122. package/lib/types/dsh-adapter/channel/compaction.js +0 -2
  123. package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
  124. package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
  125. package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
  126. package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
  127. package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
  128. package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
  129. package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
  130. package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
  131. package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
  132. package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
  133. package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
  134. package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
  135. package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
  136. package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
  137. package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
  138. package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
  139. package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
  140. package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
  141. package/lib/types/dsh-adapter/channel/model-switch.d.ts +1 -2
  142. package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
  143. package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
  144. package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
  145. package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
  146. package/lib/types/dsh-adapter/channel/projection.js +29 -19
  147. package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
  148. package/lib/types/dsh-adapter/channel/reports.js +4 -12
  149. package/lib/types/dsh-adapter/channel/session-adoption.d.ts +1 -1
  150. package/lib/types/dsh-adapter/channel/session-adoption.d.ts.map +1 -1
  151. package/lib/types/dsh-adapter/channel/session-adoption.js +1 -0
  152. package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
  153. package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
  154. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +4 -1
  155. package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
  156. package/lib/types/dsh-adapter/channel/session-live-adoption.js +2 -0
  157. package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
  158. package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
  159. package/lib/types/dsh-adapter/channel/session-metadata.js +6 -4
  160. package/lib/types/dsh-adapter/channel/session-reset.d.ts +1 -1
  161. package/lib/types/dsh-adapter/channel/session-reset.d.ts.map +1 -1
  162. package/lib/types/dsh-adapter/channel/session-reset.js +0 -1
  163. package/lib/types/dsh-adapter/channel/session-resume.d.ts +12 -3
  164. package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
  165. package/lib/types/dsh-adapter/channel/session-resume.js +292 -94
  166. package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
  167. package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
  168. package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
  169. package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
  170. package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
  171. package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
  172. package/lib/types/dsh-adapter/channel/state.d.ts +6 -1
  173. package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
  174. package/lib/types/dsh-adapter/channel/state.js +3 -3
  175. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts +21 -1
  176. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
  177. package/lib/types/dsh-adapter/channel/subagent-projection.js +173 -6
  178. package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
  179. package/lib/types/dsh-adapter/channel/transcript.js +6 -12
  180. package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
  181. package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
  182. package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
  183. package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
  184. package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
  185. package/lib/types/dsh-adapter/channel.d.ts +1 -1
  186. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  187. package/lib/types/dsh-adapter/channel.js +82 -20
  188. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  189. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  190. package/lib/types/dsh-adapter/compat/index.js +1 -1
  191. package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
  192. package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
  193. package/lib/types/dsh-adapter/compat/messages.js +15 -0
  194. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +18 -0
  195. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  196. package/lib/types/dsh-adapter/compat/sessionLog.js +18 -3
  197. package/lib/types/dsh-adapter/compat/settings.d.ts +35 -0
  198. package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
  199. package/lib/types/dsh-adapter/compat/settings.js +57 -0
  200. package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
  201. package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
  202. package/lib/types/dsh-adapter/compat/shell.js +11 -0
  203. package/lib/types/dsh-adapter/contract.d.ts +5 -4
  204. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  205. package/lib/types/dsh-adapter/contract.js +17 -6
  206. package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
  207. package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
  208. package/lib/types/dsh-adapter/ide-channel.js +556 -0
  209. package/lib/types/dsh-adapter/index.d.ts +18 -3
  210. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  211. package/lib/types/dsh-adapter/index.js +32 -5
  212. package/lib/types/dsh-adapter/plugin.d.ts +4 -1
  213. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  214. package/lib/types/dsh-adapter/plugin.js +197 -49
  215. package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
  216. package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
  217. package/lib/types/dsh-adapter/preset-resolution.js +10 -0
  218. package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
  219. package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
  220. package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
  221. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
  222. package/lib/types/dsh-adapter/sessionTree.js +3 -2
  223. package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
  224. package/lib/types/dsh-adapter/settings-sections.js +7 -4
  225. package/lib/types/dsh-adapter/subagents.d.ts +16 -3
  226. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  227. package/lib/types/dsh-adapter/subagents.js +92 -29
  228. package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
  229. package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
  230. package/lib/types/dsh-adapter/workspaces.js +116 -0
  231. package/lib/types/effortPrefs.d.ts +15 -3
  232. package/lib/types/effortPrefs.d.ts.map +1 -1
  233. package/lib/types/effortPrefs.js +37 -3
  234. package/lib/types/history.d.ts +13 -1
  235. package/lib/types/history.d.ts.map +1 -1
  236. package/lib/types/history.js +15 -2
  237. package/lib/types/homePrefs.d.ts +15 -0
  238. package/lib/types/homePrefs.d.ts.map +1 -0
  239. package/lib/types/homePrefs.js +53 -0
  240. package/lib/types/i18n.d.ts +347 -297
  241. package/lib/types/i18n.d.ts.map +1 -1
  242. package/lib/types/i18n.js +133 -85
  243. package/lib/types/ink/colorize.d.ts +10 -0
  244. package/lib/types/ink/colorize.d.ts.map +1 -1
  245. package/lib/types/ink/colorize.js +21 -0
  246. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  247. package/lib/types/ink/components/ScrollBox.js +12 -1
  248. package/lib/types/ink/dom.d.ts +1 -0
  249. package/lib/types/ink/dom.d.ts.map +1 -1
  250. package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
  251. package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
  252. package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
  253. package/lib/types/ink/ink.d.ts.map +1 -1
  254. package/lib/types/ink/ink.js +36 -1
  255. package/lib/types/ink/log-update.d.ts.map +1 -1
  256. package/lib/types/ink/log-update.js +19 -1
  257. package/lib/types/ink/output.d.ts +10 -1
  258. package/lib/types/ink/output.d.ts.map +1 -1
  259. package/lib/types/ink/output.js +109 -17
  260. package/lib/types/ink/render-border.d.ts +6 -1
  261. package/lib/types/ink/render-border.d.ts.map +1 -1
  262. package/lib/types/ink/render-border.js +25 -9
  263. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  264. package/lib/types/ink/render-node-to-output.js +48 -6
  265. package/lib/types/ink/renderer.d.ts +6 -0
  266. package/lib/types/ink/renderer.d.ts.map +1 -1
  267. package/lib/types/ink/renderer.js +2 -1
  268. package/lib/types/ink/selection.d.ts +88 -0
  269. package/lib/types/ink/selection.d.ts.map +1 -1
  270. package/lib/types/ink/selection.js +164 -0
  271. package/lib/types/ink/sixel-graphics.d.ts +19 -1
  272. package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
  273. package/lib/types/ink/sixel-graphics.js +124 -12
  274. package/lib/types/ink/terminal-image.d.ts +18 -1
  275. package/lib/types/ink/terminal-image.d.ts.map +1 -1
  276. package/lib/types/ink/terminal.d.ts +17 -0
  277. package/lib/types/ink/terminal.d.ts.map +1 -1
  278. package/lib/types/ink/terminal.js +20 -1
  279. package/lib/types/screens/Chat.d.ts +20 -1
  280. package/lib/types/screens/Chat.d.ts.map +1 -1
  281. package/lib/types/screens/Chat.js +296 -76
  282. package/lib/types/screens/SessionSupervisor.d.ts +84 -0
  283. package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
  284. package/lib/types/screens/SessionSupervisor.js +350 -0
  285. package/lib/types/screens/SessionTree.js +2 -2
  286. package/lib/types/screens/Settings.d.ts.map +1 -1
  287. package/lib/types/screens/Settings.js +32 -9
  288. package/lib/types/screens/StatusLine.d.ts +18 -1
  289. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  290. package/lib/types/screens/StatusLine.js +38 -19
  291. package/lib/types/screens/StatusMetrics.d.ts +79 -22
  292. package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
  293. package/lib/types/screens/StatusMetrics.js +153 -88
  294. package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
  295. package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
  296. package/lib/types/screens/sessionSupervisor/model.js +69 -0
  297. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
  298. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
  299. package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
  300. package/lib/types/sessionMounts.d.ts +272 -0
  301. package/lib/types/sessionMounts.d.ts.map +1 -0
  302. package/lib/types/sessionMounts.js +706 -0
  303. package/lib/types/sessions/format.d.ts.map +1 -1
  304. package/lib/types/sessions/format.js +4 -1
  305. package/lib/types/sessions/resumeFailure.d.ts +38 -0
  306. package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
  307. package/lib/types/sessions/resumeFailure.js +58 -0
  308. package/lib/types/terminal-utils/mermaid.d.ts +44 -0
  309. package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
  310. package/lib/types/terminal-utils/mermaid.js +83 -0
  311. package/lib/types/tips.d.ts.map +1 -1
  312. package/lib/types/tips.js +175 -19
  313. package/lib/types/tuiDisplayPrefs.d.ts +7 -9
  314. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  315. package/lib/types/tuiDisplayPrefs.js +31 -28
  316. package/lib/types/update.d.ts +61 -0
  317. package/lib/types/update.d.ts.map +1 -1
  318. package/lib/types/update.js +194 -0
  319. package/lib/types/utils/fold-long-lines.d.ts +1 -1
  320. package/lib/types/utils/fold-long-lines.js +1 -1
  321. package/lib/types/utils/imageResize.d.ts +90 -0
  322. package/lib/types/utils/imageResize.d.ts.map +1 -0
  323. package/lib/types/utils/imageResize.js +272 -0
  324. package/lib/types/utils/keymap.d.ts +5 -1
  325. package/lib/types/utils/keymap.d.ts.map +1 -1
  326. package/lib/types/utils/keymap.js +23 -0
  327. package/lib/types/working-activity.d.ts +10 -9
  328. package/lib/types/working-activity.d.ts.map +1 -1
  329. package/lib/types/working-activity.js +17 -10
  330. package/lib/types/workspaces.d.ts +2 -1
  331. package/lib/types/workspaces.d.ts.map +1 -1
  332. package/lib/types/workspaces.js +1 -1
  333. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +10 -10
  334. package/package.json +126 -105
  335. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  336. package/presets/liangshen/agent.cordis.yml +15 -0
  337. package/lib/types/components/design-system/KeyboardShortcutHint.d.ts +0 -19
  338. package/lib/types/components/design-system/KeyboardShortcutHint.d.ts.map +0 -1
  339. package/lib/types/components/design-system/KeyboardShortcutHint.js +0 -14
  340. package/lib/types/dsh-adapter/channel/activity.d.ts +0 -20
  341. package/lib/types/dsh-adapter/channel/activity.d.ts.map +0 -1
  342. package/lib/types/dsh-adapter/channel/activity.js +0 -113
  343. package/lib/types/screens/AgentView.d.ts +0 -23
  344. package/lib/types/screens/AgentView.d.ts.map +0 -1
  345. package/lib/types/screens/AgentView.js +0 -546
  346. package/lib/types/screens/SessionBrowser.d.ts +0 -42
  347. package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
  348. package/lib/types/screens/SessionBrowser.js +0 -911
  349. package/lib/types/terminal-utils/terminal.d.ts +0 -18
  350. package/lib/types/terminal-utils/terminal.d.ts.map +0 -1
  351. package/lib/types/terminal-utils/terminal.js +0 -96
@@ -0,0 +1,706 @@
1
+ /**
2
+ * Cross-process session mounting ledger, kept at
3
+ * `~/.dsh-tui/session-mounts.json`.
4
+ *
5
+ * A TUI terminal hosts SEVERAL agent sessions at once (the attached one plus
6
+ * every parked background session), and sessions are durable: another TUI, or
7
+ * `dsh web`, can list the same logs. Nothing in the DSH session store says
8
+ * *who is currently driving a log*, so two TUI processes resuming the same
9
+ * session would interleave writes into one append-only event log and corrupt
10
+ * the transcript. This file is that missing fact: each TUI process publishes
11
+ * the set of sessions it has mounted, and a reader that sees a foreign entry
12
+ * treats the session as OCCUPIED and refuses to mount it.
13
+ *
14
+ * Liveness is one witness, `process.kill(pid, 0)`: it catches a clean exit and
15
+ * a `kill -9`. There is no heartbeat timestamp, because a timestamp only stays
16
+ * truthful while a timer keeps refreshing it, and the process that cannot
17
+ * refresh it is exactly the one whose record should expire. The cost is pid
18
+ * REUSE — a recycled pid keeps a dead owner's record alive — which errs toward
19
+ * refusing a session that is in fact free and costs one restart; the opposite
20
+ * error interleaves two writers into one transcript. This is a same-machine
21
+ * guard only: the host's own session write lock stays the authority that
22
+ * actually separates writers.
23
+ *
24
+ * Reads never write. Pruning a dead owner happens on the write path, because a
25
+ * reader that wrote its snapshot back could erase a peer's record published
26
+ * between its read and its replace — silently dropping a live session from the
27
+ * occupancy table while its process kept writing.
28
+ *
29
+ * Writes take a short cross-process lock, re-read the file under it, and replace
30
+ * it atomically. The lock is token-checked, so a holder whose lock was reclaimed
31
+ * as stale cannot write over — or delete — the new holder's lock. A CLAIM
32
+ * re-derives its conflict under that lock ({@link claimMount}), because
33
+ * "check, then publish" is not atomic across processes.
34
+ *
35
+ * "I could not read the ledger" is NOT "nobody holds the session". Granting a
36
+ * write handle on a damaged or unreadable ledger is exactly the failure this
37
+ * module exists to prevent, and it is unrecoverable, so the authoritative
38
+ * paths refuse instead of degrading ({@link readMountLedgerStrict},
39
+ * {@link MountFailure}). Only the display surface reads best-effort.
40
+ *
41
+ * The lock is reclaimable only from a holder that is provably gone: its token
42
+ * carries the holder pid, so a lock whose holder is still ALIVE is never stolen
43
+ * from — not even by a peer that finds it old. That closes the window where a
44
+ * paused holder resumed after its lock had been reclaimed and committed a
45
+ * snapshot derived before the steal. The price is pid REUSE on a lock: a
46
+ * recycled pid keeps a dead holder's lock alive, and the remedy is to delete
47
+ * `session-mounts.lock` while every process sharing this data directory is
48
+ * stopped.
49
+ */
50
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, statSync, writeFileSync, writeSync, } from 'node:fs';
51
+ import { randomBytes } from 'node:crypto';
52
+ import { join } from 'node:path';
53
+ import { DATA_DIR } from './utils/paths.js';
54
+ const MOUNTS_FILE = 'session-mounts.json';
55
+ const LOCK_FILE = 'session-mounts.lock';
56
+ /** Schema version of the persisted document. */
57
+ const MOUNTS_VERSION = 1;
58
+ /**
59
+ * How old a lock with no readable holder pid must be before it counts as a
60
+ * crash leftover. A lock whose token names a pid is judged by THAT witness
61
+ * instead (see {@link acquireLock}); this grace window only covers the sliver
62
+ * between creating the file and writing the token into it.
63
+ */
64
+ const STALE_LOCK_MS = 30_000;
65
+ let temporarySequence = 0;
66
+ function hasCode(error, code) {
67
+ return typeof error === 'object' && error !== null && error.code === code;
68
+ }
69
+ /** Whether a published record was written by THIS process. */
70
+ export function ownerIsSelf(owner) {
71
+ return owner.pid === process.pid;
72
+ }
73
+ const EMPTY = { kind: 'free' };
74
+ /**
75
+ * Whether a process id is still alive. `process.kill(pid, 0)` sends no signal
76
+ * and throws `ESRCH` when the process is gone; `EPERM` means it exists but is
77
+ * owned by another user, which still counts as alive here.
78
+ */
79
+ export function pidAlive(pid) {
80
+ if (!Number.isInteger(pid) || pid <= 0)
81
+ return false;
82
+ if (pid === process.pid)
83
+ return true;
84
+ try {
85
+ process.kill(pid, 0);
86
+ return true;
87
+ }
88
+ catch (error) {
89
+ return hasCode(error, 'EPERM');
90
+ }
91
+ }
92
+ /** Parse one record, or undefined when the shape is wrong. */
93
+ function parseOwner(value) {
94
+ if (typeof value !== 'object' || value === null)
95
+ return undefined;
96
+ const { pid, startedAt, sessionIds } = value;
97
+ if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
98
+ return undefined;
99
+ if (typeof startedAt !== 'number' || !Number.isFinite(startedAt))
100
+ return undefined;
101
+ if (!Array.isArray(sessionIds))
102
+ return undefined;
103
+ return {
104
+ pid,
105
+ startedAt,
106
+ sessionIds: sessionIds.filter((id) => typeof id === 'string' && id.length > 0),
107
+ };
108
+ }
109
+ /**
110
+ * Read the ledger for DISPLAY, keeping every record the shape check accepts.
111
+ * A missing, empty, truncated or foreign-shaped document reads as an empty
112
+ * ledger, because a screen that cannot read the occupancy table should still
113
+ * paint the sessions it does know about.
114
+ *
115
+ * Never use this to decide whether a session may be mounted: "the read failed"
116
+ * and "nobody holds it" are the same answer here. Authoritative callers use
117
+ * {@link readMountLedgerStrict}.
118
+ */
119
+ export function readMountLedger() {
120
+ try {
121
+ const parsed = JSON.parse(readFileSync(join(DATA_DIR, MOUNTS_FILE), 'utf8'));
122
+ if (typeof parsed !== 'object' || parsed === null)
123
+ return [];
124
+ const document = parsed;
125
+ if (document.version !== MOUNTS_VERSION || !Array.isArray(document.owners))
126
+ return [];
127
+ return document.owners
128
+ .map(parseOwner)
129
+ .filter((owner) => owner !== undefined);
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ }
135
+ /**
136
+ * Read the ledger for DECIDING, where "I could not read it" must not pass for
137
+ * "it is empty".
138
+ *
139
+ * Only a missing file is an empty ledger — that is the genuine first run.
140
+ * Everything else (unreadable, malformed JSON, unknown version, a record whose
141
+ * shape does not parse) is `unavailable`, and the caller must refuse. Skipping
142
+ * a bad record and writing the ledger anyway is not an option either: the
143
+ * record that failed to parse may be another writer's only claim, so the safe
144
+ * move is to leave the file exactly as it is and ask the user to repair it.
145
+ */
146
+ export function readMountLedgerStrict() {
147
+ const path = join(DATA_DIR, MOUNTS_FILE);
148
+ let raw;
149
+ try {
150
+ raw = readFileSync(path, 'utf8');
151
+ }
152
+ catch (error) {
153
+ if (hasCode(error, 'ENOENT'))
154
+ return { ok: true, owners: [] };
155
+ return { ok: false, detail: `cannot read ${path}: ${error instanceof Error ? error.message : String(error)}` };
156
+ }
157
+ let parsed;
158
+ try {
159
+ parsed = JSON.parse(raw);
160
+ }
161
+ catch {
162
+ return { ok: false, detail: `${path} is not valid JSON` };
163
+ }
164
+ if (typeof parsed !== 'object' || parsed === null) {
165
+ return { ok: false, detail: `${path} is not a ledger document` };
166
+ }
167
+ const document = parsed;
168
+ if (document.version !== MOUNTS_VERSION) {
169
+ return { ok: false, detail: `${path} has unknown version ${JSON.stringify(document.version)}` };
170
+ }
171
+ if (!Array.isArray(document.owners)) {
172
+ return { ok: false, detail: `${path} has no owners array` };
173
+ }
174
+ const owners = [];
175
+ for (const record of document.owners) {
176
+ const owner = parseOwner(record);
177
+ if (owner === undefined)
178
+ return { ok: false, detail: `${path} has a record whose shape is wrong` };
179
+ owners.push(owner);
180
+ }
181
+ return { ok: true, owners };
182
+ }
183
+ /**
184
+ * Which process has each session mounted, considering only owners whose pid is
185
+ * still alive. A session claimed by several live owners keeps the record that
186
+ * started last: the protocol forbids that state, and picking one
187
+ * deterministically beats reporting whichever the file happened to list first.
188
+ */
189
+ export function readSessionOwners() {
190
+ const owners = new Map();
191
+ for (const owner of readMountLedger()) {
192
+ if (!pidAlive(owner.pid))
193
+ continue;
194
+ for (const sessionId of owner.sessionIds) {
195
+ const existing = owners.get(sessionId);
196
+ if (existing === undefined || existing.startedAt < owner.startedAt)
197
+ owners.set(sessionId, owner);
198
+ }
199
+ }
200
+ return owners;
201
+ }
202
+ /**
203
+ * The occupancy of one session, from THIS process's point of view.
204
+ * @param sessionId - Session id to look up.
205
+ * @param owners - A snapshot from {@link readSessionOwners}, so a screen can
206
+ * resolve a whole listing from one read instead of one per row.
207
+ * @returns `mine` for a self-held session, `occupied` for a foreign one.
208
+ */
209
+ export function occupancyOf(sessionId, owners) {
210
+ const owner = owners.get(sessionId);
211
+ if (owner === undefined)
212
+ return EMPTY;
213
+ if (ownerIsSelf(owner))
214
+ return { kind: 'mine' };
215
+ return { kind: 'occupied', pid: owner.pid };
216
+ }
217
+ /** The pid recorded at the head of a lock token, or undefined when unreadable. */
218
+ function readLockHolderPid(lockPath) {
219
+ let fd;
220
+ try {
221
+ fd = openSync(lockPath, 'r');
222
+ }
223
+ catch {
224
+ return undefined;
225
+ }
226
+ try {
227
+ const buffer = Buffer.alloc(64);
228
+ const read = readSync(fd, buffer, 0, buffer.length, 0);
229
+ const token = buffer.subarray(0, read).toString('utf8').trim();
230
+ const separator = token.indexOf('-');
231
+ const pid = Number(separator < 0 ? token : token.slice(0, separator));
232
+ return Number.isInteger(pid) && pid > 0 ? pid : undefined;
233
+ }
234
+ catch {
235
+ return undefined;
236
+ }
237
+ finally {
238
+ try {
239
+ closeSync(fd);
240
+ }
241
+ catch {
242
+ // Losing the descriptor is not a reason to report a different verdict.
243
+ }
244
+ }
245
+ }
246
+ /**
247
+ * Whether an existing lock may be removed, and the peer that left it is not
248
+ * coming back.
249
+ *
250
+ * A readable holder pid is the whole verdict: dead holder, reclaim; live
251
+ * holder, leave it alone however old the file is. Only a lock whose token
252
+ * could not be read (the window between `open` and `write`, or a crash inside
253
+ * it) falls back to age.
254
+ */
255
+ function lockIsReclaimable(lockPath) {
256
+ const holderPid = readLockHolderPid(lockPath);
257
+ if (holderPid !== undefined)
258
+ return !pidAlive(holderPid);
259
+ try {
260
+ return Date.now() - statSync(lockPath).mtimeMs > STALE_LOCK_MS;
261
+ }
262
+ catch {
263
+ return false;
264
+ }
265
+ }
266
+ /** Take the short cross-process lock, or report why another writer kept it. */
267
+ function acquireLock() {
268
+ try {
269
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
270
+ }
271
+ catch (error) {
272
+ return {
273
+ ok: false,
274
+ reason: 'unavailable',
275
+ detail: `cannot create ${DATA_DIR}: ${error instanceof Error ? error.message : String(error)}`,
276
+ };
277
+ }
278
+ const lockPath = join(DATA_DIR, LOCK_FILE);
279
+ for (let attempt = 0; attempt < 2; attempt++) {
280
+ let fd;
281
+ try {
282
+ fd = openSync(lockPath, 'wx', 0o600);
283
+ }
284
+ catch (error) {
285
+ if (!hasCode(error, 'EEXIST')) {
286
+ return {
287
+ ok: false,
288
+ reason: 'unavailable',
289
+ detail: `cannot create ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,
290
+ };
291
+ }
292
+ if (attempt > 0)
293
+ return { ok: false, reason: 'busy' };
294
+ try {
295
+ if (!lockIsReclaimable(lockPath))
296
+ return { ok: false, reason: 'busy' };
297
+ rmSync(lockPath, { force: true });
298
+ }
299
+ catch {
300
+ // Racing a peer that just released (or re-took) the lock is ordinary.
301
+ return { ok: false, reason: 'busy' };
302
+ }
303
+ continue;
304
+ }
305
+ const token = `${process.pid}-${randomBytes(6).toString('hex')}`;
306
+ try {
307
+ // `writeSync` on the descriptor (not `writeFileSync`) so the token lands
308
+ // in the file this fd owns, and flush before anyone can read it.
309
+ writeSync(fd, `${token}\n`);
310
+ return { ok: true, lock: { fd, path: lockPath, token } };
311
+ }
312
+ catch (error) {
313
+ try {
314
+ closeSync(fd);
315
+ }
316
+ catch {
317
+ // The lock name is removed below regardless.
318
+ }
319
+ try {
320
+ rmSync(lockPath, { force: true });
321
+ }
322
+ catch {
323
+ // A stale lock is recoverable on the next mutation.
324
+ }
325
+ return {
326
+ ok: false,
327
+ reason: 'unavailable',
328
+ detail: `cannot write ${lockPath}: ${error instanceof Error ? error.message : String(error)}`,
329
+ };
330
+ }
331
+ }
332
+ return { ok: false, reason: 'busy' };
333
+ }
334
+ /**
335
+ * Whether this holder's lock is still the one on disk. False means the lock was
336
+ * reclaimed (or replaced), so the holder must NOT write and must NOT delete the
337
+ * file — it belongs to somebody else now.
338
+ */
339
+ function lockIsHeld(lock) {
340
+ try {
341
+ const fd = openSync(lock.path, 'r');
342
+ try {
343
+ const buffer = Buffer.alloc(256);
344
+ const read = readSync(fd, buffer, 0, buffer.length, 0);
345
+ return buffer.subarray(0, read).toString('utf8').trim() === lock.token;
346
+ }
347
+ finally {
348
+ closeSync(fd);
349
+ }
350
+ }
351
+ catch {
352
+ return false;
353
+ }
354
+ }
355
+ /** Release the lock taken by {@link acquireLock}, if it is still ours. */
356
+ function releaseLock(lock) {
357
+ try {
358
+ closeSync(lock.fd);
359
+ }
360
+ catch {
361
+ // Removing the name below is what actually frees the lock.
362
+ }
363
+ if (!lockIsHeld(lock))
364
+ return;
365
+ try {
366
+ rmSync(lock.path, { force: true });
367
+ }
368
+ catch {
369
+ // A stale lock is reclaimable on the next mutation.
370
+ }
371
+ }
372
+ /**
373
+ * Atomically replace the ledger with the live foreign records plus `mine`.
374
+ *
375
+ * A random-suffixed sibling plus a rename means a reader never observes a
376
+ * half-written document, and a crash mid-write leaves the previous ledger
377
+ * intact. CALLERS MUST HOLD THE LOCK: the replacement is rebuilt from a read
378
+ * taken while holding it, so a peer that published since the caller's last read
379
+ * survives, and dead owners are dropped here — the write path is the only place
380
+ * that prunes.
381
+ *
382
+ * The read is the STRICT one. Rebuilding from a best-effort read would let a
383
+ * damaged ledger be "repaired" into whatever this process happened to parse,
384
+ * silently dropping the claims that failed to parse — including a peer's only
385
+ * declaration. A ledger that cannot be read is left exactly as it is.
386
+ * @param mine - This process's record; undefined clears our own record.
387
+ * @returns Whether the ledger was replaced, and why not when it was not.
388
+ */
389
+ function writeLedger(mine) {
390
+ const target = join(DATA_DIR, MOUNTS_FILE);
391
+ // The timestamp is part of the name, not decoration: a crash can leave a
392
+ // temp file behind, and a reused pid would then collide with `<pid>.<seq>`
393
+ // and fail the `wx` create.
394
+ const temporary = `${target}.${process.pid}.${Date.now()}.${temporarySequence++}.tmp`;
395
+ const existing = readMountLedgerStrict();
396
+ if (!existing.ok)
397
+ return { ok: false, reason: 'unavailable', detail: existing.detail };
398
+ try {
399
+ mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
400
+ const records = existing.owners.filter(owner => !ownerIsSelf(owner) && pidAlive(owner.pid));
401
+ if (mine !== undefined)
402
+ records.push(mine);
403
+ writeFileSync(temporary, JSON.stringify({ version: MOUNTS_VERSION, owners: records }, null, 2), { encoding: 'utf8', flag: 'wx', mode: 0o600 });
404
+ renameSync(temporary, target);
405
+ return { ok: true };
406
+ }
407
+ catch (error) {
408
+ try {
409
+ rmSync(temporary, { force: true });
410
+ }
411
+ catch {
412
+ // The previous ledger is still intact; nothing else is safe to do.
413
+ }
414
+ return {
415
+ ok: false,
416
+ reason: 'unavailable',
417
+ detail: `cannot replace ${target}: ${error instanceof Error ? error.message : String(error)}`,
418
+ };
419
+ }
420
+ }
421
+ /** This process's own record, as last published. */
422
+ let ownStartedAt;
423
+ /** Session ids this process currently has mounted. */
424
+ const ownSessionIds = new Set();
425
+ /** This process's record, or undefined before the first publish. */
426
+ function ownRecord() {
427
+ return { pid: process.pid, startedAt: ownStartedAt ?? Date.now(), sessionIds: [...ownSessionIds] };
428
+ }
429
+ /**
430
+ * Sessions with an in-flight mount operation, keyed to the operations holding
431
+ * them. A claim is committed to the ledger BEFORE the agent it stands for
432
+ * exists in the registry (a resume reads preset, route and workspace first),
433
+ * so for that window the claim has no roster entry backing it — and a publisher
434
+ * beat that rebuilt the published set from the roster alone would erase it,
435
+ * handing the session to whatever peer asks next.
436
+ */
437
+ const mountOperations = new Map();
438
+ /**
439
+ * Pin `sessionId` for the duration of one mount operation, so a concurrent
440
+ * {@link publishMounts} beat cannot drop its claim, and so the session counts
441
+ * as held until the caller has finished closing whatever it opened.
442
+ *
443
+ * The token identifies THIS operation only: it keeps a stale operation's
444
+ * cleanup from deleting a newer operation's pin. It is not a cross-process
445
+ * fencing token and does not replace the ledger. Every caller gets one through
446
+ * {@link reserveMount} or {@link reserveNewSession}.
447
+ * @param sessionId - Session the operation is about to mount.
448
+ * @returns An idempotent release for the pin.
449
+ */
450
+ function holdMountOperation(sessionId) {
451
+ const token = Symbol('session-mount-operation');
452
+ let tokens = mountOperations.get(sessionId);
453
+ if (tokens === undefined) {
454
+ tokens = new Set();
455
+ mountOperations.set(sessionId, tokens);
456
+ }
457
+ tokens.add(token);
458
+ let released = false;
459
+ return () => {
460
+ if (released)
461
+ return;
462
+ released = true;
463
+ const current = mountOperations.get(sessionId);
464
+ if (current === undefined)
465
+ return;
466
+ current.delete(token);
467
+ if (current.size === 0)
468
+ mountOperations.delete(sessionId);
469
+ };
470
+ }
471
+ /**
472
+ * Publish this process's mounted set. Call at boot, whenever the set changes,
473
+ * and just before teardown, so a peer always reads a current answer without a
474
+ * timer keeping it fresh.
475
+ *
476
+ * The argument is the set the caller can OBSERVE (the live agent roster). Any
477
+ * session with an in-flight operation is added on top, because a claim is
478
+ * published before its agent exists: without that union a beat landing inside
479
+ * a resume would delete the reservation this process just committed.
480
+ * @param sessionIds - The sessions currently mounted (replaces the prior set).
481
+ * @returns True when the ledger was updated.
482
+ */
483
+ export function publishMounts(sessionIds) {
484
+ const now = Date.now();
485
+ // Materialize the argument BEFORE clearing the live set. A caller may pass
486
+ // `ownSessionIds` (or a view of it) to re-publish the current claim with one
487
+ // change, and clearing first would then erase the input as it is read.
488
+ const requested = [...sessionIds];
489
+ ownSessionIds.clear();
490
+ for (const sessionId of requested) {
491
+ if (typeof sessionId === 'string' && sessionId.length > 0)
492
+ ownSessionIds.add(sessionId);
493
+ }
494
+ for (const sessionId of mountOperations.keys())
495
+ ownSessionIds.add(sessionId);
496
+ if (ownStartedAt === undefined)
497
+ ownStartedAt = now;
498
+ const attempt = acquireLock();
499
+ if (!attempt.ok)
500
+ return false;
501
+ const lock = attempt.lock;
502
+ try {
503
+ // A lock reclaimed by a peer belongs to somebody else now: this process's
504
+ // snapshot is older than theirs, so it must not be committed over it.
505
+ if (!lockIsHeld(lock))
506
+ return false;
507
+ return writeLedger(ownRecord()).ok;
508
+ }
509
+ finally {
510
+ releaseLock(lock);
511
+ }
512
+ }
513
+ /**
514
+ * Claim one session for THIS process, atomically.
515
+ *
516
+ * The occupancy check and the claim have to be one lock-protected step: two
517
+ * processes that each check first and publish second can both observe `free`
518
+ * and both publish, which is exactly the state this ledger exists to prevent.
519
+ * Inside the lock the conflict is re-derived from the file, and a session that
520
+ * is genuinely ours is accepted — a second mount of our own parked handle is
521
+ * not a conflict. When a live PEER holds it, nothing is written.
522
+ *
523
+ * A refusal that cannot name a peer is reported as `busy` (the lock was taken)
524
+ * or `unavailable` (the ledger could not be read or replaced) — never as an
525
+ * empty holder list, because callers have to be able to tell "somebody else
526
+ * has it" from "we could not find out".
527
+ * @param sessionId - Session id to claim.
528
+ * @returns `ok`, or why the session is not ours.
529
+ */
530
+ export function claimMount(sessionId) {
531
+ const attempt = acquireLock();
532
+ if (!attempt.ok) {
533
+ return attempt.reason === 'busy'
534
+ ? { ok: false, reason: 'busy' }
535
+ : { ok: false, reason: 'unavailable', detail: attempt.detail };
536
+ }
537
+ const lock = attempt.lock;
538
+ try {
539
+ // Lost the lock (reclaimed by a peer): the file on disk is no longer the one
540
+ // this claim was derived from, so refuse rather than write a stale merge.
541
+ if (!lockIsHeld(lock))
542
+ return { ok: false, reason: 'busy' };
543
+ const read = readMountLedgerStrict();
544
+ if (!read.ok)
545
+ return { ok: false, reason: 'unavailable', detail: read.detail };
546
+ const holders = [];
547
+ for (const owner of read.owners) {
548
+ if (ownerIsSelf(owner) || !pidAlive(owner.pid))
549
+ continue;
550
+ if (owner.sessionIds.includes(sessionId))
551
+ holders.push(owner.pid);
552
+ }
553
+ if (holders.length > 0)
554
+ return { ok: false, reason: 'occupied', holders };
555
+ // Whether THIS claim is what publishes the session: a session already in
556
+ // our set may have been put there by a parked handle or a publisher beat,
557
+ // and a caller that later gives its reservation back must not take it away.
558
+ const fresh = !ownSessionIds.has(sessionId);
559
+ ownSessionIds.add(sessionId);
560
+ if (ownStartedAt === undefined)
561
+ ownStartedAt = Date.now();
562
+ const written = writeLedger(ownRecord());
563
+ if (!written.ok) {
564
+ // The claim was not persisted, so it must not be held in memory either:
565
+ // the process must not believe it owns what the ledger does not record.
566
+ ownSessionIds.delete(sessionId);
567
+ return written;
568
+ }
569
+ return { ok: true, fresh };
570
+ }
571
+ finally {
572
+ releaseLock(lock);
573
+ }
574
+ }
575
+ /**
576
+ * Drop one session from this process's published set. Used when a session is
577
+ * explicitly closed; an ordinary session switch KEEPS its session mounted
578
+ * (that is the point of parking), so most switches do not call this.
579
+ * @param sessionId - Session id to release.
580
+ * @returns True when the ledger was updated.
581
+ */
582
+ export function releaseMount(sessionId) {
583
+ if (!ownSessionIds.delete(sessionId))
584
+ return true;
585
+ // Snapshot before republishing: `publishMounts` clears `ownSessionIds` before
586
+ // it reads its argument, so handing it the live Set would empty the very
587
+ // claim being rebuilt and release every session this process holds.
588
+ return publishMounts([...ownSessionIds]);
589
+ }
590
+ /**
591
+ * Remove this process's record entirely, so every session it held is
592
+ * immediately mountable by another TUI. Called from the teardown funnel; a
593
+ * crashed process skips this and its record is pruned by the next write
594
+ * instead, which is why both paths must work.
595
+ * @returns True when the ledger was updated.
596
+ */
597
+ export function clearOwnMounts() {
598
+ const attempt = acquireLock();
599
+ if (!attempt.ok)
600
+ return false;
601
+ const lock = attempt.lock;
602
+ try {
603
+ if (!lockIsHeld(lock))
604
+ return false;
605
+ ownSessionIds.clear();
606
+ mountOperations.clear();
607
+ ownStartedAt = undefined;
608
+ return writeLedger(undefined).ok;
609
+ }
610
+ finally {
611
+ releaseLock(lock);
612
+ }
613
+ }
614
+ /**
615
+ * {@link reserveMount} for a session id this process just minted, where a
616
+ * refusal is a loss of ANNOUNCEMENT rather than a conflict.
617
+ *
618
+ * A brand-new id cannot be held by anybody, so a create must not be blocked by
619
+ * a ledger it cannot write — but it still has to be announced before the
620
+ * factory runs, because from the moment the factory returns this process owns
621
+ * the only write handle and the publisher will not name the session until its
622
+ * next beat. A refusal therefore yields a no-op reservation and the caller
623
+ * carries on; `failure` reports what happened so the caller can say so.
624
+ * @param sessionId - Freshly minted session id.
625
+ * @returns A reservation (never a refusal) plus the failure, if any.
626
+ */
627
+ export async function reserveNewSession(sessionId) {
628
+ const reserved = await reserveMount(sessionId);
629
+ if (reserved.ok)
630
+ return { reservation: reserved.reservation };
631
+ return { reservation: { settle: () => { }, abandon: () => { } }, failure: reserved };
632
+ }
633
+ /**
634
+ * The sessions this process has published, for the diagnostics surface.
635
+ * @returns The ids last handed to {@link publishMounts}.
636
+ */
637
+ export function ownMounts() {
638
+ return [...ownSessionIds];
639
+ }
640
+ /**
641
+ * Backoff schedule for a lock a peer is holding right now. The lock only spans
642
+ * one read-and-replace of a small file, so a few hundred milliseconds covers
643
+ * every ordinary collision; this is an interaction budget, not a correctness
644
+ * condition — the claim itself is what decides, every time it is retried.
645
+ */
646
+ const CLAIM_RETRY_DELAYS_MS = [25, 50, 100, 200, 400];
647
+ /**
648
+ * {@link claimMount} with a bounded retry for `busy`.
649
+ *
650
+ * `busy` means a peer held the ledger lock for the instant we wanted it, which
651
+ * says nothing about occupancy and is worth waiting out. `occupied` and
652
+ * `unavailable` are answers, not collisions, and are returned immediately.
653
+ * @param sessionId - Session id to claim.
654
+ * @returns The final claim, `busy` only when the whole budget was spent.
655
+ */
656
+ export async function claimMountWithRetry(sessionId) {
657
+ let claim = claimMount(sessionId);
658
+ for (const delay of CLAIM_RETRY_DELAYS_MS) {
659
+ if (claim.ok || claim.reason !== 'busy')
660
+ return claim;
661
+ await new Promise(resolve => setTimeout(resolve, delay));
662
+ claim = claimMount(sessionId);
663
+ }
664
+ return claim;
665
+ }
666
+ /**
667
+ * Claim `sessionId` and pin it for the WHOLE mount attempt, not just the claim
668
+ * call.
669
+ *
670
+ * Everything that opens a writer goes through here, so that the rules live in
671
+ * one place: the claim is atomic (nothing is opened on a refusal), the pin
672
+ * survives a publisher beat while the agent does not exist yet, and the
673
+ * reservation ends explicitly — `settle` on a commit, `abandon` on every path
674
+ * that does not commit.
675
+ * @param sessionId - Session about to be mounted.
676
+ * @returns The reservation, or the {@link MountFailure} to report back.
677
+ */
678
+ export async function reserveMount(sessionId) {
679
+ const claim = await claimMountWithRetry(sessionId);
680
+ if (!claim.ok)
681
+ return claim;
682
+ const releaseOperation = holdMountOperation(sessionId);
683
+ let settled = false;
684
+ return {
685
+ ok: true,
686
+ reservation: {
687
+ settle: () => {
688
+ if (settled)
689
+ return;
690
+ settled = true;
691
+ releaseOperation();
692
+ },
693
+ abandon: () => {
694
+ if (settled)
695
+ return;
696
+ settled = true;
697
+ releaseOperation();
698
+ // Only give back what this reservation took. A claim that inherited the
699
+ // session (already ours) must not release it: this process was driving
700
+ // that log before the attempt and still is.
701
+ if (claim.fresh)
702
+ releaseMount(sessionId);
703
+ },
704
+ },
705
+ };
706
+ }