@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
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import type { ChannelUi as Channel } from '../adapter/channel/ui-policy.js';
3
3
  import type { ComposerImageRef } from '../dsh-adapter/channel.js';
4
4
  import type { TranscriptImage } from '../dsh-adapter/transcript-images.js';
5
+ import { type PromptDraftCache } from './promptDraftCache.js';
5
6
  /** Capabilities referenced by `text`, in first occurrence order. A raw token
6
7
  * restored from disk/history has no sidecar entry and therefore stays inert. */
7
8
  export declare function composerImageRefsForText(text: string, stagedByToken: ReadonlyMap<string, string>): ComposerImageRef[];
@@ -13,6 +14,8 @@ export declare function composerImageRefsForText(text: string, stagedByToken: Re
13
14
  */
14
15
  export interface PromptController {
15
16
  hasText(): boolean;
17
+ /** The draft text, for a caller that must not lose it (see {@link PromptDraftCache}). */
18
+ text(): string;
16
19
  /** Current capability-backed draft images in token order, without reads. */
17
20
  previewImages?(): readonly {
18
21
  image: TranscriptImage;
@@ -39,10 +42,33 @@ export interface PromptController {
39
42
  * Chat's working-turn Esc interrupt must yield in BOTH submodes. */
40
43
  vimActive(): boolean;
41
44
  }
45
+ /**
46
+ * Owner-held slot for the composer draft, so a screen that unmounts the
47
+ * prompt (every early return in Chat) does not discard what the user typed.
48
+ *
49
+ * The composer writes ONE complete snapshot as it unmounts and consumes it on
50
+ * the way back; the shape and the reasoning live in `promptDraftCache.ts`.
51
+ */
42
52
  export interface PromptInputProps {
43
53
  channel: Channel;
44
54
  /** Keep the draft mounted while another prompt-slot panel owns the UI. */
45
55
  suspended?: boolean;
56
+ /**
57
+ * Owner-held slot for the unsent draft.
58
+ *
59
+ * The prompt owns its text in local state, and several screens REPLACE the
60
+ * conversation (the session screen, the session tree, settings, the jobs and
61
+ * subagent panels, the trajectory scene) — early returns that unmount this
62
+ * component and would take a half-written prompt down with it. Chat owns the
63
+ * slot, so the text, its caret AND its image bindings survive that unmount
64
+ * and come back when the composer does. The owner also drops it on a session
65
+ * change, so a draft can never leak into a different conversation.
66
+ *
67
+ * Nothing is written from render: a commit-time assignment would run before
68
+ * the restore effect has read the slot and overwrite the draft with the
69
+ * empty first value.
70
+ */
71
+ draftCache?: PromptDraftCache;
46
72
  /** Whether the `?` help menu is open (state lives in the Chat screen). */
47
73
  helpOpen: boolean;
48
74
  onToggleHelp(): void;
@@ -67,6 +93,16 @@ export interface PromptInputProps {
67
93
  * opens the agent view (with text, ← moves the caret as usual).
68
94
  */
69
95
  onBackgroundRequest?(): void;
96
+ /**
97
+ * Open the session screen (the one `/resume`, `/agentview` and `/home`
98
+ * share) from the `⌂` entry at the head of the input row.
99
+ *
100
+ * Optional on purpose: the entry is rendered ONLY when this is provided, so
101
+ * hosts that mount the prompt without a session screen — and the layout
102
+ * regressions that pin this row's column budget — keep exactly the row they
103
+ * had before.
104
+ */
105
+ onOpenSessions?(): void;
70
106
  /**
71
107
  * Background sessions waiting on the user (agent view "needs input" rows
72
108
  * excluding this session); the prompt footer shows the
@@ -123,5 +159,5 @@ export interface PromptInputProps {
123
159
  * working) interrupts the turn and delivers them right away; Ctrl+Enter
124
160
  * aborts the turn and sends the current input immediately.
125
161
  */
126
- export declare function PromptInput({ channel, suspended, helpOpen, onToggleHelp, onRunCommand, selectionActive, fillText, onFillConsumed, onRewindRequest, onBackgroundRequest, backgroundAgentsNeedingInput, controllerRef, onCaretImage, caretPreviewOpen, onDismissCaretPreview, }: PromptInputProps): React.JSX.Element | null;
162
+ export declare function PromptInput({ channel, suspended, draftCache, helpOpen, onToggleHelp, onRunCommand, selectionActive, fillText, onFillConsumed, onRewindRequest, onBackgroundRequest, onOpenSessions, backgroundAgentsNeedingInput, controllerRef, onCaretImage, caretPreviewOpen, onDismissCaretPreview, }: PromptInputProps): React.JSX.Element | null;
127
163
  //# sourceMappingURL=PromptInput.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAGjB,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAgH1E;gFACgF;AAChF,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACzC,gBAAgB,EAAE,CAWpB;AA+MD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,4EAA4E;IAC5E,aAAa,CAAC,IAAI,SAAS;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtE,KAAK,IAAI,IAAI,CAAA;IACb;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO,CAAA;IAC/B,2EAA2E;IAC3E,SAAS,IAAI,OAAO,CAAA;IACpB;yEACqE;IACrE,SAAS,IAAI,OAAO,CAAA;CAAC;AAEvB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAClC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7B,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,kEAAkE;IAClE,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB;;;OAGG;IACH,mBAAmB,CAAC,IAAI,IAAI,CAAA;IAC5B;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAA;IACrC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACxD;;;;;;;OAOG;IACH,YAAY,CAAC,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAA;IAC7G;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,qBAAqB,CAAC,IAAI,IAAI,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,SAAiB,EACjB,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,4BAA4B,EAC5B,aAAa,EACb,YAAY,EACZ,gBAAwB,EACxB,qBAAqB,GACtB,EAAE,gBAAgB,4BA6hGlB"}
1
+ {"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,KAAK,EACV,gBAAgB,EAGjB,MAAM,2BAA2B,CAAA;AAClC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAA;AAY1E,OAAO,EAIL,KAAK,gBAAgB,EAEtB,MAAM,uBAAuB,CAAA;AA8H9B;gFACgF;AAChF,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACzC,gBAAgB,EAAE,CAWpB;AA+MD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,yFAAyF;IACzF,IAAI,IAAI,MAAM,CAAA;IACd,4EAA4E;IAC5E,aAAa,CAAC,IAAI,SAAS;QAAE,KAAK,EAAE,eAAe,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACtE,KAAK,IAAI,IAAI,CAAA;IACb;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;IAC5B;;;;;OAKG;IACH,oBAAoB,IAAI,OAAO,CAAA;IAC/B,2EAA2E;IAC3E,SAAS,IAAI,OAAO,CAAA;IACpB;yEACqE;IACrE,SAAS,IAAI,OAAO,CAAA;CAAC;AAEvB;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CACV,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,SAAS,gBAAgB,EAAE,GAClC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC7B,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,kEAAkE;IAClE,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB;;;OAGG;IACH,mBAAmB,CAAC,IAAI,IAAI,CAAA;IAC5B;;;;;;;;OAQG;IACH,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB;;;;OAIG;IACH,4BAA4B,CAAC,EAAE,MAAM,CAAA;IACrC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IACxD;;;;;;;OAOG;IACH,YAAY,CAAC,CAAC,KAAK,EAAE,eAAe,GAAG,SAAS,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAA;IAC7G;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,qBAAqB,CAAC,IAAI,IAAI,CAAA;CAC/B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,SAAiB,EACjB,UAAU,EACV,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,mBAAmB,EACnB,cAAc,EACd,4BAA4B,EAC5B,aAAa,EACb,YAAY,EACZ,gBAAwB,EACxB,qBAAqB,GACtB,EAAE,gBAAgB,4BAivGlB"}
@@ -34,7 +34,26 @@ import { FileSuggestions } from './FileSuggestions.js';
34
34
  import { HelpMenu } from './HelpMenu.js';
35
35
  import { OverlayAbove } from './OverlayAbove.js';
36
36
  import { SuggestionCard, cardContentWidth } from './SuggestionCard.js';
37
+ import { filterLiveImageBindings, isUsableDraftSnapshot, resolveBindingGeneration, } from './promptDraftCache.js';
37
38
  const HISTORY_LIMIT = 50;
39
+ /**
40
+ * Visible text of the session-entry control at the head of the input row:
41
+ * U+2338 APL FUNCTIONAL SYMBOL QUAD COLON plus a separator space.
42
+ *
43
+ * The glyph is chosen for ONE property above all others: the layout model and
44
+ * the terminal cell table must agree on how many columns it occupies. `\u2338`
45
+ * is a plain single-cell symbol with no emoji presentation, so
46
+ * `stringWidth` and the terminal both say 1 + 1 = 2 and
47
+ * {@link HOME_BUTTON_COLS} can be derived rather than guessed.
48
+ *
49
+ * The emoji house (`U+1F3E0`) was tried and rejected on this exact ground: it
50
+ * costs the terminal ONE cell while `stringWidth` charges the row TWO, and
51
+ * appending VARIATION SELECTOR-15 (text presentation) does not change either
52
+ * number. That standing one-column disagreement put every caret-relative
53
+ * column in the composer off by one, so no offset compensation made it work.
54
+ */
55
+ const HOME_BUTTON_TEXT = '⌸ ';
56
+ const HOME_BUTTON_COLS = stringWidth(HOME_BUTTON_TEXT);
38
57
  /**
39
58
  * Paste fold with a visible preview and no black box:
40
59
  * a paste that leaves the input this big folds into a one-line chip
@@ -67,6 +86,9 @@ function sanitizeEditableText(text) {
67
86
  .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, '');
68
87
  }
69
88
  const COMPOSER_IMAGE_TOKEN = /\[Image #\d+\]/gu;
89
+ /** Format label for one image media type, matching the image preview card's
90
+ * title (`JPEG`, `PNG`, `WEBP`, `GIF`). */
91
+ const mediaTypeLabel = (mediaType) => mediaType.replace(/^image\//u, '').replace(/\+xml$/u, '').toUpperCase();
70
92
  /** Every `[Image #N]` in `text`, in order. */
71
93
  function imageTokenSpans(text) {
72
94
  const spans = [];
@@ -352,13 +374,70 @@ const DOUBLE_CLICK_MS = 500;
352
374
  * working) interrupts the turn and delivers them right away; Ctrl+Enter
353
375
  * aborts the turn and sends the current input immediately.
354
376
  */
355
- export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp, onRunCommand, selectionActive, fillText, onFillConsumed, onRewindRequest, onBackgroundRequest, backgroundAgentsNeedingInput, controllerRef, onCaretImage, caretPreviewOpen = false, onDismissCaretPreview, }) {
377
+ export function PromptInput({ channel, suspended = false, draftCache, helpOpen, onToggleHelp, onRunCommand, selectionActive, fillText, onFillConsumed, onRewindRequest, onBackgroundRequest, onOpenSessions, backgroundAgentsNeedingInput, controllerRef, onCaretImage, caretPreviewOpen = false, onDismissCaretPreview, }) {
356
378
  const [themeName] = useTheme();
357
379
  // Raw stdout writer for OSC 52 clipboard writes (selection copy) — must
358
380
  // bypass the frame pipeline; null outside a mounted Ink App.
359
381
  const writeRaw = React.useContext(TerminalWriteContext);
382
+ // The composer owns its text in local state, so it starts EMPTY and adopts
383
+ // whatever draft the owner stored in a MOUNT-TIME effect below. Reading the
384
+ // store in this initializer instead looks equivalent and is not: a non-empty
385
+ // first frame leaves `repro-resume-position` red — resuming a session parks
386
+ // the transcript mid-history instead of pinning it to the newest message.
387
+ // Adopting after mount keeps the first frame identical to a fresh composer
388
+ // while still handing the draft back.
360
389
  const [value, setValue] = React.useState('');
361
390
  const [cursor, setCursor] = React.useState(0);
391
+ /**
392
+ * Adopt the owner's draft ONCE, after mount.
393
+ *
394
+ * This is where a screen swap gives the draft back: the slot outlives this
395
+ * component, so a remount picks up what the user had written — text, caret,
396
+ * the image bindings behind the visible `[Image #N]` tokens, and the edit
397
+ * state around them (fold chip, fullscreen editor, vim mode).
398
+ *
399
+ * It runs as an effect rather than as the `useState` initial value on
400
+ * purpose. A composer whose FIRST frame is already non-empty moves the
401
+ * transcript's restored scroll position (`repro-resume-position`); adopting
402
+ * after the first commit keeps that frame identical to a fresh composer. The
403
+ * slot is consumed here, and the composer never writes to it while mounted,
404
+ * so the empty first value cannot overwrite the draft before this reads it.
405
+ */
406
+ const adoptDraft = React.useRef(true);
407
+ React.useEffect(() => {
408
+ if (draftCache === undefined || !adoptDraft.current)
409
+ return;
410
+ adoptDraft.current = false;
411
+ const snapshot = draftCache.current;
412
+ draftCache.current = null;
413
+ if (!isUsableDraftSnapshot(snapshot, String(channel.agentId), resolveBindingGeneration(channel)))
414
+ return;
415
+ const text = snapshot.value;
416
+ // Carried edit state, restored ahead of the text: the chip, the
417
+ // fullscreen editor and the vim mode/submode come back with the draft —
418
+ // and they come back even with NO text, because they are modes rather
419
+ // than content. Fold ranges are safe by the CAPTURE invariant, not by
420
+ // restore order: a snapshot's block, when set, always sits inside the
421
+ // snapshot's own text (setInput keeps or drops it atomically), and at
422
+ // mount the caret is 0 so updateFoldBlock's caret-drag clamp cannot fire
423
+ // here. The transient state around them — editor scroll, vim undo stack,
424
+ // selection — does not come back.
425
+ updateFoldBlock(snapshot.foldBlock);
426
+ expandedRef.current = snapshot.expanded;
427
+ setExpanded(snapshot.expanded);
428
+ vimEnabledRef.current = snapshot.vimEnabled;
429
+ setVimEnabled(snapshot.vimEnabled);
430
+ vimInsertRef.current = snapshot.vimInsert;
431
+ setVimInsert(snapshot.vimInsert);
432
+ if (text === '')
433
+ return;
434
+ const restoredCursor = normalizeCursorOffset(text, snapshot.cursor);
435
+ replaceDraftImages(filterLiveImageBindings(snapshot.images, stageId => channel.hasStagedImage?.(stageId) === true).map(([token, stageId]) => ({ token, stageId })));
436
+ valueRef.current = text;
437
+ cursorRef.current = restoredCursor;
438
+ setValue(text);
439
+ setCursor(restoredCursor);
440
+ }, [draftCache]);
362
441
  /**
363
442
  * Mouse text selection: UTF-16 offsets [start, end) in `value`, snapped
364
443
  * to grapheme boundaries, start ≤ end. Null = no selection. Created by
@@ -403,7 +482,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
403
482
  const foldBlockRef = React.useRef(null);
404
483
  /**
405
484
  * Fullscreen draft editor (`expandEditor`, default Ctrl+Shift+E, or the
406
- * ⛶ affordance at the end of the input row). While expanded the SAME
485
+ * ✎ affordance at the end of the input row). While expanded the SAME
407
486
  * editing state renders into the PromptEditorLayer cover (published via
408
487
  * setPromptEditorNode each render): Enter inserts a newline, Ctrl+Enter
409
488
  * submits, Esc collapses. The fold chip is bypassed (full text shown).
@@ -424,14 +503,16 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
424
503
  const prevExpandedRef = React.useRef(false);
425
504
  /**
426
505
  * Feature gate (settings `dsh-tui.expandEditor`, on by default; a mock
427
- * channel without the field also reads as on). Off hides the ⛶
506
+ * channel without the field also reads as on). Off hides the ✎
428
507
  * affordance and refuses the shortcut — the editor cannot open.
429
508
  */
430
509
  const expandEnabled = channel.expandEditor !== false;
431
510
  /** Latest expanded viewport metrics for the useInput wheel branch. */
432
511
  const editorViewportRef = React.useRef(null);
433
- /** Hover state of the ⤢/⛶ expand affordance in the input row. */
512
+ /** Hover state of the ⤢/✎ expand affordance in the input row. */
434
513
  const [expandHovered, setExpandHovered] = React.useState(false);
514
+ /** Hover state of the ⌂ session-list affordance at the head of the row. */
515
+ const [homeHovered, setHomeHovered] = React.useState(false);
435
516
  /** Pointer over the input box (drives the hover peek card). */
436
517
  const [hovered, setHovered] = React.useState(false);
437
518
  /** 120ms grace so the pointer crossing the input border row from the
@@ -550,6 +631,10 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
550
631
  syncImageGeneration();
551
632
  valueRef.current = value;
552
633
  cursorRef.current = cursor;
634
+ // Nothing is written to the owner's draft slot here — see the unmount
635
+ // hand-off below. A commit-time write would run BEFORE the restore effect
636
+ // has consumed the slot, so the composer's own first (empty) value would
637
+ // erase the draft it just came back for.
553
638
  // Publish the live controller (fresh closure over `value` every render).
554
639
  // A prompt-slot panel withdraws the handle in the same commit: external
555
640
  // injection must not append/submit a hidden command draft while it waits
@@ -563,6 +648,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
563
648
  }
564
649
  controllerRef.current = {
565
650
  hasText: () => value.length > 0,
651
+ text: () => valueRef.current,
566
652
  previewImages: () => composerImageRefsForText(valueRef.current, draftImagesRef.current).flatMap(ref => {
567
653
  const image = channel.stagedImage(ref.stageId);
568
654
  return image === undefined ? [] : [{ image, title: ref.token.slice(1, -1) }];
@@ -660,9 +746,56 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
660
746
  clearTimeout(escTimerRef.current);
661
747
  if (hoverLeaveTimerRef.current)
662
748
  clearTimeout(hoverLeaveTimerRef.current);
663
- // An async image read/stage may outlive this component. Revoke its
664
- // draft lease so it cannot bind an invisible capability after unmount.
665
- discardDraftImages();
749
+ /**
750
+ * Hand the draft to the owner's slot as this component goes away.
751
+ *
752
+ * This is the ONLY write to that slot, and it belongs here: it happens at
753
+ * the instant a screen replaces the composer, so it can neither race the
754
+ * restore effect nor change the transcript's first frame.
755
+ *
756
+ * `advanceDraftRevision` invalidates any image read/stage still in
757
+ * flight, so a late continuation cannot bind a capability into a composer
758
+ * that no longer exists. `clearVimUndo` releases the capabilities that
759
+ * only the undo stack was holding: they are not part of the draft, and
760
+ * nothing will ever restore them once this composer is gone.
761
+ *
762
+ * The stageIds the DRAFT holds are deliberately NOT revoked — they are
763
+ * part of the snapshot the slot now keeps, and the restore filters out
764
+ * whatever the channel revoked in the meantime. Revoking them here is why
765
+ * an image draft used to come back as inert text.
766
+ */
767
+ advanceDraftRevision();
768
+ clearVimUndo();
769
+ const images = [...draftImagesRef.current.entries()]
770
+ .map(([token, stageId]) => [token, stageId]);
771
+ draftImagesRef.current.clear();
772
+ if (draftCache === undefined) {
773
+ discardUnretainedImages(images.map(image => image[1]));
774
+ return;
775
+ }
776
+ const text = valueRef.current;
777
+ // The edit state rides along even with nothing typed: vim mode and the
778
+ // fullscreen editor are MODES the user turned on, not content, and an
779
+ // empty composer must not drop them (`text === ''` alone used to skip
780
+ // the snapshot entirely, losing the vim badge on a round trip with an
781
+ // empty composer).
782
+ const editState = foldBlockRef.current !== null || expandedRef.current
783
+ || vimEnabledRef.current;
784
+ if (text === '' && images.length === 0 && !editState) {
785
+ draftCache.current = null;
786
+ return;
787
+ }
788
+ draftCache.current = {
789
+ ownerAgentId: String(channel.agentId),
790
+ bindingGeneration: resolveBindingGeneration(channel),
791
+ value: text,
792
+ cursor: cursorRef.current,
793
+ foldBlock: foldBlockRef.current,
794
+ expanded: expandedRef.current,
795
+ vimEnabled: vimEnabledRef.current,
796
+ vimInsert: vimInsertRef.current,
797
+ images,
798
+ };
666
799
  };
667
800
  }, []);
668
801
  const { columns, rows: terminalRows } = useTerminalSize();
@@ -768,14 +901,17 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
768
901
  onCaretImageRef.current = onCaretImage;
769
902
  const lastCaretImageRef = React.useRef({ image: undefined, title: undefined });
770
903
  /** Tell the caller which staged image the caret is on. `'caret'` reports
771
- * only changes; `'click'` always reports (see onCaretImage). */
904
+ * only changes; `'click'` always reports (see onCaretImage). "Same image"
905
+ * is the attachment id plus the token, not facade identity: this runs
906
+ * after every commit and the caller stores what it reports, so an
907
+ * identity-only difference would re-render the caller forever (#885). */
772
908
  const reportCaretImage = (reason) => {
773
909
  const report = onCaretImageRef.current;
774
910
  if (report === undefined)
775
911
  return;
776
912
  const found = suspended ? undefined : caretImageAt(valueRef.current, cursorRef.current);
777
913
  const last = lastCaretImageRef.current;
778
- if (reason === 'caret' && last.image === found?.image && last.title === found?.title)
914
+ if (reason === 'caret' && last.image?.id === found?.image.id && last.title === found?.title)
779
915
  return;
780
916
  lastCaretImageRef.current = { image: found?.image, title: found?.title };
781
917
  report(found?.image, found?.title, reason);
@@ -1231,7 +1367,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1231
1367
  if (!tryRunCommand(value))
1232
1368
  submitText(value);
1233
1369
  };
1234
- /** Expand/collapse the fullscreen editor (shortcut + ⛶ affordance).
1370
+ /** Expand/collapse the fullscreen editor (shortcut + ✎ affordance).
1235
1371
  * Expanding also DROPS any fold block: the fullscreen view shows and
1236
1372
  * edits the full text, and the block's atomic-clamp semantics (caret
1237
1373
  * pushed to its edges, selection clamped to one side) would contradict
@@ -1324,6 +1460,35 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1324
1460
  imageStageChainRef.current = queued.then(() => undefined, () => undefined);
1325
1461
  return queued;
1326
1462
  };
1463
+ /** Success notice for one staged image. An adapted paste says what the user
1464
+ * actually got (stored dimensions, stored format, filled alpha) — naming it
1465
+ * is the difference between a reported adaptation and a silently rewritten
1466
+ * image. */
1467
+ const stagedImageNotice = (token, handle) => {
1468
+ const adjustment = handle.adjustment;
1469
+ if (adjustment === undefined)
1470
+ return t('input-image-pasted', { token });
1471
+ const details = [];
1472
+ if (adjustment.resized) {
1473
+ details.push(t('input-image-detail-resized', {
1474
+ width: adjustment.width,
1475
+ height: adjustment.height,
1476
+ }));
1477
+ }
1478
+ if (adjustment.mediaType !== adjustment.sourceMediaType) {
1479
+ const from = mediaTypeLabel(adjustment.sourceMediaType);
1480
+ const to = mediaTypeLabel(adjustment.mediaType);
1481
+ details.push(adjustment.flattened
1482
+ ? t('input-image-detail-converted-flattened', { from, to })
1483
+ : t('input-image-detail-converted', { from, to }));
1484
+ }
1485
+ // An adjustment always describes a resize or a conversion; anything else
1486
+ // keeps the plain notice rather than an empty parenthetical. ' · ' joins
1487
+ // the clauses in both shipped languages.
1488
+ if (details.length === 0)
1489
+ return t('input-image-pasted', { token });
1490
+ return t('input-image-pasted-adjusted', { token, detail: details.join(' · ') });
1491
+ };
1327
1492
  const discardStagedHandles = (handles) => {
1328
1493
  for (const stageId of new Set(handles.map(handle => handle.stageId))) {
1329
1494
  channel.discardStagedImage(stageId);
@@ -1411,7 +1576,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1411
1576
  setFileSelected(0);
1412
1577
  return;
1413
1578
  }
1414
- // ── 全屏草稿编辑(expandEditor,默认 Ctrl+Shift+E / 输入行 ⛶)─────
1579
+ // ── 全屏草稿编辑(expandEditor,默认 Ctrl+Shift+E / 输入行 ✎)─────
1415
1580
  // 展开态拥有屏幕;Esc 收起(有选区时上面的 selection 分支已先行只清
1416
1581
  // 选区)。滚轮不经此——编辑区的 onWheel 位置路由直接驱动滚动窗口。
1417
1582
  if (key.escape && expandedRef.current) {
@@ -1497,7 +1662,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1497
1662
  // Bind and insert share this synchronous continuation: setInput's
1498
1663
  // sidecar pruning can never observe a bound-but-not-visible token.
1499
1664
  insertClipboardAtCaret(`${token} `);
1500
- channel.notify(t('input-image-pasted', { token }), { timeoutMs: 2500 });
1665
+ channel.notify(stagedImageNotice(token, handle), { timeoutMs: 2500 });
1501
1666
  })
1502
1667
  .catch(() => {
1503
1668
  if (!draftImageLeaseIsCurrent(lease))
@@ -1561,7 +1726,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1561
1726
  const handle = await stageImagePath(content.path, lease);
1562
1727
  const token = bindStagedImage(handle, lease);
1563
1728
  insertClipboardAtCaret(`${token} `);
1564
- channel.notify(t('input-image-pasted', { token }), { timeoutMs: 2500 });
1729
+ channel.notify(stagedImageNotice(token, handle), { timeoutMs: 2500 });
1565
1730
  });
1566
1731
  }
1567
1732
  catch (error) {
@@ -1605,9 +1770,14 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
1605
1770
  // All bindings and their visible labels enter together;
1606
1771
  // typing while an earlier file saves cannot prune one.
1607
1772
  insertClipboardAtCaret(`${rendered.join(' ')} `);
1773
+ // A batch cannot itemise every image in one line, but it must
1774
+ // still say that some of them were not stored as pasted.
1775
+ const adapted = staged.filter(handle => handle.adjustment !== undefined).length;
1608
1776
  channel.notify(boundTokens.length === 1
1609
- ? t('input-image-pasted', { token: boundTokens[0] })
1610
- : t('input-images-staged', { count: boundTokens.length }), { timeoutMs: 2500 });
1777
+ ? stagedImageNotice(boundTokens[0], staged[0])
1778
+ : adapted > 0
1779
+ ? t('input-images-staged-adapted', { count: boundTokens.length, adapted })
1780
+ : t('input-images-staged', { count: boundTokens.length }), { timeoutMs: 2500 });
1611
1781
  return true;
1612
1782
  }
1613
1783
  catch (error) {
@@ -2463,6 +2633,11 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
2463
2633
  // the same row, so the wrap budget must shrink by its width or long lines
2464
2634
  // would be clipped at the value box's right edge.
2465
2635
  const vimBadgeCols = vimEnabled ? 7 : 0;
2636
+ // 行首会话入口占列:未传 onOpenSessions 时按钮整体不渲染,预算同步归还
2637
+ // (既有调用方的可用列数逐列不变)。用实测常量而不是 stringWidth:该字符
2638
+ // 是 U+1F3E0+VS15,stringWidth 按码点算 2(含 VS15 仍是 2),终端只给 1,
2639
+ // 见 HOME_BUTTON_COLS 的说明。
2640
+ const homeButtonCols = onOpenSessions === undefined ? 0 : HOME_BUTTON_COLS;
2466
2641
  // 补全卡片边框与输入框 idle 边框同色(plan 模式下整套面板一起变 sage
2467
2642
  // 绿)。`/color` 会话强调色优先于主题 promptBorder(plan 模式仍整体走
2468
2643
  // sage 绿)。`?? ''` 防御最小 mock channel(只声明用到的字段的回归脚
@@ -2470,14 +2645,15 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
2470
2645
  const sessionAccent = sessionColorHex(channel.sessionColor ?? '');
2471
2646
  const promptAccent = channel.mode.plan === true ? 'planMode' : (sessionAccent ?? 'promptBorder');
2472
2647
  // 展开态布局参数:编辑器独占整屏 —— 行号槽(宽度随逻辑行数伸缩)+
2473
- // 圆角边框 2 + 两侧 padding 各 1 占列,❯ 前缀 / vim 徽标 / ⛶ 按钮
2474
- // 全部让位;收起态额外扣掉行尾 ⛶ 按钮的 2 列。
2648
+ // 圆角边框 2 + 两侧 padding 各 1 占列,⌸ 入口 / ❯ 前缀 / vim 徽标 /
2649
+ // ⛶ 按钮全部让位;收起态额外扣掉行首 ⌸ 入口(未渲染时 0 列)与行尾
2650
+ // ⛶ 按钮的 2 列。
2475
2651
  const editorLogicalLines = expanded ? value.split('\n').length : 1;
2476
2652
  const editorNoWidth = Math.max(2, String(editorLogicalLines).length);
2477
2653
  const editorGutterCols = editorNoWidth + 3;
2478
2654
  const inputWidth = expanded
2479
2655
  ? Math.max(1, columns - 4 - editorGutterCols)
2480
- : Math.max(1, columns - 3 - vimBadgeCols - (expandEnabled ? 2 : 0));
2656
+ : Math.max(1, columns - 3 - vimBadgeCols - homeButtonCols - (expandEnabled ? 2 : 0));
2481
2657
  // 展开态无视折叠块:全屏编辑就是为了看全文(foldBlock 状态保留,
2482
2658
  // 收起后折叠显示恢复)。
2483
2659
  const block = expanded ? null : foldBlock;
@@ -2546,22 +2722,50 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
2546
2722
  visibleCount = MAX_VISIBLE_LINES;
2547
2723
  }
2548
2724
  const visibleLines = visualLines.slice(windowStart, windowStart + visibleCount);
2725
+ // Caret-window jump: when the caret leaves MAX_VISIBLE_LINES (typing past
2726
+ // it, arrow-key walks through a long prompt) the whole visible band is
2727
+ // replaced in place — same-height rows, completely different text. The
2728
+ // per-cell diff that repaints that band is exactly the path that goes
2729
+ // haywire when the inline viewport is even one row out of sync with the
2730
+ // terminal's scrollback ("重叠变花": old rows bleeding through the new
2731
+ // ones). Same one-shot reanchor family as the shrink/floaters/editor
2732
+ // patches above — the window jump is a viewport-level discontinuity, not
2733
+ // an ordinary content edit.
2734
+ const prevWindowStartRef = React.useRef(windowStart);
2735
+ React.useLayoutEffect(() => {
2736
+ if (windowStart !== prevWindowStartRef.current) {
2737
+ prevWindowStartRef.current = windowStart;
2738
+ const ink = instances.get(process.stdout) ?? instances.values().next().value;
2739
+ ink?.invalidatePrevFrame();
2740
+ ink?.reanchorViewport();
2741
+ }
2742
+ }, [windowStart]);
2549
2743
  // useInput 的滚轮分支需要这份几何(它在这些派生之前注册)。
2550
2744
  if (expanded) {
2551
2745
  editorViewportRef.current = { maxRows: editorMaxRows, total: visualLines.length };
2552
2746
  }
2553
2747
  // Folded chip content: block stats + first-line preview + hover hint,
2554
2748
  // all pre-truncated to the input width (the row is one line, always).
2749
+ // `·` separators are U+30FB KATAKANA MIDDLE DOT, NOT U+00B7: the latter
2750
+ // is East-Asian-ambiguous (model 1 cell, CJK terminal fonts paint 2) and
2751
+ // this row's stats→preview→hint truncation arithmetic all runs through
2752
+ // stringWidth — a painted-wide separator shifts every segment right by
2753
+ // one column per separator and the row reads as overlapping text.
2754
+ // U+30FB is officially Ambiguous too, but get-east-asian-width hardcodes
2755
+ // it Wide and mainstream terminals (Western included) paint it 2 cells —
2756
+ // the model and the painted width agree in practice. A wcwidth-strict
2757
+ // Western terminal painting it 1 cell would leave a spare column, which
2758
+ // the `- 6` slack below absorbs.
2555
2759
  const foldBadge = `▸ ${stats}`;
2556
2760
  const foldHint = t('input-fold-hover');
2557
- const foldPreviewWidth = inputWidth - stringWidth(foldBadge) - stringWidth(` · ${foldHint}`) - 6;
2761
+ const foldPreviewWidth = inputWidth - stringWidth(foldBadge) - stringWidth(`・${foldHint}`) - 6;
2558
2762
  const foldPreview = foldPreviewWidth >= 8
2559
2763
  ? truncateToWidth(foldText.split('\n')[0] ?? '', foldPreviewWidth)
2560
2764
  : '';
2561
2765
  // Expanded-state fold affordance: a `▾` prefix at the start of the FIRST
2562
2766
  // row (only while the window is at the top and no block exists); its
2563
2767
  // cells fold the whole input into a block again on click.
2564
- const prefixLabel = `▾ ${stats} · `;
2768
+ const prefixLabel = `▾ ${stats}・`;
2565
2769
  const prefixCols = !block && !expanded && big && windowStart === 0 ? stringWidth(prefixLabel) : 0;
2566
2770
  // Offset range [start, end) of every visual row — the SAME break rule as
2567
2771
  // wrapToWidth — so the selection highlight can intersect each row. With a
@@ -2639,7 +2843,7 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
2639
2843
  return (_jsxs(Box, { flexDirection: "row", onClick: (event) => {
2640
2844
  event.stopImmediatePropagation();
2641
2845
  updateFoldBlock(null);
2642
- }, onMouseEnter: hoverEnter, onMouseLeave: hoverLeave, children: [_jsx(Text, { dimColor: true, children: foldBadge }), foldPreview !== '' && _jsx(Text, { dimColor: true, children: " \u00B7 " }), foldPreview !== '' && _jsx(Text, { wrap: "truncate-end", children: foldPreview }), _jsx(Text, { dimColor: true, children: ` · ${foldHint}` })] }, `fold-${absoluteLine}`));
2846
+ }, onMouseEnter: hoverEnter, onMouseLeave: hoverLeave, children: [_jsx(Text, { dimColor: true, children: foldBadge }), foldPreview !== '' && _jsx(Text, { dimColor: true, children: "\u30FB" }), foldPreview !== '' && _jsx(Text, { wrap: "truncate-end", children: foldPreview }), _jsx(Text, { dimColor: true, children: `・${foldHint}` })] }, `fold-${absoluteLine}`));
2643
2847
  }
