@deepseek-harness-tui/dsh-tui 0.8.8 → 0.9.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 (467) hide show
  1. package/README.md +60 -238
  2. package/bin/dsh-tui.js +172 -5
  3. package/cordis.patch.yml +16 -4
  4. package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
  5. package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
  6. package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
  7. package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
  8. package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
  9. package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
  10. package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
  11. package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
  12. package/dsh-ecosystem-spec/registry/README.md +14 -14
  13. package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
  14. package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
  15. package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
  16. package/lib/types/cc/hyperlink.d.ts +7 -1
  17. package/lib/types/cc/hyperlink.d.ts.map +1 -1
  18. package/lib/types/cc/hyperlink.js +3 -2
  19. package/lib/types/cc/markdown.d.ts.map +1 -1
  20. package/lib/types/cc/markdown.js +33 -3
  21. package/lib/types/cc/sessionColors.d.ts +27 -0
  22. package/lib/types/cc/sessionColors.d.ts.map +1 -0
  23. package/lib/types/cc/sessionColors.js +39 -0
  24. package/lib/types/commands.d.ts.map +1 -1
  25. package/lib/types/commands.js +10 -2
  26. package/lib/types/components/ActivityPicker.d.ts +4 -1
  27. package/lib/types/components/ActivityPicker.d.ts.map +1 -1
  28. package/lib/types/components/ActivityPicker.js +2 -2
  29. package/lib/types/components/AutoRecapRow.d.ts +23 -0
  30. package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
  31. package/lib/types/components/AutoRecapRow.js +23 -0
  32. package/lib/types/components/BtwPanel.d.ts.map +1 -1
  33. package/lib/types/components/BtwPanel.js +1 -1
  34. package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
  35. package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
  36. package/lib/types/components/Chat/SubagentMessage.js +11 -3
  37. package/lib/types/components/ColorPicker.d.ts +17 -0
  38. package/lib/types/components/ColorPicker.d.ts.map +1 -0
  39. package/lib/types/components/ColorPicker.js +20 -0
  40. package/lib/types/components/CommandSuggestions.d.ts +23 -4
  41. package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
  42. package/lib/types/components/CommandSuggestions.js +41 -14
  43. package/lib/types/components/ContextBarView.d.ts +33 -0
  44. package/lib/types/components/ContextBarView.d.ts.map +1 -0
  45. package/lib/types/components/ContextBarView.js +46 -0
  46. package/lib/types/components/EffortInputBorder.d.ts +12 -1
  47. package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
  48. package/lib/types/components/EffortInputBorder.js +22 -5
  49. package/lib/types/components/EffortSlider.d.ts +4 -1
  50. package/lib/types/components/EffortSlider.d.ts.map +1 -1
  51. package/lib/types/components/EffortSlider.js +4 -2
  52. package/lib/types/components/ExtensionDialog.js +6 -2
  53. package/lib/types/components/FileActionsPanel.d.ts +22 -0
  54. package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
  55. package/lib/types/components/FileActionsPanel.js +28 -0
  56. package/lib/types/components/FileSuggestions.d.ts +26 -7
  57. package/lib/types/components/FileSuggestions.d.ts.map +1 -1
  58. package/lib/types/components/FileSuggestions.js +39 -12
  59. package/lib/types/components/GoalTodoPanel.d.ts +26 -2
  60. package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
  61. package/lib/types/components/GoalTodoPanel.js +86 -17
  62. package/lib/types/components/HelpMenu.d.ts +7 -1
  63. package/lib/types/components/HelpMenu.d.ts.map +1 -1
  64. package/lib/types/components/HelpMenu.js +12 -4
  65. package/lib/types/components/HistorySearchDialog.d.ts +4 -1
  66. package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
  67. package/lib/types/components/HistorySearchDialog.js +2 -2
  68. package/lib/types/components/LangPicker.d.ts +14 -0
  69. package/lib/types/components/LangPicker.d.ts.map +1 -0
  70. package/lib/types/components/LangPicker.js +17 -0
  71. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  72. package/lib/types/components/LoadedContextPanel.js +3 -1
  73. package/lib/types/components/Markdown.d.ts.map +1 -1
  74. package/lib/types/components/Markdown.js +12 -3
  75. package/lib/types/components/MessageList.d.ts +45 -2
  76. package/lib/types/components/MessageList.d.ts.map +1 -1
  77. package/lib/types/components/MessageList.js +560 -41
  78. package/lib/types/components/ModelPicker.d.ts +27 -2
  79. package/lib/types/components/ModelPicker.d.ts.map +1 -1
  80. package/lib/types/components/ModelPicker.js +26 -6
  81. package/lib/types/components/PermissionsPicker.d.ts +21 -0
  82. package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
  83. package/lib/types/components/PermissionsPicker.js +27 -0
  84. package/lib/types/components/PlanPicker.d.ts +15 -0
  85. package/lib/types/components/PlanPicker.d.ts.map +1 -0
  86. package/lib/types/components/PlanPicker.js +18 -0
  87. package/lib/types/components/PresetPicker.d.ts +4 -1
  88. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  89. package/lib/types/components/PresetPicker.js +2 -2
  90. package/lib/types/components/PromptInput.d.ts +4 -3
  91. package/lib/types/components/PromptInput.d.ts.map +1 -1
  92. package/lib/types/components/PromptInput.js +602 -75
  93. package/lib/types/components/RecapPanel.d.ts +25 -0
  94. package/lib/types/components/RecapPanel.d.ts.map +1 -0
  95. package/lib/types/components/RecapPanel.js +55 -0
  96. package/lib/types/components/RewindPicker.d.ts +12 -1
  97. package/lib/types/components/RewindPicker.d.ts.map +1 -1
  98. package/lib/types/components/RewindPicker.js +6 -4
  99. package/lib/types/components/ScrollbarGutter.d.ts +28 -0
  100. package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
  101. package/lib/types/components/ScrollbarGutter.js +119 -0
  102. package/lib/types/components/Select.d.ts +8 -1
  103. package/lib/types/components/Select.d.ts.map +1 -1
  104. package/lib/types/components/Select.js +4 -2
  105. package/lib/types/components/SkillsPicker.d.ts +4 -1
  106. package/lib/types/components/SkillsPicker.d.ts.map +1 -1
  107. package/lib/types/components/SkillsPicker.js +2 -2
  108. package/lib/types/components/SubagentCard.d.ts +2 -1
  109. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  110. package/lib/types/components/SubagentCard.js +9 -2
  111. package/lib/types/components/SubagentDashboard.d.ts +4 -0
  112. package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
  113. package/lib/types/components/SubagentDashboard.js +9 -1
  114. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  115. package/lib/types/components/SubagentDetailScene.js +5 -4
  116. package/lib/types/components/SuggestionCard.d.ts +65 -0
  117. package/lib/types/components/SuggestionCard.d.ts.map +1 -0
  118. package/lib/types/components/SuggestionCard.js +77 -0
  119. package/lib/types/components/ThemePicker.d.ts +4 -1
  120. package/lib/types/components/ThemePicker.d.ts.map +1 -1
  121. package/lib/types/components/ThemePicker.js +2 -2
  122. package/lib/types/components/ThinkingToggle.d.ts +4 -1
  123. package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
  124. package/lib/types/components/ThinkingToggle.js +2 -2
  125. package/lib/types/components/TimelineRail.d.ts +53 -0
  126. package/lib/types/components/TimelineRail.d.ts.map +1 -0
  127. package/lib/types/components/TimelineRail.js +197 -0
  128. package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
  129. package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
  130. package/lib/types/components/WorkspaceFlowPicker.js +4 -2
  131. package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
  132. package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
  133. package/lib/types/components/WorkspaceMenuPicker.js +19 -0
  134. package/lib/types/components/WorkspacePicker.d.ts +4 -1
  135. package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
  136. package/lib/types/components/WorkspacePicker.js +2 -2
  137. package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
  138. package/lib/types/components/approvals/ApprovalPanel.js +5 -1
  139. package/lib/types/components/design-system/ListItem.d.ts +8 -1
  140. package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
  141. package/lib/types/components/design-system/ListItem.js +8 -3
  142. package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
  143. package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
  144. package/lib/types/components/design-system/ThemeProvider.js +2 -1
  145. package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
  146. package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
  147. package/lib/types/components/messages/AssistantTextMessage.js +6 -2
  148. package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
  149. package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
  150. package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
  151. package/lib/types/components/messages/AssistantToolUseMessage.d.ts +15 -1
  152. package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
  153. package/lib/types/components/messages/AssistantToolUseMessage.js +37 -7
  154. package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
  155. package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
  156. package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
  157. package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
  158. package/lib/types/components/questions/AskUserQuestionPanel.js +75 -11
  159. package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
  160. package/lib/types/components/questions/PlanReviewPanel.js +13 -2
  161. package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
  162. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
  163. package/lib/types/components/sessions/SessionListRow.js +4 -2
  164. package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
  165. package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
  166. package/lib/types/components/trajectory/HotspotView.js +8 -2
  167. package/lib/types/components/trajectory/Ledger.d.ts +9 -1
  168. package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
  169. package/lib/types/components/trajectory/Ledger.js +23 -5
  170. package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
  171. package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
  172. package/lib/types/components/trajectory/WaveBand.js +7 -2
  173. package/lib/types/dsh-adapter/channel.d.ts +117 -7
  174. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  175. package/lib/types/dsh-adapter/channel.js +1301 -122
  176. package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
  177. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  178. package/lib/types/dsh-adapter/compat/index.js +8 -6
  179. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
  180. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  181. package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
  182. package/lib/types/dsh-adapter/contract.d.ts +1 -1
  183. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  184. package/lib/types/dsh-adapter/contract.js +1 -0
  185. package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
  186. package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
  187. package/lib/types/dsh-adapter/index.d.ts +20 -2
  188. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  189. package/lib/types/dsh-adapter/index.js +9 -1
  190. package/lib/types/dsh-adapter/plugin.d.ts +0 -9
  191. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  192. package/lib/types/dsh-adapter/plugin.js +453 -14
  193. package/lib/types/dsh-adapter/presets.d.ts +18 -0
  194. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  195. package/lib/types/dsh-adapter/presets.js +31 -0
  196. package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
  197. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  198. package/lib/types/dsh-adapter/providerWizard.js +89 -5
  199. package/lib/types/dsh-adapter/questions.d.ts +19 -2
  200. package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
  201. package/lib/types/dsh-adapter/questions.js +53 -10
  202. package/lib/types/dsh-adapter/recap.d.ts +49 -0
  203. package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
  204. package/lib/types/dsh-adapter/recap.js +121 -0
  205. package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
  206. package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
  207. package/lib/types/dsh-adapter/sessionTree.js +954 -0
  208. package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
  209. package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
  210. package/lib/types/dsh-adapter/shortcuts.js +28 -165
  211. package/lib/types/dsh-adapter/types.d.ts +1 -1
  212. package/lib/types/dsh-adapter/types.d.ts.map +1 -1
  213. package/lib/types/herdr.js +1 -1
  214. package/lib/types/i18n.d.ts +770 -181
  215. package/lib/types/i18n.d.ts.map +1 -1
  216. package/lib/types/i18n.js +257 -80
  217. package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
  218. package/lib/types/ink/components/AlternateScreen.js +13 -1
  219. package/lib/types/ink/components/App.d.ts +12 -1
  220. package/lib/types/ink/components/App.d.ts.map +1 -1
  221. package/lib/types/ink/components/App.js +101 -4
  222. package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
  223. package/lib/types/ink/components/ClockContext.js +2 -0
  224. package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
  225. package/lib/types/ink/components/ScrollBox.js +170 -91
  226. package/lib/types/ink/constants.d.ts +9 -0
  227. package/lib/types/ink/constants.d.ts.map +1 -1
  228. package/lib/types/ink/constants.js +9 -0
  229. package/lib/types/ink/events/click-event.d.ts +7 -16
  230. package/lib/types/ink/events/click-event.d.ts.map +1 -1
  231. package/lib/types/ink/events/click-event.js +16 -19
  232. package/lib/types/ink/events/event-handlers.d.ts +11 -1
  233. package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
  234. package/lib/types/ink/events/event-handlers.js +2 -0
  235. package/lib/types/ink/events/input-event.d.ts +9 -0
  236. package/lib/types/ink/events/input-event.d.ts.map +1 -1
  237. package/lib/types/ink/events/input-event.js +4 -0
  238. package/lib/types/ink/events/pointer-event.d.ts +61 -0
  239. package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
  240. package/lib/types/ink/events/pointer-event.js +61 -0
  241. package/lib/types/ink/events/wheel-event.d.ts +17 -0
  242. package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
  243. package/lib/types/ink/events/wheel-event.js +20 -0
  244. package/lib/types/ink/geometry-trace.d.ts +51 -0
  245. package/lib/types/ink/geometry-trace.d.ts.map +1 -0
  246. package/lib/types/ink/geometry-trace.js +83 -0
  247. package/lib/types/ink/hit-test.d.ts +56 -1
  248. package/lib/types/ink/hit-test.d.ts.map +1 -1
  249. package/lib/types/ink/hit-test.js +146 -8
  250. package/lib/types/ink/ink.d.ts +58 -1
  251. package/lib/types/ink/ink.d.ts.map +1 -1
  252. package/lib/types/ink/ink.js +230 -29
  253. package/lib/types/ink/log-update.d.ts.map +1 -1
  254. package/lib/types/ink/log-update.js +21 -0
  255. package/lib/types/ink/node-cache.d.ts +5 -0
  256. package/lib/types/ink/node-cache.d.ts.map +1 -1
  257. package/lib/types/ink/parse-keypress.d.ts +8 -0
  258. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  259. package/lib/types/ink/parse-keypress.js +101 -12
  260. package/lib/types/ink/render-node-to-output.d.ts +10 -0
  261. package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
  262. package/lib/types/ink/render-node-to-output.js +111 -18
  263. package/lib/types/ink/timeline-rail.d.ts +138 -0
  264. package/lib/types/ink/timeline-rail.d.ts.map +1 -0
  265. package/lib/types/ink/timeline-rail.js +179 -0
  266. package/lib/types/ink/wrap-text.d.ts.map +1 -1
  267. package/lib/types/ink/wrap-text.js +47 -5
  268. package/lib/types/migrationPrefs.d.ts +60 -0
  269. package/lib/types/migrationPrefs.d.ts.map +1 -0
  270. package/lib/types/migrationPrefs.js +97 -0
  271. package/lib/types/modelGroups.d.ts +72 -0
  272. package/lib/types/modelGroups.d.ts.map +1 -0
  273. package/lib/types/modelGroups.js +97 -0
  274. package/lib/types/modelRecents.d.ts +39 -0
  275. package/lib/types/modelRecents.d.ts.map +1 -0
  276. package/lib/types/modelRecents.js +97 -0
  277. package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
  278. package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
  279. package/lib/types/native-ts/yoga-layout/index.js +31 -1
  280. package/lib/types/oauth.d.ts +15 -0
  281. package/lib/types/oauth.d.ts.map +1 -0
  282. package/lib/types/oauth.js +14 -0
  283. package/lib/types/reload.d.ts +109 -0
  284. package/lib/types/reload.d.ts.map +1 -0
  285. package/lib/types/reload.js +118 -0
  286. package/lib/types/screens/Chat.d.ts +3 -1
  287. package/lib/types/screens/Chat.d.ts.map +1 -1
  288. package/lib/types/screens/Chat.js +1377 -371
  289. package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
  290. package/lib/types/screens/SessionBrowser.js +56 -29
  291. package/lib/types/screens/SessionTree.d.ts +27 -0
  292. package/lib/types/screens/SessionTree.d.ts.map +1 -0
  293. package/lib/types/screens/SessionTree.js +555 -0
  294. package/lib/types/screens/Settings.d.ts.map +1 -1
  295. package/lib/types/screens/Settings.js +64 -22
  296. package/lib/types/screens/StatusLine.d.ts +0 -8
  297. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  298. package/lib/types/screens/StatusLine.js +224 -45
  299. package/lib/types/screens/StatusMetrics.d.ts +35 -3
  300. package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
  301. package/lib/types/screens/StatusMetrics.js +63 -8
  302. package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
  303. package/lib/types/screens/TrajectoryScene.js +81 -14
  304. package/lib/types/screens/chatOverlay.d.ts +237 -0
  305. package/lib/types/screens/chatOverlay.d.ts.map +1 -0
  306. package/lib/types/screens/chatOverlay.js +116 -0
  307. package/lib/types/tips.d.ts.map +1 -1
  308. package/lib/types/tips.js +64 -10
  309. package/lib/types/tuiDisplayPrefs.d.ts +15 -0
  310. package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
  311. package/lib/types/tuiDisplayPrefs.js +9 -0
  312. package/lib/types/update.d.ts +139 -0
  313. package/lib/types/update.d.ts.map +1 -1
  314. package/lib/types/update.js +382 -30
  315. package/lib/types/utils/debug.d.ts +10 -0
  316. package/lib/types/utils/debug.d.ts.map +1 -1
  317. package/lib/types/utils/debug.js +24 -0
  318. package/lib/types/utils/fileTarget.d.ts +46 -0
  319. package/lib/types/utils/fileTarget.d.ts.map +1 -0
  320. package/lib/types/utils/fileTarget.js +178 -0
  321. package/lib/types/utils/keymap.d.ts +134 -0
  322. package/lib/types/utils/keymap.d.ts.map +1 -0
  323. package/lib/types/utils/keymap.js +311 -0
  324. package/lib/types/utils/mentions.d.ts +25 -2
  325. package/lib/types/utils/mentions.d.ts.map +1 -1
  326. package/lib/types/utils/mentions.js +69 -10
  327. package/lib/types/utils/openExternal.d.ts +50 -0
  328. package/lib/types/utils/openExternal.d.ts.map +1 -0
  329. package/lib/types/utils/openExternal.js +170 -0
  330. package/lib/types/utils/paths.d.ts.map +1 -1
  331. package/lib/types/utils/paths.js +1 -0
  332. package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
  333. package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
  334. package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
  335. package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
  336. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
  337. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
  338. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
  339. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
  340. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
  341. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
  342. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
  343. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
  344. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
  345. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
  346. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
  347. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
  348. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
  349. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
  350. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
  351. package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
  352. package/node_modules/@dsh-std/command/LICENSE +21 -21
  353. package/node_modules/@dsh-std/command/README.md +26 -26
  354. package/node_modules/@dsh-std/command/README.zh.md +28 -28
  355. package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
  356. package/node_modules/@dsh-std/command/package.json +22 -22
  357. package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  358. package/node_modules/@dsh-std/connection/LICENSE +21 -21
  359. package/node_modules/@dsh-std/connection/README.md +42 -42
  360. package/node_modules/@dsh-std/connection/README.zh.md +42 -42
  361. package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  362. package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  363. package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  364. package/node_modules/@dsh-std/connection/package.json +20 -20
  365. package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  366. package/node_modules/@dsh-std/core/LICENSE +21 -21
  367. package/node_modules/@dsh-std/core/README.md +13 -13
  368. package/node_modules/@dsh-std/core/README.zh.md +13 -13
  369. package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  370. package/node_modules/@dsh-std/core/package.json +21 -21
  371. package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  372. package/node_modules/@dsh-std/manifest/LICENSE +21 -21
  373. package/node_modules/@dsh-std/manifest/README.md +5 -5
  374. package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  375. package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  376. package/node_modules/@dsh-std/manifest/package.json +20 -20
  377. package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  378. package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
  379. package/node_modules/@dsh-std/messages/LICENSE +21 -21
  380. package/node_modules/@dsh-std/messages/README.md +5 -5
  381. package/node_modules/@dsh-std/messages/README.zh.md +7 -7
  382. package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
  383. package/node_modules/@dsh-std/messages/package.json +20 -20
  384. package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
  385. package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  386. package/node_modules/@dsh-std/presentation/LICENSE +21 -21
  387. package/node_modules/@dsh-std/presentation/README.md +27 -27
  388. package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  389. package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  390. package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  391. package/node_modules/@dsh-std/presentation/package.json +25 -25
  392. package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
  393. package/node_modules/@dsh-std/storage/LICENSE +21 -21
  394. package/node_modules/@dsh-std/storage/README.md +5 -5
  395. package/node_modules/@dsh-std/storage/README.zh.md +7 -7
  396. package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
  397. package/node_modules/@dsh-std/storage/package.json +20 -20
  398. package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
  399. package/package.json +19 -5
  400. package/presets/liangshen/.dsh-tui-managed.json +1 -1
  401. package/presets/liangshen/instruction-hint.mjs +8 -0
  402. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  403. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
  404. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
  405. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
  406. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  407. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  408. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  409. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
  410. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  411. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
  412. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
  413. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
  414. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  415. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
  416. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
  417. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
  418. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
  419. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
  420. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
  421. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
  422. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
  423. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
  424. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
  425. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
  426. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
  427. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
  428. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
  429. package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
  430. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  431. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
  432. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
  433. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
  434. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  435. package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
  436. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  437. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
  438. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
  439. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
  440. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  441. package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
  442. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  443. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
  444. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
  445. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
  446. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  447. package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
  448. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
  449. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
  450. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
  451. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
  452. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
  453. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
  454. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
  455. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
  456. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  457. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
  458. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
  459. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
  460. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  461. package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
  462. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
  463. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
  464. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
  465. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
  466. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
  467. package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