2644
2848
  const withPrefix = absoluteLine === 0 && prefixCols > 0;
2645
2849
  const text = withPrefix ? truncateToWidth(line, inputWidth - prefixCols) : line;
@@ -2723,12 +2927,16 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
2723
2927
  // In the expanded editor the declared box starts at its outer edge
2724
2928
  // (padding + gutter included), so those columns ride along and the
2725
2929
  // clamp grows with them.
2726
- column: Math.min(caretVisualCol +
2727
- (expanded
2728
- ? editorGutterCols + 1
2729
- : caretVisualLine === 0 && prefixCols > 0
2730
- ? prefixCols
2731
- : 0), expanded ? editorGutterCols + 1 + inputWidth : inputWidth),
2930
+ //
2931
+ // The declared column is relative to the VALUE BOX, and `caretVisualCol`
2932
+ // is already measured in that box's cell space: the text box IS the text
2933
+ // run, so nothing that renders before it — the session entry, the `❯ `
2934
+ // glyph, the fold prefix — shifts a cell inside it. Adding any of those
2935
+ // back double-counts them and parks the hardware cursor to the RIGHT of
2936
+ // the inverted caret cell (a 2-column miss shows up as a displaced IME
2937
+ // preedit target). Only the expanded editor declares against a wider box
2938
+ // (gutter + padding), so only that branch adds columns.
2939
+ column: Math.min(caretVisualCol + (expanded ? editorGutterCols + 1 : 0), expanded ? editorGutterCols + 1 + inputWidth : inputWidth),
2732
2940
  active: !suspended && !selectionActive,