@@ -8,8 +8,13 @@ import { EffortChargeGlyph } from './EffortChargeGlyph.js';
8
8
  import { EffortInputBorder } from './EffortInputBorder.js';
9
9
  import { EffortTierBadge } from './EffortTierBadge.js';
10
10
  import { isLightThemeActive } from '../theme.js';
11
+ import { sessionColorHex } from '../cc/sessionColors.js';
11
12
  import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
13
+ import { noteAuxNumber } from '../ink/geometry-trace.js';
14
+ import instances from '../ink/instances.js';
12
15
  import { stringWidth } from '../ink/stringWidth.js';
16
+ import { truncateToWidth } from '../ink/truncateToWidth.js';
17
+ import { getGraphemeSegmenter } from '../utils/intl.js';
13
18
  import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
14
19
  import { editInExternalEditor } from '../utils/externalEditor.js';
15
20
  import { isHiddenCommandName, parseCommandName } from '../commands.js';
@@ -17,11 +22,25 @@ import { appendHistory } from '../history.js';
17
22
  import { mentionAtCaret } from '../utils/mentions.js';
18
23
  import { preserveSelection } from '../utils/fileSuggestions.js';
19
24
  import { isMod } from '../utils/modifiers.js';
25
+ import { actionMatches } from '../utils/keymap.js';
20
26
  import { CommandSuggestions } from './CommandSuggestions.js';
21
27
  import { FileSuggestions } from './FileSuggestions.js';
22
28
  import { HelpMenu } from './HelpMenu.js';
23
29
  import { OverlayAbove } from './OverlayAbove.js';
30
+ import { SuggestionCard, cardContentWidth } from './SuggestionCard.js';
24
31
  const HISTORY_LIMIT = 50;
32
+ /**
33
+ * Paste fold (CC-style collapse with a visible preview, no black box):
34
+ * a paste that leaves the input this big folds into a one-line chip
35
+ * showing the line/char count PLUS the first line of content. Hover peeks
36
+ * at the full text (window pinned to the head); clicking the chip — or
37
+ * the `▾` prefix on the first row while expanded — toggles the fold; Esc
38
+ * or any editing key unfolds first. Enter still submits the FULL text:
39
+ * folding never drops data.
40
+ */
41
+ const FOLD_MIN_LINES = 6;
42
+ const FOLD_MIN_CHARS = 600;
43
+ const isBigInput = (text) => text.split('\n').length >= FOLD_MIN_LINES || text.length >= FOLD_MIN_CHARS;
25
44
  function clipboardImageMediaType(path) {
26
45
  if (/\.png$/iu.test(path))
27
46
  return 'image/png';
@@ -52,6 +71,84 @@ function wordBoundaryRight(text, cursor) {
52
71
  index++;
53
72
  return index;
54
73
  }
74
+ // --- grapheme-cluster geometry ---------------------------------------------
75
+ // The caret, editing keys, and wrapping MUST agree on one text unit. Mixing
76
+ // UTF-16 code units (arrows/backspace), code points (wrap), and display
77
+ // cells (stringWidth) lets the caret land inside a surrogate pair or a ZWJ
78
+ // emoji — the inverted caret then shows half a glyph, Backspace deletes
79
+ // half a character, and `line.slice()` splits clusters. All offsets below
80
+ // are UTF-16 indices snapped to grapheme boundaries via the shared
81
+ // Intl.Segmenter (utils/intl.ts).
82
+ /** Ascending grapheme boundary offsets of `text` (starts at 0, ends at
83
+ * `text.length`). Empty text yields `[0]`. */
84
+ function graphemeBoundaries(text) {
85
+ const bounds = [0];
86
+ for (const { index, segment } of getGraphemeSegmenter().segment(text)) {
87
+ const end = index + segment.length;
88
+ if (end > bounds[bounds.length - 1])
89
+ bounds.push(end);
90
+ }
91
+ return bounds;
92
+ }
93
+ /** Largest grapheme boundary `<= offset` (clamped into the text). Snaps a
94
+ * cursor that landed mid-cluster back onto a boundary. */
95
+ function boundaryAtOrBefore(bounds, offset) {
96
+ let lo = 0;
97
+ let hi = bounds.length - 1;
98
+ let ans = bounds[0];
99
+ while (lo <= hi) {
100
+ const mid = (lo + hi) >> 1;
101
+ if (bounds[mid] <= offset) {
102
+ ans = bounds[mid];
103
+ lo = mid + 1;
104
+ }
105
+ else {
106
+ hi = mid - 1;
107
+ }
108
+ }
109
+ return ans;
110
+ }
111
+ /** Largest grapheme boundary strictly before `offset` (0 when none). */
112
+ function previousGraphemeBoundary(bounds, offset) {
113
+ let lo = 0;
114
+ let hi = bounds.length - 1;
115
+ let ans = 0;
116
+ while (lo <= hi) {
117
+ const mid = (lo + hi) >> 1;
118
+ if (bounds[mid] < offset) {
119
+ ans = bounds[mid];
120
+ lo = mid + 1;
121
+ }
122
+ else {
123
+ hi = mid - 1;
124
+ }
125
+ }
126
+ return ans;
127
+ }
128
+ /** Smallest grapheme boundary strictly after `offset` (text.length when
129
+ * none). Returns `offset` unchanged when it already is the last boundary. */
130
+ function nextGraphemeBoundary(bounds, offset) {
131
+ let lo = 0;
132
+ let hi = bounds.length - 1;
133
+ let ans = bounds[hi] ?? 0;
134
+ while (lo <= hi) {
135
+ const mid = (lo + hi) >> 1;
136
+ if (bounds[mid] > offset) {
137
+ ans = bounds[mid];
138
+ hi = mid - 1;
139
+ }
140
+ else {
141
+ lo = mid + 1;
142
+ }
143
+ }
144
+ return ans;
145
+ }
146
+ /** Snap an arbitrary UTF-16 offset onto a grapheme boundary of `text`,
147
+ * clamping into range. The safety net under every cursor write. */
148
+ function normalizeCursorOffset(text, offset) {
149
+ const clamped = Math.max(0, Math.min(offset, text.length));
150
+ return boundaryAtOrBefore(graphemeBoundaries(text), clamped);
151
+ }
55
152
  /**
56
153
  * The empty input deliberately shows NO placeholder text: terminal emulators
57
154
  * paint the IME preedit (pinyin) at the physical cursor, which the app parks
@@ -66,9 +163,10 @@ const MAX_VISIBLE_LINES = 5;
66
163
  /**
67
164
  * Claude Code style prompt input: rounded border box (top+bottom borders
68
165
  * only), `❯ ` prompt char (dimmed while a turn is working), the text with a
69
- * block cursor at the cursor position, and below it the slash-command
70
- * suggestion overlay (name column + description, selected row in the
71
- * `suggestion` color — mirroring Claude Code's PromptInputFooterSuggestions).
166
+ * block cursor at the cursor position, and above it the slash-command /
167
+ * file-completion suggestion card (SuggestionCard: rounded panel with the
168
+ * selected row behind a `❯` pointer in the theme's `suggestion` color,
169
+ * mirroring Claude Code's PromptInputFooterSuggestions layout).
72
170
  *
73
171
  * Empty input: a solid block caret on a blank cell and nothing else — no
74
172
  * placeholder text, so the terminal-painted IME preedit (pinyin) at the
@@ -97,6 +195,30 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
97
195
  const [themeName] = useTheme();
98
196
  const [value, setValue] = React.useState('');
99
197
  const [cursor, setCursor] = React.useState(0);
198
+ /**
199
+ * CC-style fold block: the [start, end) span of `value` that renders as
200
+ * a one-line chip while the text around it stays fully editable. Created
201
+ * by a big paste; only an EXPLICIT expand (chip/card click, Esc) or
202
+ * delete removes it — typing NEVER unfolds the block.
203
+ */
204
+ const [foldBlock, setFoldBlock] = React.useState(null);
205
+ /** Pointer over the input box (drives the hover peek card). */
206
+ const [hovered, setHovered] = React.useState(false);
207
+ /** 120ms grace so the pointer crossing the input border row from the
208
+ * chip up onto the peek card never flickers the card. */
209
+ const hoverLeaveTimerRef = React.useRef(null);
210
+ const hoverEnter = React.useCallback(() => {
211
+ if (hoverLeaveTimerRef.current) {
212
+ clearTimeout(hoverLeaveTimerRef.current);
213
+ hoverLeaveTimerRef.current = null;
214
+ }
215
+ setHovered(true);
216
+ }, []);
217
+ const hoverLeave = React.useCallback(() => {
218
+ if (hoverLeaveTimerRef.current)
219
+ clearTimeout(hoverLeaveTimerRef.current);
220
+ hoverLeaveTimerRef.current = setTimeout(() => setHovered(false), 120);
221
+ }, []);
100
222
  const valueRef = React.useRef(value);
101
223
  const cursorRef = React.useRef(cursor);
102
224
  valueRef.current = value;
@@ -129,19 +251,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
129
251
  React.useEffect(() => {
130
252
  if (fillText && fillText !== lastFill.current) {
131
253
  lastFill.current = fillText;
132
- valueRef.current = fillText;
133
- cursorRef.current = fillText.length;
134
- setValue(fillText);
135
- setCursor(fillText.length);
254
+ updateFoldBlock(null);
255
+ setInput(fillText);
136
256
  onFillConsumed?.();
137
257
  }
138
258
  }, [fillText, onFillConsumed]);
139
259
  // Double-tap Esc to clear (CC semantics).
140
260
  const escPendingRef = React.useRef(false);
141
261
  const escTimerRef = React.useRef(null);
142
- /** True while a Ctrl+V clipboard read is in flight (ignore repeat keys). */
262
+ /** True while a clipboard paste read is in flight (ignore repeat keys). */
143
263
  const clipboardBusyRef = React.useRef(false);
144
- /** True while the external editor owns the terminal (Ctrl+G round-trip). */
264
+ /** True while the external editor owns the terminal (editor-key round-trip). */
145
265
  const editorBusyRef = React.useRef(false);
146
266
  /** Enter dedupe window: cmd pipelines can deliver one Enter as `\r`+`\n`. */
147
267
  const lastEnterAtRef = React.useRef(0);
@@ -149,14 +269,26 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
149
269
  return () => {
150
270
  if (escTimerRef.current)
151
271
  clearTimeout(escTimerRef.current);
272
+ if (hoverLeaveTimerRef.current)
273
+ clearTimeout(hoverLeaveTimerRef.current);
152
274
  };
153
275
  }, []);