2733
2941
  });
2734
2942
  /**
@@ -3063,7 +3271,14 @@ export function PromptInput({ channel, suspended = false, helpOpen, onToggleHelp
3063
3271
  : undefined, rows: peekVisualLines.map((row, index) => (_jsx(Text, { wrap: "truncate-end", children: row }, index))), onRowPick: () => {
3064
3272
  updateFoldBlock(null);
3065
3273
  setHovered(false);
3066
- } }) }))] })), lastNotification && (_jsx(Box, { position: "absolute", marginTop: -1, height: 1, width: "100%", paddingLeft: 2, paddingRight: 1, flexDirection: "column", justifyContent: "flex-end", overflow: "hidden", children: _jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { color: lastNotification.color, dimColor: !lastNotification.color, wrap: "truncate", children: lastNotification.text }) }) })), _jsx(EffortInputBorder, { effort: channel.reasoningEffort, levels: channel.effortLevels, columns: columns, onLight: isLightThemeActive(themeName), idleColor: promptAccent, topRightLabel: topRightLabel, children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [_jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), vimEnabled && (_jsxs(Text, { bold: true, color: vimInsert ? 'success' : 'warning', children: [vimInsert ? 'INSERT' : 'NORMAL', " "] })), _jsx(Box, { ref: expanded ? undefined : valueBoxRef, flexGrow: 1, flexShrink: 1, onClick: handleValueClick, onDragStart: handleDragStart, onDragMove: handleDragMove, onDragEnd: handleDragEnd, children: value.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(EffortTierBadge, { effort: channel.reasoningEffort, levels: channel.effortLevels, onLight: isLightThemeActive(themeName), columns: columns, leadingColumns: 3 })] })) : (_jsx(Box, { flexDirection: "column", children: rendered })) }), expandEnabled && (_jsx(Box, { flexShrink: 0, onClick: (event) => {
3274
+ } }) }))] })), lastNotification && (_jsx(Box, { position: "absolute", marginTop: -1, height: 1, width: "100%", paddingLeft: 2, paddingRight: 1, flexDirection: "column", justifyContent: "flex-end", overflow: "hidden", children: _jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { color: lastNotification.color, dimColor: !lastNotification.color, wrap: "truncate", children: lastNotification.text }) }) })), _jsx(EffortInputBorder, { effort: channel.reasoningEffort, levels: channel.effortLevels, columns: columns, onLight: isLightThemeActive(themeName), idleColor: promptAccent, topRightLabel: topRightLabel, children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [onOpenSessions !== undefined && (_jsx(Box, { flexShrink: 0, onClick: (event) => {
3275
+ event.stopImmediatePropagation();
3276
+ onOpenSessions?.();
3277
+ }, onMouseEnter: () => {
3278
+ setHomeHovered(true);
3279
+ }, onMouseLeave: () => {
3280
+ setHomeHovered(false);
3281
+ }, children: _jsx(Text, { dimColor: !homeHovered, bold: homeHovered, color: homeHovered ? promptAccent : undefined, children: HOME_BUTTON_TEXT }) })), _jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), vimEnabled && (_jsxs(Text, { bold: true, color: vimInsert ? 'success' : 'warning', children: [vimInsert ? 'INSERT' : 'NORMAL', " "] })), _jsx(Box, { ref: expanded ? undefined : valueBoxRef, flexGrow: 1, flexShrink: 1, onClick: handleValueClick, onDragStart: handleDragStart, onDragMove: handleDragMove, onDragEnd: handleDragEnd, children: value.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(EffortTierBadge, { effort: channel.reasoningEffort, levels: channel.effortLevels, onLight: isLightThemeActive(themeName), columns: columns, leadingColumns: 3 + homeButtonCols })] })) : (_jsx(Box, { flexDirection: "column", children: rendered })) }), expandEnabled && (_jsx(Box, { flexShrink: 0, onClick: (event) => {
3067
3282
  event.stopImmediatePropagation();
3068
3283
  toggleExpand();
3069
3284
  }, onMouseEnter: () => {
@@ -10,16 +10,31 @@ import { type ScrollBoxHandle } from '../ui.js';
10
10
  * - clicking the track scrolls the clicked position to the viewport top
11
11
  * (classic scrollbar semantics — the thumb centers under the click
12
12
  * through the follow-up renders);
13
+ * - dragging the track scrubs the transcript: the drag protocol's
14
+ * per-target localRow maps through the SAME trackScrollTop as a click
15
+ * (absolute mapping — drag to point, no grabbed thumb offset), so a
16
+ * drag is a continuous run of click-to-position jumps. The mapping only
17
+ * reads the render-time maxScroll/trackH and never re-reads scrollTop
18
+ * mid-gesture, so a throttled/clamped scrollTo cannot feed back into the
19
+ * pointer mapping (no jitter). Dragging the thumb therefore lands the
20
+ * pointer's row at the viewport top, exactly like clicking that row;
21
+ * - a landing (drag or the back-to-bottom affordance) breaks and re-pins
22
+ * sticky, which unmounts the chrome that had shrunk the transcript row;
23
+ * the re-pinning Ink pass follows the commit and fires no subscriber
24
+ * notify, so the gutter re-reads the handle once that frame lands (see
25
+ * the subscribe effect). Without it the thumb stays painted from the
26
+ * shrunken viewport and floats a few rows above the bottom until an
27
+ * unrelated render (hover) re-reads the handle;
13
28
  * - the gutter is permanent while scrollable (Qwen's rule: an
14
29
  * auto-hiding gutter that changes content width rewraps everything);
15
30
  * hidden below 60 terminal columns or when the content fits (inline
16
31
  * mode keeps the terminal's native scrollback);
17
32
  * - NoSelect fences the glyphs out of click-drag text selection; the
18
- * wheel over the gutter scrolls the transcript.
19
- *
20
- * Thumb dragging is intentionally NOT implemented (Grok's MVP rule): a
21
- * proportional thumb encodes position, and the tick rail remains the
22
- * semantic navigator. Click-to-position covers the same need.
33
+ * wheel over the gutter scrolls the transcript. Because the track is a
34
+ * drag target, an unmodified left drag scrubs instead of selecting, and
35
+ * multi-clicks on the track no longer feed the select-line chain;
36
+ * Shift/Alt/Ctrl drags never open a drag session and keep the
37
+ * selection path.
23
38
  */
24
39
  export declare function ScrollbarGutter({ handle, terminalWidth, }: {
25
40
  handle: ScrollBoxHandle | null;
@@ -1 +1 @@
1
- {"version":3,"file":"ScrollbarGutter.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollbarGutter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAUpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,KAAK,CAAC,SAAS,CAsIlB"}
1
+ {"version":3,"file":"ScrollbarGutter.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollbarGutter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAqBpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,KAAK,CAAC,SAAS,CA6LlB"}