154
276
  const { columns, rows: terminalRows } = useTerminalSize();
155
277
  const helpScrollRef = React.useRef(null);
156
- // OverlayAbove reserves six terminal rows for the composer/status chrome;
157
- // the help block also keeps one row below it. Its own final row is a
158
- // persistent navigation hint, leaving the remainder to ScrollBox.
159
- const helpViewportHeight = Math.max(3, Math.max(terminalRows - 6, 4) - 1);
278
+ // Help viewport budget: the overlay anchors at the composer's top edge
279
+ // (OverlayAbove bottom:'100%') and grows UP, so its budget is the space
280
+ // ABOVE that anchor — smallest on an empty session, where the whale
281
+ // splash (~15 rows) sits between the screen top and the composer
282
+ // (terminalRows minus chrome only applies once the transcript fills the
283
+ // viewport). Take the conservative intersection: 15 rows of viewport (16
284
+ // with the hint + margin) fits the empty-session anchor on the default
285
+ // layout at any terminal size, and the renderer's bottom-anchored
286
+ // clipping for absolute overlays (no negative-y clamp) then never has
287
+ // to eat the overlay's FIRST rows — the shortcut-column headers. The
288
+ // command registry scrolls inside the viewport, so a taller terminal
289
+ // loses nothing functional. (PR #446; restored after the picker
290
+ // snapshot's cherry-pick resurrected the old formula.)
291
+ const helpViewportHeight = Math.max(3, Math.min(terminalRows - 7, 15));
160
292
  const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
161
293
  const overlayOpen = suggestions.length > 0 &&
162
294
  !helpOpen &&
@@ -201,24 +333,77 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
201
333
  !helpOpen &&
202
334
  !selectionActive &&
203
335
  fileEscRef.current !== mention?.start;
336
+ /** Fold-block state + a synchronous mirror (setInput reads the ref).
337
+ * Creating a block also drags a caret that sits inside it out to the
338
+ * block's end (the block is atomic; typing continues after it). */
339
+ const foldBlockRef = React.useRef(null);
340
+ const updateFoldBlock = (block) => {
341
+ foldBlockRef.current = block;
342
+ setFoldBlock(block);
343
+ if (block) {
344
+ const c = cursorRef.current;
345
+ if (c > block.start && c < block.end) {
346
+ cursorRef.current = block.end;
347
+ setCursor(block.end);
348
+ }
349
+ }
350
+ };
204
351
  const setInput = (next, cursorOffset = next.length) => {
352
+ const prev = valueRef.current;
353
+ const prevCursor = cursorRef.current;
354
+ const block = foldBlockRef.current;
355
+ // Normalize onto a grapheme boundary (also clamps into range): every
356
+ // caller passes a caret they believe is on a character edge — paste
357
+ // merges, history fills, and IME composition can still hand back an
358
+ // offset inside a surrogate pair or combining cluster.
359
+ let offset = normalizeCursorOffset(next, cursorOffset);
360
+ if (block) {
361
+ // Fold block is atomic: the caret never lands INSIDE [start, end).
362
+ if (offset > block.start && offset < block.end) {
363
+ offset = offset - block.start < block.end - offset ? block.start : block.end;
364
+ }
365
+ // Every real edit happens AT the caret, so a block that starts at or
366
+ // after the caret shifts with the value delta; one fully past the
367
+ // caret is untouched. Whole-value replacements (fill/history/editor)
368
+ // clear the block explicitly at their call sites.
369
+ const delta = next.length - prev.length;
370
+ let start = block.start;
371
+ let end = block.end;
372
+ if (prevCursor <= block.start) {
373
+ start += delta;
374
+ end += delta;
375
+ }
376
+ start = Math.max(0, Math.min(start, next.length));
377
+ end = Math.max(start, Math.min(end, next.length));
378
+ if (start >= end)
379
+ updateFoldBlock(null);
380
+ else if (start !== block.start || end !== block.end)
381
+ updateFoldBlock({ start, end });
382
+ }
383
+ // The synchronous mirrors are what batch-dispatched events (one stdin
384
+ // read → several keys, no render in between) read on their next turn.
205
385
  valueRef.current = next;
206
- cursorRef.current = Math.max(0, Math.min(cursorOffset, next.length));
386
+ cursorRef.current = offset;
207
387
  setValue(next);
208
- setCursor(cursorRef.current);
388
+ setCursor(offset);
209
389
  };
210
390
  /**
211
391
  * Accept the selected file suggestion: replace ONLY the mention token at
212
392
  * the caret (prefix/suffix text survives), quoting whitespace paths. A
213
393
  * directory inserts `@dir/` without a trailing space so completion
214
- * continues into it; a file completes the token with a space.
394
+ * continues into it; a file completes the token with a space. A typed
395
+ * `#L12-14` suffix is NOT part of the replacement — completion ends at
396
+ * `pathEnd` so the line range survives acceptance (issue #359).
215
397
  */
216
398
  const acceptFile = (candidate) => {
217
399
  if (!mention)
218
400
  return;
219
401
  const file = candidate.path;
220
402
  const body = /\s/.test(file) ? `@"${file}"` : `@${file}`;
221
- const insert = candidate.kind === 'directory' ? body : `${body} `;
403
+ // A typed `#L12-14` suffix rides along AFTER the completed body (before
404
+ // the trailing space) — quoting a whitespace path must not detach it.
405
+ const suffix = mention.pathEnd === undefined ? '' : value.slice(mention.pathEnd, mention.end);
406
+ const insert = candidate.kind === 'directory' ? `${body}${suffix}` : `${body}${suffix} `;
222
407
  const next = value.slice(0, mention.start) + insert + value.slice(mention.end);
223
408
  setInput(next, mention.start + insert.length);
224
409
  setFileSelected(0);
@@ -295,6 +480,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
295
480
  return;
296
481
  }
297
482
  setInput(item.text);
483
+ updateFoldBlock(null);
298
484
  setSelectedCommand(0);
299
485
  setFileSelected(0);
300
486
  channel.notify(t('input-retracted'), { timeoutMs: 2000 });
@@ -356,13 +542,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
356
542
  return handled;
357
543
  };
358
544
  /** Clipboard reads are asynchronous; insert against the latest render so
359
- * typing while PowerShell owns the clipboard never gets overwritten. */
545
+ * typing while PowerShell owns the clipboard never gets overwritten.
546
+ * Returns the resulting value and the insertion offset so callers can
547
+ * apply the paste fold. */
360
548
  const insertClipboardAtCaret = (text) => {
361
549
  const current = valueRef.current;
362
550
  const position = cursorRef.current;
363
- setInput(current.slice(0, position) + text + current.slice(position), position + text.length);
551
+ const next = current.slice(0, position) + text + current.slice(position);
552
+ setInput(next, position + text.length);
364
553
  setSelectedCommand(0);
365
554
  setFileSelected(0);
555
+ return { next, at: position };
366
556
  };
367
557
  /** Line index of the cursor; -1 when the cursor is at the very end. */
368
558
  const cursorLine = (text, cursorOffset) => {
@@ -388,6 +578,42 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
388
578
  // the text/caret produced by the preceding event in that batch.
389
579
  const value = valueRef.current;
390
580
  const cursor = cursorRef.current;
581
+ // Fold block (CC-style): Esc expands it — it must NEVER clear text
582
+ // that LOOKS like one line; Backspace at the block's tail / Delete at
583
+ // its head deletes the WHOLE block in one key; ←/→ jump over the
584
+ // atomic block. Typing NEVER expands it — the caret lives outside the
585
+ // block and edits land in the visible text around it.
586
+ const block = foldBlockRef.current;
587
+ // Line-level moves/edits (↑/↓/Home/End/Ctrl+A/E/U/K/W) treat the
588
+ // block as an atomic row: boundaries that would land inside it are
589
+ // clamped to its edges.
590
+ const clampRowStart = (pos) => block && cursor >= block.end ? Math.max(pos, block.end) : pos;
591
+ const clampRowEnd = (pos) => block && cursor <= block.start ? Math.min(pos, block.start) : pos;
592
+ if (block) {
593
+ if (key.escape) {
594
+ updateFoldBlock(null);
595
+ return;
596
+ }
597
+ if ((key.backspace && cursor === block.end) || (key.delete && cursor === block.start)) {
598
+ const next = value.slice(0, block.start) + value.slice(block.end);
599
+ updateFoldBlock(null);
600
+ setInput(next, block.start);
601
+ setSelectedCommand(0);
602
+ setFileSelected(0);
603
+ return;
604
+ }
605
+ if (key.leftArrow && cursor === block.end) {
606
+ setInput(value, block.start);
607
+ return;
608
+ }
609
+ if (key.rightArrow && cursor === block.start) {
610
+ setInput(value, block.end);
611
+ return;
612
+ }
613
+ }
614
+ // Grapheme boundaries of the current text: every caret move / delete
615
+ // below snaps onto one of these offsets (never mid-cluster).
616
+ const bounds = graphemeBoundaries(value);
391
617
  /** Insert text at the caret (typing, paste) and dismiss overlays. */
392
618
  const insertAtCaret = (text) => {
393
619
  if (helpOpen)
@@ -401,13 +627,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
401
627
  // verbatim at the caret. Paste content may contain newlines — that is
402
628
  // NOT Enter — so this branch runs before the whole-line submit rule.
403
629
  if (event?.isPasted && input.length > 0) {
404
- insertAtCaret(input.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
630
+ const text = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
631
+ insertAtCaret(text);
632
+ // A big paste becomes a CC-style fold block right away (hover peeks
633
+ // at it); an existing block is replaced by the new paste's span.
634
+ if (isBigInput(text))
635
+ updateFoldBlock({ start: cursor, end: cursor + text.length });
405
636
  return;
406
637
  }
407
- // Ctrl+V / Cmd+V: raw mode hands the key to the app, so the clipboard is
408
- // read here — text, file paths when the file manager copied files, or an
409
- // exported temp-file path when the clipboard holds a raw image.
410
- if (isMod(key) && input === 'v') {
638
+ // Clipboard paste (default Ctrl+V / Cmd+V, plus the Alt+V alias for
639
+ // terminals that intercept Ctrl+V — combos are user-remappable via
640
+ // /settings): raw mode hands the key to the app, so the clipboard is
641
+ // read here — text, file paths when the file manager copied files, or
642
+ // an exported temp-file path when the clipboard holds a raw image.
643
+ if (actionMatches('paste', input, key)) {
411
644
  if (clipboardBusyRef.current)
412
645
  return;
413
646
  // Match insertAtCaret's overlay/selection dismissal up front: the
@@ -451,7 +684,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
451
684
  // Insert against the LIVE input state: the read above resolved
452
685
  // asynchronously and the user may have typed while waiting.
453
686
  const text = formatClipboardInsert(content);
454
- insertClipboardAtCaret(text);
687
+ const { at } = insertClipboardAtCaret(text);
688
+ // Same fold as bracketed paste: big clipboard text becomes a
689
+ // fold block covering exactly the pasted span.
690
+ if (isBigInput(text))
691
+ updateFoldBlock({ start: at, end: at + text.length });
455
692
  })
456
693
  .catch(() => {
457
694
  channel.notify(t('input-clipboard-read-failed'), { color: 'warning' });
@@ -463,27 +700,30 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
463
700
  });
464
701
  return;
465
702
  }
466
- // Help is modal for modified keys and every Enter variant. Ctrl+V above
467
- // is the intentional exception: paste closes Help and inserts visibly.
703
+ // Help is modal for modified keys and every Enter variant. The paste
704
+ // branch above is the intentional exception: paste closes Help and
705
+ // inserts visibly.
468
706
  // Swallow here before editor/submit/interrupt branches can mutate hidden
469
707
  // composer or working-turn state; plain typing still dismisses Help below.
470
708
  if (helpOpen && !key.escape && (key.ctrl || key.meta || key.super || key.return || input.includes('\n') || input.includes('\r'))) {
471
709
  event.stopImmediatePropagation();
472
710
  return;
473
711
  }
474
- // Ctrl+G: edit the current draft in $VISUAL/$EDITOR (issue #123,
475
- // readline's edit-and-execute-command). The draft is written to a temp
476
- // file, the terminal is handed to the editor (Ink's alt-screen handoff),
477
- // and the saved text replaces the input when it differs. The util maps
478
- // every failure to an outcome, but the catch/finally here is the hard
479
- // guarantee: a rejected promise must never kill the process, and the
480
- // busy flag must always clear or Ctrl+G stays locked forever.
481
- if (key.ctrl && input === 'g') {
712
+ // Ctrl+G (remappable via /settings): edit the current draft in
713
+ // $VISUAL/$EDITOR (issue #123, readline's edit-and-execute-command). The
714
+ // draft is written to a temp file, the terminal is handed to the editor
715
+ // (Ink's alt-screen handoff), and the saved text replaces the input when
716
+ // it differs. The util maps every failure to an outcome, but the
717
+ // catch/finally here is the hard guarantee: a rejected promise must
718
+ // never kill the process, and the busy flag must always clear or the
719
+ // editor key stays locked forever.
720
+ if (actionMatches('editor', input, key)) {
482
721
  editorBusyRef.current = true;
483
722
  void (async () => {
484
723
  try {
485
724
  const outcome = await editInExternalEditor(value);
486
725
  if (outcome.kind === 'edited') {
726
+ updateFoldBlock(null);
487
727
  setInput(outcome.text);
488
728
  setSelectedCommand(0);
489
729
  setFileSelected(0);
@@ -628,8 +868,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
628
868
  }
629
869
  if (key.tab && overlayOpen) {
630
870
  const command = suggestions[selectedCommand];
631
- if (command)
871
+ if (command) {
872
+ updateFoldBlock(null);
632
873
  setInput(command.replacement);
874
+ }
633
875
  return;
634
876
  }
635
877
  // Tab while the model is working = queue for AFTER the turn (followup),
@@ -683,6 +925,22 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
683
925
  setFileSelected(index => index <= 0 ? fileMatches.length - 1 : index - 1);
684
926
  return;
685
927
  }
928
+ // With a fold block the caret in the tail walks the TAIL's lines
929
+ // (the block is one atomic row); the first tail line steps over the
930
+ // block to its head.
931
+ if (block && cursor >= block.end) {
932
+ const tailCursor = cursor - block.end;
933
+ const line = cursorLine(tail, tailCursor);
934
+ if (line > 0) {
935
+ const upToLineStart = tail.lastIndexOf('\n', tailCursor - 1);
936
+ const prevLineStart = upToLineStart === -1 ? 0 : tail.lastIndexOf('\n', upToLineStart - 1) + 1;
937
+ const prevLine = tail.slice(prevLineStart, upToLineStart);
938
+ setInput(value, block.end + prevLineStart + Math.min(cursorColumn(tail, tailCursor), prevLine.length));
939
+ return;
940
+ }
941
+ setInput(value, block.start);
942
+ return;
943
+ }
686
944
  const line = cursorLine(value, cursor);
687
945
  if (line > 0) {
688
946
  // Move to the previous line, clamping to its length.
@@ -706,6 +964,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
706
964
  historyIndex.current = Math.max(0, historyIndex.current - 1);
707
965
  }
708
966
  const entry = history.current[historyIndex.current] ?? '';
967
+ updateFoldBlock(null);
709
968
  setInput(entry);
710
969
  return;
711
970
  }
@@ -714,6 +973,36 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
714
973
  setFileSelected(index => index >= fileMatches.length - 1 ? 0 : index + 1);
715
974
  return;
716
975
  }
976
+ // Mirror of the tail-side ↑ walk: with a fold block the caret walks
977
+ // the TAIL's lines; past its last line it falls through to the
978
+ // overlay/history handling below.
979
+ if (block && cursor >= block.end) {
980
+ const tailCursor = cursor - block.end;
981
+ const line = cursorLine(tail, tailCursor);
982
+ const lines = tail.split('\n');
983
+ if (line < lines.length - 1) {
984
+ const nextLineStart = tail.indexOf('\n', tailCursor) + 1;
985
+ const nextLineEnd = tail.indexOf('\n', nextLineStart);
986
+ const nextLine = tail.slice(nextLineStart, nextLineEnd === -1 ? tail.length : nextLineEnd);
987
+ setInput(value, block.end + nextLineStart + Math.min(cursorColumn(tail, tailCursor), nextLine.length));
988
+ return;
989
+ }
990
+ }
991
+ else if (block && cursor <= block.start) {
992
+ // Head side: walk the HEAD's lines; its last line steps over the
993
+ // block to the tail (never into history for a multi-row input).
994
+ const line = cursorLine(head, cursor);
995
+ const lines = head.split('\n');
996
+ if (line < lines.length - 1) {
997
+ const nextLineStart = head.indexOf('\n', cursor) + 1;
998
+ const nextLineEnd = head.indexOf('\n', nextLineStart);
999
+ const nextLine = head.slice(nextLineStart, nextLineEnd === -1 ? head.length : nextLineEnd);
1000
+ setInput(value, nextLineStart + Math.min(cursorColumn(head, cursor), nextLine.length));
1001
+ return;
1002
+ }
1003
+ setInput(value, block.end);
1004
+ return;
1005
+ }
717
1006
  const line = cursorLine(value, cursor);
718
1007
  const lines = value.split('\n');
719
1008
  if (line < lines.length - 1) {
@@ -731,6 +1020,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
731
1020
  return;
732
1021
  if (historyIndex.current >= history.current.length - 1) {
733
1022
  historyIndex.current = -1;
1023
+ updateFoldBlock(null);
734
1024
  setInput(historyDraft.current);
735
1025
  }
736
1026
  else {
@@ -751,63 +1041,68 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
751
1041
  return;
752
1042
  }
753
1043
  if (key.leftArrow) {
754
- setInput(value, Math.max(0, cursor - 1));
1044
+ // Grapheme-step: skip the whole cluster (surrogate pair, ZWJ emoji,
1045
+ // combining mark) so the caret never sits inside one.
1046
+ setInput(value, previousGraphemeBoundary(bounds, cursor));
755
1047
  return;
756
1048
  }
757
1049
  if (key.rightArrow) {
758
- setInput(value, Math.min(value.length, cursor + 1));
1050
+ setInput(value, nextGraphemeBoundary(bounds, cursor));
759
1051
  return;
760
1052
  }
761
1053
  if (key.backspace) {
762
1054
  if (cursor === 0)
763
1055
  return;
764
- setInput(value.slice(0, cursor - 1) + value.slice(cursor), cursor - 1);
1056
+ const start = previousGraphemeBoundary(bounds, cursor);
1057
+ setInput(value.slice(0, start) + value.slice(cursor), start);
765
1058
  return;
766
1059
  }
767
1060
  if (key.delete) {
768
- if (cursor >= value.length)
1061
+ const end = nextGraphemeBoundary(bounds, cursor);
1062
+ if (end === cursor)
769
1063
  return;
770
- setInput(value.slice(0, cursor) + value.slice(cursor + 1), cursor);
1064
+ setInput(value.slice(0, cursor) + value.slice(end), cursor);
771
1065
  return;
772
1066
  }
773
1067
  if (key.home) {
774
- // Start of the current line.
775
- const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
1068
+ // Start of the current line (the block is one atomic row).
1069
+ const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
776
1070
  setInput(value, lineStart);
777
1071
  return;
778
1072
  }
779
1073
  if (key.end) {
780
1074
  // End of the current line.
781
1075
  const nextLine = value.indexOf('\n', cursor);
782
- setInput(value, nextLine === -1 ? value.length : nextLine);
1076
+ setInput(value, nextLine === -1 ? value.length : clampRowEnd(nextLine));
783
1077
  return;
784
1078
  }
785
1079
  if (isMod(key) && input === 'a') {
786
- const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
1080
+ const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
787
1081
  setInput(value, lineStart);
788
1082
  return;
789
1083
  }
790
1084
  if (isMod(key) && input === 'e') {
791
1085
  const nextLine = value.indexOf('\n', cursor);
792
- setInput(value, nextLine === -1 ? value.length : nextLine);
1086
+ setInput(value, nextLine === -1 ? value.length : clampRowEnd(nextLine));
793
1087
  return;
794
1088
  }
795
1089
  if (isMod(key) && input === 'u') {
796
- // Delete to start of line.
797
- const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
1090
+ // Delete to start of line (never into the block).
1091
+ const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
798
1092
  setInput(value.slice(0, lineStart) + value.slice(cursor), lineStart);
799
1093
  return;
800
1094
  }
801
1095
  if (isMod(key) && input === 'k') {
802
- // Delete to end of line.
1096
+ // Delete to end of line (never into the block).
803
1097
  const nextLine = value.indexOf('\n', cursor);
804
- const end = nextLine === -1 ? value.length : nextLine;
1098
+ const end = nextLine === -1 ? value.length : clampRowEnd(nextLine);
805
1099
  setInput(value.slice(0, cursor) + value.slice(end), cursor);
806
1100
  return;
807
1101
  }
808
1102
  if (isMod(key) && input === 'w') {
809
1103
  // Delete the word before the cursor (CC/readline behavior): skip
810
- // trailing whitespace, then the whitespace-delimited word.
1104
+ // trailing whitespace, then the whitespace-delimited word. The
1105
+ // deletion start never crosses into the block.
811
1106
  const before = value.slice(0, cursor);
812
1107
  let end = before.length;
813
1108
  while (end > 0 && /\s/.test(before[end - 1]))
@@ -815,7 +1110,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
815
1110
  let start = end;
816
1111
  while (start > 0 && !/\s/.test(before[start - 1]))
817
1112
  start--;
818
- setInput(value.slice(0, start) + value.slice(cursor), start);
1113
+ const clipped = clampRowStart(start);
1114
+ setInput(value.slice(0, clipped) + value.slice(cursor), clipped);
819
1115
  return;
820
1116
  }
821
1117
  if (key.escape) {
@@ -848,8 +1144,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
848
1144
  return;
849
1145
  }
850
1146
  // A single Esc clears the current input (if any); the double-tap
851
- // path below handles rewind/clear on an already-empty input.
1147
+ // path below handles rewind/clear on an already-empty input. A BIG
1148
+ // input folds into a block instead (Esc = the fold toggle; the
1149
+ // expand → fold → expand cycle is lossless, and clearing a big draft
1150
+ // stays reachable via the block-delete Backspace or Ctrl+C).
852
1151
  if (value.length > 0) {
1152
+ if (isBigInput(value)) {
1153
+ updateFoldBlock({ start: 0, end: value.length });
1154
+ setSelectedCommand(0);
1155
+ setFileSelected(0);
1156
+ return;
1157
+ }
853
1158
  setInput('', 0);
854
1159
  setSelectedCommand(0);
855
1160
  setFileSelected(0);
@@ -894,9 +1199,37 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
894
1199
  // === Render: hard-wrap every logical line at the input width, then show
895
1200
  // the window of visual lines with the caret row always visible (CC's
896
1201
  // maxVisibleLines behavior with automatic wrapping).
897
- const inputWidth = Math.max(10, columns - 3);
898
- const visualLines = wrapToWidth(value, inputWidth);
899
- const caretVisualLine = wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
1202
+ // Narrow terminals: the usable width follows the real terminal down to a
1203
+ // single column — a fixed floor of 10 would wrap far too early and park
1204
+ // the declared cursor past the value box's actual width.
1205
+ const inputWidth = Math.max(1, columns - 3);
1206
+ const block = foldBlock;
1207
+ // Fold-block model: the value renders as [head rows][chip row][tail rows]
1208
+ // — the block is ONE atomic visual row regardless of its text size; text
1209
+ // before/after it stays fully editable and never unfolds it.
1210
+ const foldText = block ? value.slice(block.start, block.end) : value;
1211
+ const head = block ? value.slice(0, block.start) : '';
1212
+ const tail = block ? value.slice(block.end) : '';
1213
+ const headRows = block ? wrapToWidth(head, inputWidth) : [];
1214
+ const tailRows = block ? wrapToWidth(tail, inputWidth) : [];
1215
+ const chipRow = block ? headRows.length : -1;
1216
+ const visualLines = block
1217
+ ? [...headRows, '', ...tailRows]
1218
+ : wrapToWidth(value, inputWidth);
1219
+ const caretVisualLine = block
1220
+ ? cursor <= block.start
1221
+ ? wrapToWidth(value.slice(0, cursor), inputWidth).length - 1
1222
+ : headRows.length + 1 + wrapToWidth(value.slice(block.end, cursor), inputWidth).length - 1
1223
+ : wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
1224
+ // Fold stats describe the BLOCK (or the whole input when expanded and
1225
+ // the ▾ prefix offers a manual whole-input fold). The chip shows the
1226
+ // block's own line/char count + first-line preview — the preview text
1227
+ // around it is unaffected.
1228
+ const big = isBigInput(foldText);
1229
+ const stats = big
1230
+ ? t('input-fold-stats', { lines: foldText.split('\n').length, chars: foldText.length })
1231
+ : '';
1232
+ const peekOpen = block !== null && hovered && !selectionActive;
900
1233
  const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
901
1234
  const visibleLines = visualLines.slice(windowStart, windowStart + MAX_VISIBLE_LINES);
902
1235
  // Caret position in the caret's visual row, in two units:
@@ -906,29 +1239,94 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
906
1239
  // cursor mid-character and Windows Terminal would paint the IME
907
1240
  // preedit (pinyin) over the surrounding text).
908
1241
  const caretCharCol = () => {
909
- const before = value.slice(0, cursor);
1242
+ const before = block && cursor >= block.end ? value.slice(block.end, cursor) : value.slice(0, cursor);
910
1243
  const rows = wrapToWidth(before, inputWidth);
911
1244
  const last = rows[rows.length - 1] ?? '';
912
1245
  return last.length;
913
1246
  };
914
1247
  const caretVisualCol = () => {
915
- const before = value.slice(0, cursor);
1248
+ const before = block && cursor >= block.end ? value.slice(block.end, cursor) : value.slice(0, cursor);
916
1249
  const rows = wrapToWidth(before, inputWidth);
917
1250
  const last = rows[rows.length - 1] ?? '';
918
1251
  return stringWidth(last);
919
1252
  };
1253
+ // Folded chip content: block stats + first-line preview + hover hint,
1254
+ // all pre-truncated to the input width (the row is one line, always).
1255
+ const foldBadge = `▸ ${stats}`;
1256
+ const foldHint = t('input-fold-hover');
1257
+ const foldPreviewWidth = inputWidth - stringWidth(foldBadge) - stringWidth(` · ${foldHint}`) - 6;
1258
+ const foldPreview = foldPreviewWidth >= 8
1259
+ ? truncateToWidth(foldText.split('\n')[0] ?? '', foldPreviewWidth)
1260
+ : '';
1261
+ // Expanded-state fold affordance: a `▾` prefix at the start of the FIRST
1262
+ // row (only while the window is at the top and no block exists); its
1263
+ // cells fold the whole input into a block again on click.
1264
+ const prefixLabel = `▾ ${stats} · `;
1265
+ const prefixCols = !block && big && windowStart === 0 ? stringWidth(prefixLabel) : 0;
920
1266
  const rendered = visibleLines.map((line, index) => {
921
1267
  const absoluteLine = windowStart + index;
1268
+ if (block && absoluteLine === chipRow) {
1269
+ // The fold block's atomic chip row: click expands it; hover pops the
1270
+ // peek card. The row is one line no matter how big the block is.
1271
+ return (_jsxs(Box, { flexDirection: "row", onClick: (event) => {
1272
+ event.stopImmediatePropagation();
1273
+ updateFoldBlock(null);
1274
+ }, 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}`));
1275
+ }
1276
+ const withPrefix = absoluteLine === 0 && prefixCols > 0;
1277
+ const text = withPrefix ? truncateToWidth(line, inputWidth - prefixCols) : line;
1278
+ const prefix = withPrefix ? _jsx(Text, { dimColor: true, children: prefixLabel }) : null;
922
1279
  if (absoluteLine !== caretVisualLine) {
923
- return (_jsx(Text, { wrap: "truncate-end", children: line }, absoluteLine));
924
- }
925
- // Caret row: invert the char at the caret column (solid block).
926
- const col = caretCharCol();
927
- const before = line.slice(0, col);
928
- const at = line[col] ?? ' ';
929
- const after = line.slice(col + 1);
930
- return (_jsxs(Text, { wrap: "truncate-end", children: [before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
1280
+ return (_jsxs(Text, { wrap: "truncate-end", children: [prefix, text] }, absoluteLine));
1281
+ }
1282
+ // Caret row: invert the grapheme cluster at the caret column (solid
1283
+ // block). `col` is a cluster boundary by construction (the cursor is
1284
+ // normalized onto boundaries and wrapping only breaks between
1285
+ // graphemes), so [col, next boundary) covers the WHOLE cluster — a
1286
+ // surrogate pair or ZWJ emoji inverts as one glyph, never two broken
1287
+ // halves. Clamped into the prefix-shortened row text so the block
1288
+ // caret never renders under the fold prefix.
1289
+ const col = Math.min(caretCharCol(), text.length);
1290
+ const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(text), col);
1291
+ const before = text.slice(0, col);
1292
+ const at = clusterEnd > col ? text.slice(col, clusterEnd) : ' ';
1293
+ const after = text.slice(clusterEnd);
1294
+ return (_jsxs(Text, { wrap: "truncate-end", children: [prefix, before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
931
1295
  });
1296
+ // Peek card content: the BLOCK's text wrapped to the card's inner width,
1297
+ // capped at PEEK_MAX_ROWS visual rows (a preview — click to expand and
1298
+ // edit the real input). The footer reports the clipped remainder.
1299
+ const PEEK_MAX_ROWS = 10;
1300
+ const peekVisualLines = [];
1301
+ for (const line of foldText.split('\n')) {
1302
+ for (const row of wrapToWidth(line, cardContentWidth(columns))) {
1303
+ if (peekVisualLines.length >= PEEK_MAX_ROWS)
1304
+ break;
1305
+ peekVisualLines.push(row);
1306
+ }
1307
+ if (peekVisualLines.length >= PEEK_MAX_ROWS)
1308
+ break;
1309
+ }
1310
+ const peekClipped = peekVisualLines.length >= PEEK_MAX_ROWS;
1311
+ // Composer height shrink: clearing multi-line text (Enter/Esc/Ctrl+C/
1312
+ // Backspace) collapses the input area within one commit, shifting the
1313
+ // status line up and the whole chrome with it. The renderer's
1314
+ // full-damage pass (didLayoutShift) repaints the shifted siblings, but
1315
+ // inline mode's virtual↔scrollback correspondence needs the stronger
1316
+ // in-place viewport repaint — same treatment as Ctrl+O and the
1317
+ // loaded-context toggle (see Chat.tsx). One-shot, only on SHRINK:
1318
+ // growth scrolls the terminal naturally and needs no recovery.
1319
+ const contentRows = value.length === 0 ? 1 : visibleLines.length;
1320
+ noteAuxNumber('promptContentRows', contentRows);
1321
+ const prevContentRowsRef = React.useRef(contentRows);
1322
+ React.useLayoutEffect(() => {
1323
+ if (contentRows < prevContentRowsRef.current) {
1324
+ const ink = instances.get(process.stdout) ?? instances.values().next().value;
1325
+ ink?.invalidatePrevFrame();
1326
+ ink?.reanchorViewport();
1327
+ }
1328
+ prevContentRowsRef.current = contentRows;
1329
+ }, [contentRows]);
932
1330
  const lastNotification = channel.notifications[channel.notifications.length - 1];
933
1331
  // Park the native terminal cursor at the input caret (via the renderer's
934
1332
  // cursor-declaration mechanism). Terminal emulators render IME preedit
@@ -939,27 +1337,111 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
939
1337
  // relative to the value box the ref attaches to.
940
1338
  const valueBoxRef = useDeclaredCursor({
941
1339
  line: caretVisualLine - windowStart,
942
- column: caretVisualCol(),
1340
+ // Clamp the declared column to the wrap width: a grapheme wider than
1341
+ // the last remaining column (emoji at width 1) can push the visual
1342
+ // column past inputWidth, and the park must stay inside the value box.
1343
+ column: Math.min(caretVisualCol() + (caretVisualLine === 0 && prefixCols > 0 ? prefixCols : 0), inputWidth),
943
1344
  active: !selectionActive,
944
1345
  });
1346
+ /**
1347
+ * Click-to-position the caret: map a click inside the value box (local
1348
+ * row/column relative to the box) to a UTF-16 cursor offset via the same
1349
+ * grapheme walk the renderer wraps with — exact under CJK widths, wrapped
1350
+ * rows and multi-codepoint clusters. Clicks land on the boundary nearest
1351
+ * the clicked cell (mid-grapheme snaps to its start). With a fold block,
1352
+ * clicks map into the head/tail text (the chip row has its own expand
1353
+ * onClick); without one, the fold prefix's cells fold the whole input.
1354
+ */
1355
+ const handleValueClick = React.useCallback((e) => {
1356
+ const clickedVisual = windowStart + e.localRow;
1357
+ const clamped = Math.max(0, Math.min(clickedVisual, visualLines.length - 1));
1358
+ if (block) {
1359
+ if (clamped === chipRow)
1360
+ return;
1361
+ if (clamped < chipRow) {
1362
+ setCursor(clickToCursorOffset(head, inputWidth, clamped, e.localCol));
1363
+ }
1364
+ else {
1365
+ setCursor(block.end + clickToCursorOffset(tail, inputWidth, clamped - chipRow - 1, e.localCol));
1366
+ }
1367
+ return;
1368
+ }
1369
+ if (clamped === 0 && prefixCols > 0 && e.localCol < prefixCols) {
1370
+ updateFoldBlock({ start: 0, end: value.length });
1371
+ return;
1372
+ }
1373
+ const col = clamped === 0 && prefixCols > 0 ? Math.max(0, e.localCol - prefixCols) : e.localCol;
1374
+ setCursor(clickToCursorOffset(value, clamped === 0 && prefixCols > 0 ? inputWidth - prefixCols : inputWidth, clamped, col));
1375
+ }, [windowStart, visualLines.length, value, inputWidth, prefixCols, block, chipRow, head, tail]);
945
1376
  // 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
946
1377
  // absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
947
1378
  // 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
948
1379
  // 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
949
- const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
950
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [floatersOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 6, 4), children: [helpOpen && (_jsx(Box, { marginBottom: 1, children: _jsx(HelpMenu, { commands: channel.commandList, viewportHeight: helpViewportHeight, viewportWidth: columns, scrollRef: helpScrollRef }) })), !helpOpen && channel.pending.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingBottom: 1, children: [channel.pending.some(item => item.placement === 'steer') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u26A1 ", t('input-pending-steer-label')] }), channel.pending
1380
+ const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen || peekOpen;
1381
+ // 补全卡片边框与输入框 idle 边框同色(plan 模式下整套面板一起变 sage 绿)。
1382
+ // `/color` 会话强调色优先于主题 promptBorder(plan 模式仍整体走 sage 绿)。
1383
+ // `?? ''` 防御最小 mock channel(只声明用到的字段的回归脚本)。
1384
+ const sessionAccent = sessionColorHex(channel.sessionColor ?? '');
1385
+ const promptAccent = channel.mode.plan === true ? 'planMode' : (sessionAccent ?? 'promptBorder');
1386
+ // 顶边框右侧的会话名标签(CC 风格 chip):色随强调色;超宽截断,宽度
1387
+ // 随终端列数伸缩但不超过 28 显示单元。默认关闭——`/settings` 的
1388
+ // 「会话名标签」开关(dsh-tui.promptSessionLabel)开启后显示。
1389
+ const sessionTitle = channel.sessionTitle ?? '';
1390
+ const topRightLabel = channel.promptSessionLabel === true && sessionTitle !== ''
1391
+ ? {
1392
+ text: truncateToWidth(sessionTitle, Math.max(8, Math.min(28, columns - 8))),
1393
+ color: channel.mode.plan === true ? 'planMode' : (sessionAccent ?? 'claude'),
1394
+ ink: 'inverseText',
1395
+ }
1396
+ : undefined;
1397
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [floatersOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 6, 1), children: [helpOpen && (_jsx(Box, { marginBottom: 1, children: _jsx(HelpMenu, { commands: channel.commandList, viewportHeight: helpViewportHeight, viewportWidth: columns, scrollRef: helpScrollRef, onCommandPick: (name) => {
1398
+ // 点击命令行 = 填入 /name 并关闭帮助(Tab 补全的鼠标等价)
1399
+ updateFoldBlock(null);
1400
+ setInput(`/${name} `);
1401
+ onToggleHelp();
1402
+ } }) })), !helpOpen && channel.pending.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingBottom: 1, children: [channel.pending.some(item => item.placement === 'steer') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u26A1 ", t('input-pending-steer-label')] }), channel.pending
951
1403
  .filter(item => item.placement === 'steer')
952
1404
  .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), channel.pending.some(item => item.placement === 'followup') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u23F3 ", t('input-pending-queue-label')] }), channel.pending
953
1405
  .filter(item => item.placement === 'followup')
954
- .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(Box, { paddingLeft: 2, paddingBottom: 1, children: _jsx(FileSuggestions, { files: fileMatches, selectedIndex: fileSelected, columns: columns }) })), overlayOpen && (_jsx(Box, { paddingLeft: 2, paddingBottom: 1, children: _jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns }) }))] })), 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: channel.mode.plan === true ? 'planMode' : 'promptBorder', children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [_jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), _jsx(Box, { ref: valueBoxRef, flexGrow: 1, flexShrink: 1, 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 })) })] }) })] }));
1406
+ .map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(FileSuggestions, { files: fileMatches, selectedIndex: fileSelected, columns: columns, query: mention?.query ?? '', accent: promptAccent,
1407
+ // 点击行 = 接受该项(与 Enter 同路径)
1408
+ onPick: (index) => {
1409
+ const file = fileMatches[index];
1410
+ if (file)
1411
+ acceptFile(file);
1412
+ },
1413
+ // 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
1414
+ onWheelStep: (step) => {
1415
+ setFileSelected(i => Math.max(0, Math.min(fileMatches.length - 1, i + step)));
1416
+ } })), overlayOpen && (_jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns, query: value, accent: promptAccent,
1417
+ // 点击行 = 运行该命令(与 Enter 同路径)
1418
+ onPick: (index) => {
1419
+ const command = suggestions[index];
1420
+ if (command)
1421
+ tryRunCommand(command.commandLine);
1422
+ },
1423
+ // 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
1424
+ onWheelStep: (step) => {
1425
+ setSelectedCommand(i => Math.max(0, Math.min(suggestions.length - 1, i + step)));
1426
+ } })), peekOpen && (_jsx(Box, { onMouseEnter: hoverEnter, onMouseLeave: hoverLeave, children: _jsx(SuggestionCard, { title: stats, columns: columns, accent: promptAccent, footer: peekClipped
1427
+ ? t('input-fold-peek-footer', { lines: foldText.split('\n').length })
1428
+ : undefined, rows: peekVisualLines.map((row, index) => (_jsx(Text, { wrap: "truncate-end", children: row }, index))), onRowPick: () => {
1429
+ updateFoldBlock(null);
1430
+ setHovered(false);
1431
+ } }) }))] })), 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 }), _jsx(Box, { ref: valueBoxRef, flexGrow: 1, flexShrink: 1, onClick: handleValueClick, 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 })) })] }) })] }));
955
1432
  }
956
1433
  /**
957
1434
  * Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
958
1435
  * stringWidth). Used by the input renderer so long lines wrap instead of
959
- * truncating, with exact caret-row mapping.
1436
+ * truncating, with exact caret-row mapping. Wrapping only ever breaks
1437
+ * BETWEEN grapheme clusters: iterating code points would split ZWJ emoji
1438
+ * and combining sequences across rows, leaving a broken half at each edge
1439
+ * and desyncing the caret's row arithmetic (which walks cluster
1440
+ * boundaries).
960
1441
  */
961
1442
  function wrapToWidth(text, width) {
962
1443
  const rows = [];
1444
+ const segmenter = getGraphemeSegmenter();
963
1445
  for (const line of text.split('\n')) {
964
1446
  if (line === '') {
965
1447
  rows.push('');
@@ -967,15 +1449,15 @@ function wrapToWidth(text, width) {
967
1449
  }
968
1450
  let current = '';
969
1451
  let currentWidth = 0;
970
- for (const ch of line) {
971
- const w = stringWidth(ch);
1452
+ for (const { segment } of segmenter.segment(line)) {
1453
+ const w = stringWidth(segment);
972
1454
  if (currentWidth + w > width && current !== '') {
973
1455
  rows.push(current);
974
- current = ch;
1456
+ current = segment;
975
1457
  currentWidth = w;
976
1458
  }
977
1459
  else {
978
- current += ch;
1460
+ current += segment;
979
1461
  currentWidth += w;
980
1462
  }
981
1463
  }
@@ -983,3 +1465,48 @@ function wrapToWidth(text, width) {
983
1465
  }
984
1466
  return rows;
985
1467
  }
1468
+ /**
1469
+ * Inverse of {@link wrapToWidth}: map a click position (visual row index +
1470
+ * visual column) back to a UTF-16 offset in the original text. Walks the
1471
+ * same grapheme boundaries with the same break rule, so every visual row's
1472
+ * start offset is known exactly. Within the clicked row, the caret snaps to
1473
+ * the boundary nearest the click: a grapheme whose midpoint lies past the
1474
+ * click column takes the caret before it, otherwise after.
1475
+ */
1476
+ function clickToCursorOffset(text, width, visualLine, visualCol) {
1477
+ const segmenter = getGraphemeSegmenter();
1478
+ let row = 0;
1479
+ let offset = 0;
1480
+ for (const line of text.split('\n')) {
1481
+ if (line === '') {
1482
+ if (row === visualLine)
1483
+ return offset;
1484
+ row++;
1485
+ offset++; // the '\n'
1486
+ continue;
1487
+ }
1488
+ let currentWidth = 0;
1489
+ for (const { segment } of segmenter.segment(line)) {
1490
+ const w = stringWidth(segment);
1491
+ if (currentWidth + w > width && currentWidth > 0) {
1492
+ if (row === visualLine)
1493
+ return offset; // end of the clicked wrapped row
1494
+ row++;
1495
+ currentWidth = 0;
1496
+ }
1497
+ if (row === visualLine) {
1498
+ if (currentWidth + w / 2 > visualCol)
1499
+ return offset;
1500
+ if (currentWidth + w > visualCol)
1501
+ return offset + segment.length;
1502
+ }
1503
+ currentWidth += w;
1504
+ offset += segment.length;
1505
+ }
1506
+ if (row === visualLine)
1507
+ return offset;
1508
+ row++;
1509
+ offset++; // the '\n'
1510
+ }
1511
+ return offset;
1512
+ }