@vibe-forge/client 0.8.4 → 0.10.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 (301) hide show
  1. package/AGENTS.md +75 -0
  2. package/dist/assets/abap-DLDM7-KI.js +1 -0
  3. package/dist/assets/apex-DNDY2TF8.js +1 -0
  4. package/dist/assets/{arc-DCuZPvAs.js → arc-CCXV7u3V.js} +1 -1
  5. package/dist/assets/azcli-Y6nb8tq_.js +1 -0
  6. package/dist/assets/bat-BwHxbl9M.js +1 -0
  7. package/dist/assets/bicep-CFznDFnq.js +2 -0
  8. package/dist/assets/{blockDiagram-c4efeb88-C39d7-Bu.js → blockDiagram-c4efeb88-Bm52FmvT.js} +1 -1
  9. package/dist/assets/{c4Diagram-c83219d4-jWPBJdeq.js → c4Diagram-c83219d4-C8tTEpcK.js} +1 -1
  10. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  11. package/dist/assets/channel-gq_WMRvv.js +1 -0
  12. package/dist/assets/{classDiagram-beda092f-Bo7Yvv2T.js → classDiagram-beda092f-CNAIBAH1.js} +1 -1
  13. package/dist/assets/{classDiagram-v2-2358418a-DfoCP9XM.js → classDiagram-v2-2358418a-BHeZAVdc.js} +1 -1
  14. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  15. package/dist/assets/clone-XxGY7A5N.js +1 -0
  16. package/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
  17. package/dist/assets/coffee-Bd8akH9Z.js +1 -0
  18. package/dist/assets/cpp-BbWJElDN.js +1 -0
  19. package/dist/assets/{createText-1719965b-8_Ez5rxh.js → createText-1719965b-BS2hLG8t.js} +1 -1
  20. package/dist/assets/csharp-Co3qMtFm.js +1 -0
  21. package/dist/assets/csp-D-4FJmMZ.js +1 -0
  22. package/dist/assets/css-DdJfP1eB.js +3 -0
  23. package/dist/assets/css.worker-BvV5MPou.js +93 -0
  24. package/dist/assets/cssMode-WHcTFAOU.js +1 -0
  25. package/dist/assets/cypher-cTPe9QuQ.js +1 -0
  26. package/dist/assets/dart-BOtBlQCF.js +1 -0
  27. package/dist/assets/dockerfile-BG73LgW2.js +1 -0
  28. package/dist/assets/ecl-BEgZUVRK.js +1 -0
  29. package/dist/assets/{edges-96097737-BEegO-R-.js → edges-96097737-C07f4iWA.js} +1 -1
  30. package/dist/assets/editor.worker-CKy7Pnvo.js +26 -0
  31. package/dist/assets/elixir-BkW5O-1t.js +1 -0
  32. package/dist/assets/{erDiagram-0228fc6a-derRgkLz.js → erDiagram-0228fc6a-BytsAWUs.js} +1 -1
  33. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  34. package/dist/assets/{flowDb-c6c81e3f-ChtOxgsJ.js → flowDb-c6c81e3f-CQJkOpAs.js} +1 -1
  35. package/dist/assets/{flowDiagram-50d868cf-Bm4ufmmA.js → flowDiagram-50d868cf-CD5Tng2S.js} +1 -1
  36. package/dist/assets/flowDiagram-v2-4f6560a1-DIBOANLV.js +1 -0
  37. package/dist/assets/{flowchart-elk-definition-6af322e1-BRYLO-BL.js → flowchart-elk-definition-6af322e1-ylso-GWH.js} +1 -1
  38. package/dist/assets/freemarker2-U_9Jyyr3.js +3 -0
  39. package/dist/assets/fsharp-PahG7c26.js +1 -0
  40. package/dist/assets/{ganttDiagram-a2739b55-CbP6dzRO.js → ganttDiagram-a2739b55-Cg98bJx5.js} +1 -1
  41. package/dist/assets/{gitGraphDiagram-82fe8481-Du44v02s.js → gitGraphDiagram-82fe8481-7Yp4hz0N.js} +1 -1
  42. package/dist/assets/go-acbASCJo.js +1 -0
  43. package/dist/assets/{graph-0_VzJX6O.js → graph-Ig3nvzvL.js} +1 -1
  44. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  45. package/dist/assets/handlebars-DQyCBwHe.js +1 -0
  46. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  47. package/dist/assets/html-CNC2AT5k.js +1 -0
  48. package/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  49. package/dist/assets/htmlMode-DlATk4xW.js +1 -0
  50. package/dist/assets/{index-5325376f-DLP7F7of.js → index-5325376f-C4zed9sb.js} +1 -1
  51. package/dist/assets/index-DRLsOoqb.css +32 -0
  52. package/dist/assets/index-Dbx0JG0p.js +1511 -0
  53. package/dist/assets/{infoDiagram-8eee0895-DY19rRl6.js → infoDiagram-8eee0895-C8oSBaFs.js} +1 -1
  54. package/dist/assets/ini-Kd9XrMLS.js +1 -0
  55. package/dist/assets/java-CXBNlu9o.js +1 -0
  56. package/dist/assets/javascript-9wv9uKW4.js +1 -0
  57. package/dist/assets/{journeyDiagram-c64418c1-4Asnwc86.js → journeyDiagram-c64418c1-D5kJldvy.js} +1 -1
  58. package/dist/assets/json.worker-usMZ-FED.js +58 -0
  59. package/dist/assets/jsonMode-45dv39mU.js +7 -0
  60. package/dist/assets/julia-cl7-CwDS.js +1 -0
  61. package/dist/assets/kotlin-s7OhZKlX.js +1 -0
  62. package/dist/assets/{layout-BILp7GjD.js → layout-DYNFLnIl.js} +1 -1
  63. package/dist/assets/less-9HpZscsL.js +2 -0
  64. package/dist/assets/lexon-OrD6JF1K.js +1 -0
  65. package/dist/assets/{line-D0Xqr8mi.js → line-1gvOYQYZ.js} +1 -1
  66. package/dist/assets/{linear-LpL8RZsq.js → linear-DHGm6Zdw.js} +1 -1
  67. package/dist/assets/liquid-BGoxrdXO.js +1 -0
  68. package/dist/assets/lspLanguageFeatures-CpCCXhrd.js +4 -0
  69. package/dist/assets/lua-Cyyb5UIc.js +1 -0
  70. package/dist/assets/m3-B8OfTtLu.js +1 -0
  71. package/dist/assets/markdown-BFxVWTOG.js +1 -0
  72. package/dist/assets/mdx-Dc2iMbEw.js +1 -0
  73. package/dist/assets/{mermaid.core-Bk0Y_0sz.js → mermaid.core-Cq2bBFF1.js} +6 -6
  74. package/dist/assets/{mindmap-definition-8da855dc-eCAgn5kY.js → mindmap-definition-8da855dc-y5l6GRVh.js} +1 -1
  75. package/dist/assets/mips-CiqrrVzr.js +1 -0
  76. package/dist/assets/msdax-DmeGPVcC.js +1 -0
  77. package/dist/assets/mysql-C_tMU-Nz.js +1 -0
  78. package/dist/assets/objective-c-BDtDVThU.js +1 -0
  79. package/dist/assets/pascal-vHIfCaH5.js +1 -0
  80. package/dist/assets/pascaligo-DtZ0uQbO.js +1 -0
  81. package/dist/assets/perl-Ub6l9XKa.js +1 -0
  82. package/dist/assets/pgsql-BlNEE0v7.js +1 -0
  83. package/dist/assets/php-BBUBE1dy.js +1 -0
  84. package/dist/assets/{pieDiagram-a8764435-BeGFFS1p.js → pieDiagram-a8764435-PNROcv9y.js} +1 -1
  85. package/dist/assets/pla-DSh2-awV.js +1 -0
  86. package/dist/assets/postiats-CocnycG-.js +1 -0
  87. package/dist/assets/powerquery-tScXyioY.js +1 -0
  88. package/dist/assets/powershell-COWaemsV.js +1 -0
  89. package/dist/assets/protobuf-Brw8urJB.js +2 -0
  90. package/dist/assets/pug-8SOpv6rk.js +1 -0
  91. package/dist/assets/python-DjYAge7h.js +1 -0
  92. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  93. package/dist/assets/{quadrantDiagram-1e28029f-tLqbYOHC.js → quadrantDiagram-1e28029f-CFJ3VPpp.js} +1 -1
  94. package/dist/assets/r-j7ic8hl3.js +1 -0
  95. package/dist/assets/razor-OIY8fx_i.js +1 -0
  96. package/dist/assets/redis-Bu5POkcn.js +1 -0
  97. package/dist/assets/redshift-Bs9aos_-.js +1 -0
  98. package/dist/assets/{requirementDiagram-08caed73-DCverr_g.js → requirementDiagram-08caed73-BpzDIINS.js} +1 -1
  99. package/dist/assets/restructuredtext-CqXO7rUv.js +1 -0
  100. package/dist/assets/ruby-zBfavPgS.js +1 -0
  101. package/dist/assets/rust-BzKRNQWT.js +1 -0
  102. package/dist/assets/{sankeyDiagram-a04cb91d-DidhF5PL.js → sankeyDiagram-a04cb91d-CH69-iIn.js} +1 -1
  103. package/dist/assets/sb-BBc9UKZt.js +1 -0
  104. package/dist/assets/scala-D9hQfWCl.js +1 -0
  105. package/dist/assets/scheme-BPhDTwHR.js +1 -0
  106. package/dist/assets/scss-CBJaRo0y.js +3 -0
  107. package/dist/assets/{sequenceDiagram-c5b8d532-CEKaiwTo.js → sequenceDiagram-c5b8d532-yBBEeVFU.js} +1 -1
  108. package/dist/assets/shell-DiJ1NA_G.js +1 -0
  109. package/dist/assets/solidity-Db0IVjzk.js +1 -0
  110. package/dist/assets/sophia-CnS9iZB_.js +1 -0
  111. package/dist/assets/sparql-CJmd_6j2.js +1 -0
  112. package/dist/assets/sql-ClhHkBeG.js +1 -0
  113. package/dist/assets/st-CHwy0fLd.js +1 -0
  114. package/dist/assets/{stateDiagram-1ecb1508-Cj9ZY7RH.js → stateDiagram-1ecb1508-BvF4sign.js} +1 -1
  115. package/dist/assets/{stateDiagram-v2-c2b004d7-Gsmps-dk.js → stateDiagram-v2-c2b004d7-BeyT7Ghx.js} +1 -1
  116. package/dist/assets/{styles-b4e223ce-D7E8Th0j.js → styles-b4e223ce-C58zxmK6.js} +1 -1
  117. package/dist/assets/{styles-ca3715f6-tTC26Jsm.js → styles-ca3715f6-DCE5sFi5.js} +1 -1
  118. package/dist/assets/{styles-d45a18b0-Bf6oqNdR.js → styles-d45a18b0-CG-C1aM8.js} +1 -1
  119. package/dist/assets/{svgDrawCommon-b86b1483-BxKi01m2.js → svgDrawCommon-b86b1483-F-K8GeDd.js} +1 -1
  120. package/dist/assets/swift-Bqt4WxQ4.js +3 -0
  121. package/dist/assets/systemverilog-Bs9z6M-B.js +1 -0
  122. package/dist/assets/tcl-Dm6ycUr_.js +1 -0
  123. package/dist/assets/{timeline-definition-faaaa080-D6PePEip.js → timeline-definition-faaaa080-DPv4uqVX.js} +1 -1
  124. package/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  125. package/dist/assets/tsMode-BtU8ZELV.js +11 -0
  126. package/dist/assets/twig-Csy3S7wG.js +1 -0
  127. package/dist/assets/typescript-CJHgISWo.js +1 -0
  128. package/dist/assets/typespec-Btyra-wh.js +1 -0
  129. package/dist/assets/vb-Db0cS2oM.js +1 -0
  130. package/dist/assets/wgsl-BTesnYfV.js +298 -0
  131. package/dist/assets/xml-C_TJw4Bi.js +1 -0
  132. package/dist/assets/{xychartDiagram-f5964ef8-BuP4qfXm.js → xychartDiagram-f5964ef8-BmXlhBzX.js} +1 -1
  133. package/dist/assets/yaml-BujeJOJ6.js +1 -0
  134. package/dist/favicon.svg +3 -3
  135. package/dist/index.html +2 -2
  136. package/package.json +14 -10
  137. package/public/favicon.svg +3 -3
  138. package/src/api/sessions.ts +36 -0
  139. package/src/api/workspace.ts +19 -0
  140. package/src/api.ts +4 -0
  141. package/src/components/NavRail.scss +9 -10
  142. package/src/components/ShortcutDisplay.scss +38 -0
  143. package/src/components/ShortcutDisplay.tsx +37 -0
  144. package/src/components/ShortcutTooltip.scss +36 -0
  145. package/src/components/ShortcutTooltip.tsx +84 -0
  146. package/src/components/Sidebar.scss +55 -13
  147. package/src/components/Sidebar.tsx +141 -52
  148. package/src/components/chat/AGENTS.md +163 -0
  149. package/src/components/chat/ChatHeader.scss +308 -49
  150. package/src/components/chat/ChatHeader.tsx +394 -80
  151. package/src/components/chat/ChatHistoryView.tsx +284 -69
  152. package/src/components/chat/ChatSettingsView.tsx +5 -3
  153. package/src/components/chat/ChatTimelineView.scss +3 -2
  154. package/src/components/chat/{sender/ThinkingStatus.tsx → ThinkingStatus.tsx} +1 -1
  155. package/src/components/chat/messages/MessageContextMenu.scss +145 -0
  156. package/src/components/chat/messages/MessageContextMenu.tsx +108 -0
  157. package/src/components/chat/messages/MessageContextMenuContent.tsx +87 -0
  158. package/src/components/chat/messages/MessageFooter.tsx +44 -24
  159. package/src/components/chat/messages/MessageItem.scss +147 -10
  160. package/src/components/chat/messages/MessageItem.tsx +378 -13
  161. package/src/components/chat/messages/build-message-context-menu-entries.ts +166 -0
  162. package/src/components/chat/messages/message-content-utils.ts +121 -0
  163. package/src/components/chat/messages/message-turns.ts +88 -0
  164. package/src/components/chat/messages/message-utils.ts +19 -1
  165. package/src/components/chat/sender/@components/adapter-select/AdapterSelectControl.scss +86 -0
  166. package/src/components/chat/sender/@components/adapter-select/AdapterSelectControl.tsx +54 -0
  167. package/src/components/chat/sender/@components/adapter-select/AdapterSelectDropdown.scss +42 -0
  168. package/src/components/chat/sender/@components/effort-select/EffortSelectControl.scss +68 -0
  169. package/src/components/chat/sender/@components/effort-select/EffortSelectControl.tsx +137 -0
  170. package/src/components/chat/sender/@components/effort-select/EffortSelectDropdown.scss +96 -0
  171. package/src/components/chat/sender/@components/model-select/ModelSelectControl.scss +82 -0
  172. package/src/components/chat/sender/@components/model-select/ModelSelectControl.tsx +171 -0
  173. package/src/components/chat/sender/@components/model-select/ModelSelectMenu.scss +95 -0
  174. package/src/components/chat/sender/@components/model-select/ModelSelectMenuLabels.scss +144 -0
  175. package/src/components/chat/sender/@components/model-select/ModelSelectOptionLabel.tsx +109 -0
  176. package/src/components/chat/sender/@components/reference-actions/ReferenceActionsControl.scss +106 -0
  177. package/src/components/chat/sender/@components/reference-actions/ReferenceActionsControl.tsx +156 -0
  178. package/src/components/chat/sender/@components/reference-actions/ReferenceActionsOption.scss +34 -0
  179. package/src/components/chat/sender/@components/reference-actions/ReferencePermissionActionsPopover.tsx +111 -0
  180. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.scss +103 -0
  181. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.tsx +47 -0
  182. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +137 -0
  183. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.scss +178 -0
  184. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.tsx +145 -0
  185. package/src/components/chat/sender/@components/sender-monaco-editor/SenderMonacoEditor.scss +47 -0
  186. package/src/components/chat/sender/@components/sender-monaco-editor/SenderMonacoEditor.tsx +121 -0
  187. package/src/components/chat/sender/@components/sender-monaco-editor/monaco-runtime.ts +99 -0
  188. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-editor-handle.ts +48 -0
  189. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-monaco-editor.ts +209 -0
  190. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-monaco-theme.ts +24 -0
  191. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.scss +54 -0
  192. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.tsx +80 -0
  193. package/src/components/chat/sender/@components/sender-toolbar/SenderSelectBase.scss +71 -0
  194. package/src/components/chat/sender/@components/sender-toolbar/SenderSelectShared.scss +118 -0
  195. package/src/components/chat/sender/@components/sender-toolbar/SenderToolbar.scss +99 -0
  196. package/src/components/chat/sender/@components/sender-toolbar/SenderToolbar.tsx +87 -0
  197. package/src/components/chat/sender/@core/build-sender-controller-result.ts +119 -0
  198. package/src/components/chat/sender/@core/build-sender-toolbar.ts +122 -0
  199. package/src/components/chat/sender/@core/content-attachments.ts +76 -0
  200. package/src/components/chat/sender/@core/create-sender-toolbar-handlers.ts +115 -0
  201. package/src/components/chat/sender/@core/get-sender-interaction-state.ts +18 -0
  202. package/src/components/chat/sender/@core/get-sender-runtime-state.ts +14 -0
  203. package/src/components/chat/sender/@core/interaction-request.ts +5 -0
  204. package/src/components/chat/sender/@core/sender-toolbar-bindings.ts +155 -0
  205. package/src/components/chat/sender/@hooks/use-model-select-browser.tsx +189 -0
  206. package/src/components/chat/sender/@hooks/use-sender-attachments.ts +143 -0
  207. package/src/components/chat/sender/@hooks/use-sender-autofocus.ts +34 -0
  208. package/src/components/chat/sender/@hooks/use-sender-completion.ts +62 -0
  209. package/src/components/chat/sender/@hooks/use-sender-composer-state.ts +34 -0
  210. package/src/components/chat/sender/@hooks/use-sender-controller.ts +193 -0
  211. package/src/components/chat/sender/@hooks/use-sender-focus-restore.ts +72 -0
  212. package/src/components/chat/sender/@hooks/use-sender-history.ts +79 -0
  213. package/src/components/chat/sender/@hooks/use-sender-keydown.ts +113 -0
  214. package/src/components/chat/sender/@hooks/use-sender-reference-actions.ts +191 -0
  215. package/src/components/chat/sender/@hooks/use-sender-reference-focus-restore.ts +21 -0
  216. package/src/components/chat/sender/@hooks/use-sender-refs.ts +19 -0
  217. package/src/components/chat/sender/@hooks/use-sender-select-overlays.ts +83 -0
  218. package/src/components/chat/sender/@hooks/use-sender-shortcuts.ts +78 -0
  219. package/src/components/chat/sender/@hooks/use-sender-submit.ts +81 -0
  220. package/src/components/chat/sender/@types/sender-composer.ts +19 -0
  221. package/src/components/chat/sender/@types/sender-editor.ts +12 -0
  222. package/src/components/chat/sender/@types/sender-props.ts +50 -0
  223. package/src/components/chat/sender/@types/sender-toolbar-types.ts +83 -0
  224. package/src/components/chat/sender/@types/sender-types.ts +21 -0
  225. package/src/components/chat/sender/@utils/sender-completion.ts +164 -0
  226. package/src/components/chat/sender/@utils/sender-constants.ts +18 -0
  227. package/src/components/chat/sender/@utils/sender-utils.ts +45 -0
  228. package/src/components/chat/sender/Sender.scss +8 -605
  229. package/src/components/chat/sender/Sender.tsx +54 -880
  230. package/src/components/chat/session-metadata.ts +55 -0
  231. package/src/components/chat/terminal/@hooks/use-terminal-instance.ts +152 -0
  232. package/src/components/chat/terminal/@hooks/use-terminal-session.ts +196 -0
  233. package/src/components/chat/terminal/ChatTerminalView.scss +62 -0
  234. package/src/components/chat/terminal/ChatTerminalView.tsx +114 -0
  235. package/src/components/chat/tools/core/ToolGroup.scss +7 -0
  236. package/src/components/chat/tools/core/ToolGroup.tsx +97 -56
  237. package/src/components/config/ConfigSectionForm.tsx +8 -1
  238. package/src/components/config/ConfigShortcutInput.tsx +9 -2
  239. package/src/components/config/configSchema.ts +12 -2
  240. package/src/components/config/record-editors/ModelServicesRecordEditor.tsx +0 -14
  241. package/src/components/dock-panel/DockPanel.scss +152 -0
  242. package/src/components/dock-panel/DockPanel.tsx +195 -0
  243. package/src/components/layout/AppShell.scss +40 -2
  244. package/src/components/layout/AppShell.tsx +25 -10
  245. package/src/components/sidebar/SessionContextMenu.scss +143 -0
  246. package/src/components/sidebar/SessionContextMenu.tsx +196 -0
  247. package/src/components/sidebar/SessionContextMenuContent.tsx +89 -0
  248. package/src/components/sidebar/SessionItem.scss +150 -67
  249. package/src/components/sidebar/SessionItem.tsx +183 -134
  250. package/src/components/sidebar/SessionList.scss +47 -17
  251. package/src/components/sidebar/SessionList.tsx +31 -16
  252. package/src/components/sidebar/SidebarHeader.scss +329 -49
  253. package/src/components/sidebar/SidebarHeader.tsx +108 -86
  254. package/src/components/sidebar/SidebarHeaderBatchActions.tsx +81 -0
  255. package/src/components/sidebar/SidebarHeaderSearchActions.tsx +176 -0
  256. package/src/components/sidebar/SidebarHeaderSelectField.tsx +24 -0
  257. package/src/components/sidebar/filter-utils.ts +23 -0
  258. package/src/components/workspace/ContextFilePicker.scss +64 -0
  259. package/src/components/workspace/ContextFilePicker.tsx +171 -0
  260. package/src/connectionManager.ts +4 -2
  261. package/src/hooks/chat/interaction-state.ts +104 -0
  262. package/src/hooks/chat/model-selector-data-builders.ts +146 -0
  263. package/src/hooks/chat/model-selector-data-option-utils.ts +62 -0
  264. package/src/hooks/chat/model-selector-data-types.ts +27 -0
  265. package/src/hooks/chat/model-selector-data.ts +109 -0
  266. package/src/hooks/chat/model-selector-recommendations.ts +69 -0
  267. package/src/hooks/chat/model-selector.ts +9 -0
  268. package/src/hooks/chat/use-chat-interaction.ts +13 -8
  269. package/src/hooks/chat/use-chat-model-adapter-selection.tsx +167 -164
  270. package/src/hooks/chat/use-chat-models.tsx +46 -23
  271. package/src/hooks/chat/use-chat-session-actions.ts +69 -23
  272. package/src/hooks/chat/use-chat-session-messages.ts +165 -60
  273. package/src/hooks/chat/use-chat-session.ts +34 -9
  274. package/src/hooks/chat/use-chat-view.ts +26 -6
  275. package/src/hooks/chat/use-composer-control-shortcuts.ts +69 -0
  276. package/src/hooks/chat/use-terminal-dock-visibility.ts +39 -0
  277. package/src/hooks/use-roving-focus-list.ts +104 -0
  278. package/src/hooks/use-sidebar-navigation.ts +9 -4
  279. package/src/hooks/use-sidebar-query-state.ts +79 -0
  280. package/src/main.tsx +6 -1
  281. package/src/resources/locales/en.json +151 -6
  282. package/src/resources/locales/zh.json +151 -6
  283. package/src/routes/ChatRoute.scss +159 -4
  284. package/src/routes/ChatRoute.tsx +43 -9
  285. package/src/runtime-config.ts +21 -0
  286. package/src/store/index.ts +1 -3
  287. package/src/styles/global.scss +12 -2
  288. package/src/utils/chat-links.ts +21 -0
  289. package/src/utils/copy.ts +18 -0
  290. package/src/utils/shortcutUtils.ts +111 -1
  291. package/src/vite-env.d.ts +1 -0
  292. package/src/ws.ts +6 -5
  293. package/vite.config.ts +71 -7
  294. package/dist/assets/channel-bLjHfx-Q.js +0 -1
  295. package/dist/assets/clone-upfY39Je.js +0 -1
  296. package/dist/assets/flowDiagram-v2-4f6560a1-F66FzZTY.js +0 -1
  297. package/dist/assets/index-C1O04Df8.js +0 -557
  298. package/dist/assets/index-sE8VA1N7.css +0 -1
  299. package/src/components/chat/sender/CompletionMenu.scss +0 -70
  300. package/src/components/chat/sender/CompletionMenu.tsx +0 -58
  301. /package/src/components/chat/{sender/ThinkingStatus.scss → ThinkingStatus.scss} +0 -0
@@ -1,10 +1,10 @@
1
1
  import { App } from 'antd'
2
2
  import { useCallback, useEffect, useState } from 'react'
3
3
  import { useTranslation } from 'react-i18next'
4
- import { useNavigate } from 'react-router-dom'
4
+ import { useLocation, useNavigate } from 'react-router-dom'
5
5
  import { useSWRConfig } from 'swr'
6
6
 
7
- import { createSession, getApiErrorMessage } from '#~/api.js'
7
+ import { branchSessionFromMessage, createSession, getApiErrorMessage } from '#~/api.js'
8
8
  import { connectionManager } from '#~/connectionManager.js'
9
9
  import type { ChatMessageContent, Session } from '@vibe-forge/core'
10
10
  import type { ChatEffort } from './use-chat-effort'
@@ -30,10 +30,32 @@ export function useChatSessionActions({
30
30
  const { message } = App.useApp()
31
31
  const { t } = useTranslation()
32
32
  const navigate = useNavigate()
33
+ const location = useLocation()
33
34
  const { mutate } = useSWRConfig()
34
35
  const [isCreating, setIsCreating] = useState(false)
35
36
  const isThinking = isCreating || session?.status === 'running'
36
37
 
38
+ const navigateWithCurrentSearch = useCallback((pathname: string) => {
39
+ void navigate({
40
+ pathname,
41
+ search: location.search
42
+ })
43
+ }, [location.search, navigate])
44
+
45
+ const insertSessionIntoCache = useCallback(async (newSession: Session) => {
46
+ await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
47
+ if (!prev?.sessions) {
48
+ return { sessions: [newSession] }
49
+ }
50
+
51
+ const withoutCurrent = prev.sessions.filter((item) => item.id !== newSession.id)
52
+ return {
53
+ ...prev,
54
+ sessions: [newSession, ...withoutCurrent]
55
+ }
56
+ }, false)
57
+ }, [mutate])
58
+
37
59
  useEffect(() => {
38
60
  if (!isCreating || session?.id == null || session.id === '') return
39
61
  setIsCreating(false)
@@ -55,15 +77,9 @@ export function useChatSessionActions({
55
77
  adapter
56
78
  })
57
79
 
58
- await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
59
- if (!prev?.sessions) return { sessions: [newSession] }
60
- return {
61
- ...prev,
62
- sessions: [newSession, ...prev.sessions]
63
- }
64
- }, false)
80
+ await insertSessionIntoCache(newSession)
65
81
 
66
- void navigate(`/session/${newSession.id}`)
82
+ navigateWithCurrentSearch(`/session/${newSession.id}`)
67
83
  return true
68
84
  } catch (err) {
69
85
  console.error(err)
@@ -82,9 +98,9 @@ export function useChatSessionActions({
82
98
  adapter,
83
99
  hasAvailableModels,
84
100
  isThinking,
101
+ insertSessionIntoCache,
85
102
  message,
86
- mutate,
87
- navigate,
103
+ navigateWithCurrentSearch,
88
104
  effort,
89
105
  permissionMode,
90
106
  modelForQuery,
@@ -108,15 +124,9 @@ export function useChatSessionActions({
108
124
  adapter
109
125
  })
110
126
 
111
- await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
112
- if (!prev?.sessions) return { sessions: [newSession] }
113
- return {
114
- ...prev,
115
- sessions: [newSession, ...prev.sessions]
116
- }
117
- }, false)
127
+ await insertSessionIntoCache(newSession)
118
128
 
119
- void navigate(`/session/${newSession.id}`)
129
+ navigateWithCurrentSearch(`/session/${newSession.id}`)
120
130
  return true
121
131
  } catch (err) {
122
132
  console.error(err)
@@ -134,10 +144,10 @@ export function useChatSessionActions({
134
144
  }, [
135
145
  adapter,
136
146
  hasAvailableModels,
147
+ insertSessionIntoCache,
137
148
  isThinking,
149
+ navigateWithCurrentSearch,
138
150
  message,
139
- mutate,
140
- navigate,
141
151
  effort,
142
152
  permissionMode,
143
153
  modelForQuery,
@@ -157,12 +167,48 @@ export function useChatSessionActions({
157
167
  void message.success('Messages cleared')
158
168
  }, [message, onClearMessages])
159
169
 
170
+ const runMessageAction = useCallback(async (
171
+ messageId: string,
172
+ action: 'fork' | 'recall' | 'edit',
173
+ options?: { content?: string | ChatMessageContent[] }
174
+ ) => {
175
+ if (session?.id == null || session.id === '') {
176
+ return false
177
+ }
178
+
179
+ try {
180
+ const { session: newSession } = await branchSessionFromMessage(session.id, messageId, action, options)
181
+ await insertSessionIntoCache(newSession)
182
+ navigateWithCurrentSearch(`/session/${newSession.id}`)
183
+ return true
184
+ } catch (err) {
185
+ console.error(err)
186
+ void message.error(getApiErrorMessage(err, t('common.operationFailed')))
187
+ return false
188
+ }
189
+ }, [insertSessionIntoCache, message, navigateWithCurrentSearch, session?.id, t])
190
+
191
+ const forkMessage = useCallback((messageId: string) => {
192
+ return runMessageAction(messageId, 'fork')
193
+ }, [runMessageAction])
194
+
195
+ const recallMessage = useCallback((messageId: string) => {
196
+ return runMessageAction(messageId, 'recall')
197
+ }, [runMessageAction])
198
+
199
+ const editMessage = useCallback((messageId: string, content: string | ChatMessageContent[]) => {
200
+ return runMessageAction(messageId, 'edit', { content })
201
+ }, [runMessageAction])
202
+
160
203
  return {
161
204
  isCreating,
162
205
  isThinking,
163
206
  send,
164
207
  sendContent,
208
+ editMessage,
209
+ forkMessage,
165
210
  interrupt,
166
- clearMessages
211
+ clearMessages,
212
+ recallMessage
167
213
  }
168
214
  }
@@ -6,6 +6,13 @@ import { getSessionMessages } from '#~/api.js'
6
6
  import { connectionManager } from '#~/connectionManager.js'
7
7
  import type { AskUserQuestionParams, ChatMessage, Session, WSEvent } from '@vibe-forge/core'
8
8
  import type { SessionInfo } from '@vibe-forge/types'
9
+ import type { ChatErrorBannerState } from './interaction-state'
10
+ import {
11
+ applyInteractionStateEvent,
12
+ findLatestFatalError,
13
+ getFatalSessionError,
14
+ restoreInteractionStateFromHistory
15
+ } from './interaction-state'
9
16
  import type { ChatEffort } from './use-chat-effort'
10
17
  import type { PermissionMode } from './use-chat-permission-mode'
11
18
 
@@ -56,19 +63,118 @@ export function useChatSessionMessages({
56
63
  const [messages, setMessages] = useState<ChatMessage[]>([])
57
64
  const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null)
58
65
  const [isReady, setIsReady] = useState(false)
59
- const [connectionError, setConnectionError] = useState<string | null>(null)
66
+ const [errorBanner, setErrorBanner] = useState<ChatErrorBannerState | null>(null)
60
67
  const [retryCount, setRetryCount] = useState(0)
61
68
  const isInitialLoadRef = useRef<boolean>(true)
62
69
  const lastConnectedModelRef = useRef<string | undefined>(undefined)
63
70
  const lastConnectedEffortRef = useRef<string | undefined>(undefined)
64
71
  const lastConnectedPermissionModeRef = useRef<string | undefined>(undefined)
65
72
  const lastConnectedAdapterRef = useRef<string | undefined>(undefined)
73
+ const lastObservedSessionStatusRef = useRef<Session['status'] | undefined>(session?.status)
66
74
  const expectedCloseRef = useRef(false)
75
+ const interactionRequestRef = useRef<{ id: string; payload: AskUserQuestionParams } | null>(null)
76
+ const activeSessionIdRef = useRef<string | undefined>(session?.id)
77
+ const historyRequestSeqRef = useRef(0)
78
+ const reconcileTimersRef = useRef<Array<ReturnType<typeof setTimeout>>>([])
79
+
80
+ activeSessionIdRef.current = session?.id
81
+
82
+ const clearScheduledReconciles = useCallback(() => {
83
+ for (const timer of reconcileTimersRef.current) {
84
+ clearTimeout(timer)
85
+ }
86
+ reconcileTimersRef.current = []
87
+ }, [])
88
+
89
+ const refreshHistory = useCallback(async (options: { updateReadiness?: boolean } = {}) => {
90
+ const sessionId = activeSessionIdRef.current
91
+ if (sessionId == null || sessionId === '') {
92
+ return
93
+ }
94
+
95
+ const requestSeq = ++historyRequestSeqRef.current
96
+
97
+ try {
98
+ const res = await getSessionMessages(sessionId)
99
+ if (activeSessionIdRef.current !== sessionId || historyRequestSeqRef.current !== requestSeq) {
100
+ return
101
+ }
102
+
103
+ const events = res.messages as WSEvent[]
104
+
105
+ if (res.session) {
106
+ const updatedSession = res.session
107
+ void mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
108
+ if (prev?.sessions == null) return prev
109
+ const newSessions = prev.sessions.map((s: Session) =>
110
+ s.id === updatedSession.id ? { ...s, ...updatedSession } : s
111
+ )
112
+ return { ...prev, sessions: newSessions }
113
+ }, false)
114
+ }
115
+
116
+ let currentMessages: ChatMessage[] = []
117
+ let currentSessionInfo: SessionInfo | null = null
118
+ const restoredInteraction = restoreInteractionStateFromHistory(
119
+ events,
120
+ res.interaction ?? null,
121
+ res.session?.status
122
+ )
123
+ const latestFatalError = findLatestFatalError(events)
124
+
125
+ interactionRequestRef.current = restoredInteraction
126
+ setInteractionRequest(restoredInteraction)
127
+ setErrorBanner(
128
+ restoredInteraction == null && res.session?.status === 'failed' && latestFatalError != null
129
+ ? {
130
+ kind: 'session',
131
+ message: latestFatalError.message
132
+ }
133
+ : null
134
+ )
135
+
136
+ for (const data of events) {
137
+ currentMessages = applyMessageEvent(currentMessages, data)
138
+ currentMessages = applyToolResultEvent(currentMessages, data)
139
+ if (data.type === 'session_info') {
140
+ if (data.info != null && data.info.type !== 'summary') {
141
+ currentSessionInfo = data.info
142
+ }
143
+ }
144
+ }
145
+
146
+ setMessages(currentMessages)
147
+ setSessionInfo(currentSessionInfo)
148
+
149
+ if (options.updateReadiness !== false) {
150
+ setTimeout(() => {
151
+ if (activeSessionIdRef.current !== sessionId || historyRequestSeqRef.current !== requestSeq) {
152
+ return
153
+ }
154
+ setIsReady(true)
155
+ isInitialLoadRef.current = false
156
+ }, 100)
157
+ }
158
+ } catch (err) {
159
+ console.error('Failed to fetch history messages:', err)
160
+ }
161
+ }, [mutate, setInteractionRequest])
162
+
163
+ const reconcileAfterInteraction = useCallback(() => {
164
+ clearScheduledReconciles()
165
+
166
+ for (const delay of [0, 800, 2400, 5000, 9000, 15000]) {
167
+ const timer = globalThis.setTimeout(() => {
168
+ void refreshHistory({ updateReadiness: false })
169
+ }, delay)
170
+ reconcileTimersRef.current.push(timer)
171
+ }
172
+ }, [clearScheduledReconciles, refreshHistory])
67
173
 
68
174
  const retryConnection = useCallback(() => {
69
175
  if (session?.id == null || session.id === '') return
70
176
  expectedCloseRef.current = true
71
- setConnectionError(null)
177
+ setErrorBanner(null)
72
178
  connectionManager.close(session.id)
73
179
  setRetryCount((count) => count + 1)
74
180
  }, [session?.id])
@@ -77,8 +183,9 @@ export function useChatSessionMessages({
77
183
  setMessages([])
78
184
  setSessionInfo(null)
79
185
  setIsReady(false)
80
- setConnectionError(null)
186
+ setErrorBanner(null)
81
187
  setInteractionRequest(null)
188
+ interactionRequestRef.current = null
82
189
  isInitialLoadRef.current = true
83
190
 
84
191
  if (session?.id == null || session.id === '') {
@@ -87,64 +194,32 @@ export function useChatSessionMessages({
87
194
  lastConnectedEffortRef.current = undefined
88
195
  lastConnectedPermissionModeRef.current = undefined
89
196
  lastConnectedAdapterRef.current = undefined
197
+ clearScheduledReconciles()
90
198
  return
91
199
  }
92
200
 
93
- let isDisposed = false
94
-
95
- const fetchHistory = async () => {
96
- try {
97
- const res = await getSessionMessages(session.id)
98
- if (isDisposed) return
99
- const events = res.messages as WSEvent[]
100
-
101
- if (res.session) {
102
- const updatedSession = res.session
103
- void mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
104
- if (prev?.sessions == null) return prev
105
- const newSessions = prev.sessions.map((s: Session) =>
106
- s.id === updatedSession.id ? { ...s, ...updatedSession } : s
107
- )
108
- return { ...prev, sessions: newSessions }
109
- }, false)
110
- }
111
-
112
- if (res.interaction) {
113
- setInteractionRequest(res.interaction)
114
- }
115
-
116
- let currentMessages: ChatMessage[] = []
117
- let currentSessionInfo: SessionInfo | null = null
118
-
119
- for (const data of events) {
120
- currentMessages = applyMessageEvent(currentMessages, data)
121
- currentMessages = applyToolResultEvent(currentMessages, data)
122
- if (data.type === 'session_info') {
123
- if (data.info != null && data.info.type !== 'summary') {
124
- currentSessionInfo = data.info
125
- }
126
- }
127
- }
201
+ void refreshHistory()
128
202
 
129
- setMessages(currentMessages)
130
- setSessionInfo(currentSessionInfo)
203
+ return () => {
204
+ clearScheduledReconciles()
205
+ }
206
+ }, [clearScheduledReconciles, refreshHistory, session?.id, setInteractionRequest])
131
207
 
132
- setTimeout(() => {
133
- if (isDisposed) return
134
- setIsReady(true)
135
- isInitialLoadRef.current = false
136
- }, 100)
137
- } catch (err) {
138
- console.error('Failed to fetch history messages:', err)
139
- }
208
+ useEffect(() => {
209
+ if (session?.id == null || session.id === '') {
210
+ lastObservedSessionStatusRef.current = undefined
211
+ return
140
212
  }
141
213
 
142
- void fetchHistory()
214
+ const previousStatus = lastObservedSessionStatusRef.current
215
+ lastObservedSessionStatusRef.current = session.status
143
216
 
144
- return () => {
145
- isDisposed = true
217
+ if (previousStatus == null || previousStatus === session.status) {
218
+ return
146
219
  }
147
- }, [mutate, session?.id, setInteractionRequest])
220
+
221
+ void refreshHistory({ updateReadiness: false })
222
+ }, [refreshHistory, session?.id, session?.status])
148
223
 
149
224
  useEffect(() => {
150
225
  if (session?.id == null || session.id === '') {
@@ -174,7 +249,7 @@ export function useChatSessionMessages({
174
249
  session?.status !== 'running'
175
250
  if (modelChanged || effortChanged || permissionModeChanged || adapterChanged) {
176
251
  expectedCloseRef.current = true
177
- setConnectionError(null)
252
+ setErrorBanner(null)
178
253
  connectionManager.send(session.id, { type: 'terminate_session' })
179
254
  connectionManager.close(session.id)
180
255
  }
@@ -203,12 +278,30 @@ export function useChatSessionMessages({
203
278
  cleanup = connectionManager.connect(session.id, {
204
279
  onOpen() {
205
280
  expectedCloseRef.current = false
206
- setConnectionError(null)
281
+ setErrorBanner((current) => current?.kind === 'session' ? current : null)
207
282
  },
208
283
  onMessage(data: WSEvent) {
209
284
  if (isDisposed) return
285
+ const nextInteraction = applyInteractionStateEvent(interactionRequestRef.current, data)
286
+ if (nextInteraction !== interactionRequestRef.current) {
287
+ interactionRequestRef.current = nextInteraction
288
+ setInteractionRequest(nextInteraction)
289
+ if (nextInteraction != null) {
290
+ setErrorBanner(null)
291
+ }
292
+ }
293
+ if (data.type === 'interaction_response') {
294
+ reconcileAfterInteraction()
295
+ return
296
+ }
210
297
  if (data.type === 'error') {
211
- setConnectionError(data.data?.message ?? data.message ?? 'Unknown error')
298
+ const fatalError = getFatalSessionError(data)
299
+ if (fatalError != null) {
300
+ setErrorBanner({
301
+ kind: 'session',
302
+ message: fatalError.message
303
+ })
304
+ }
212
305
  return
213
306
  }
214
307
 
@@ -269,12 +362,15 @@ export function useChatSessionMessages({
269
362
  }
270
363
 
271
364
  if (data.type === 'interaction_request') {
272
- setInteractionRequest({ id: data.id, payload: data.payload })
365
+ setInteractionRequest(data)
273
366
  }
274
367
  },
275
368
  onError() {
276
369
  if (isDisposed) return
277
- setConnectionError(t('chat.connectionError'))
370
+ setErrorBanner({
371
+ kind: 'connection',
372
+ message: t('chat.connectionError')
373
+ })
278
374
  },
279
375
  onClose() {
280
376
  if (isDisposed) return
@@ -282,7 +378,12 @@ export function useChatSessionMessages({
282
378
  expectedCloseRef.current = false
283
379
  return
284
380
  }
285
- setConnectionError((current) => current ?? t('chat.connectionClosed'))
381
+ setErrorBanner((current) =>
382
+ current ?? {
383
+ kind: 'connection',
384
+ message: t('chat.connectionClosed')
385
+ }
386
+ )
286
387
  }
287
388
  }, Object.keys(connectionParams).length > 0 ? connectionParams : undefined)
288
389
  }, (modelChanged || effortChanged || permissionModeChanged || adapterChanged) ? 200 : 100)
@@ -294,11 +395,14 @@ export function useChatSessionMessages({
294
395
  }
295
396
  }, [
296
397
  adapter,
398
+ clearScheduledReconciles,
297
399
  effort,
298
400
  modelForQuery,
299
401
  mutate,
300
402
  permissionMode,
403
+ reconcileAfterInteraction,
301
404
  retryCount,
405
+ refreshHistory,
302
406
  session?.id,
303
407
  session?.status,
304
408
  setInteractionRequest,
@@ -310,7 +414,8 @@ export function useChatSessionMessages({
310
414
  setMessages,
311
415
  sessionInfo,
312
416
  isReady,
313
- connectionError,
314
- retryConnection
417
+ errorBanner,
418
+ retryConnection,
419
+ reconcileAfterInteraction
315
420
  }
316
421
  }
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from 'react'
1
+ import { useCallback, useEffect, useRef } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
4
  import type { Session } from '@vibe-forge/core'
@@ -18,23 +18,37 @@ export function useChatSession({
18
18
  const {
19
19
  adapterOptions,
20
20
  applySessionSelection,
21
+ modelMenuGroups,
21
22
  selectedAdapter,
22
23
  selectedModel,
23
24
  selectedModelWithService,
24
25
  setSelectedModel,
25
26
  setSelectedAdapter,
26
- modelOptions,
27
+ modelSearchOptions,
28
+ recommendedModelOptions,
29
+ servicePreviewModelOptions,
30
+ toggleRecommendedModel,
31
+ updatingRecommendedModelValue,
27
32
  hasAvailableModels
28
33
  } = useChatModelAdapterSelection({
29
34
  adapterLocked: session?.id != null
30
35
  })
31
36
  const { permissionMode, setPermissionMode, permissionModeOptions } = useChatPermissionMode()
32
37
  const { effort, setEffort, effortOptions } = useChatEffort()
33
- const { activeView, setActiveView } = useChatView()
34
- const { interactionRequest, setInteractionRequest, handleInteractionResponse } = useChatInteraction({
35
- sessionId: session?.id
36
- })
37
- const { messages, setMessages, sessionInfo, isReady, connectionError, retryConnection } = useChatSessionMessages({
38
+ const { activeView, isTerminalOpen, setActiveView, setIsTerminalOpen } = useChatView()
39
+ const { interactionRequest, setInteractionRequest, handleInteractionResponse: submitInteractionResponse } =
40
+ useChatInteraction({
41
+ sessionId: session?.id
42
+ })
43
+ const {
44
+ messages,
45
+ setMessages,
46
+ sessionInfo,
47
+ isReady,
48
+ errorBanner,
49
+ retryConnection,
50
+ reconcileAfterInteraction
51
+ } = useChatSessionMessages({
38
52
  session,
39
53
  modelForQuery: selectedModelWithService,
40
54
  effort,
@@ -42,6 +56,10 @@ export function useChatSession({
42
56
  adapter: selectedAdapter,
43
57
  setInteractionRequest
44
58
  })
59
+ const handleInteractionResponse = useCallback((id: string, data: string | string[]) => {
60
+ reconcileAfterInteraction()
61
+ submitInteractionResponse(id, data)
62
+ }, [reconcileAfterInteraction, submitInteractionResponse])
45
63
  const lastObservedSessionRef = useRef<Pick<Session, 'id' | 'model' | 'permissionMode' | 'adapter' | 'effort'> | null>(
46
64
  null
47
65
  )
@@ -94,15 +112,22 @@ export function useChatSession({
94
112
  sessionInfo,
95
113
  interactionRequest,
96
114
  isReady,
97
- connectionError,
115
+ errorBanner,
98
116
  retryConnection,
99
117
  isThinking,
100
118
  activeView,
119
+ isTerminalOpen,
101
120
  setActiveView,
121
+ setIsTerminalOpen,
102
122
  handleInteractionResponse,
103
123
  setMessages,
104
124
  placeholder: !session?.id ? t('chat.newSessionPlaceholder') : undefined,
105
- modelOptions,
125
+ modelMenuGroups,
126
+ modelSearchOptions,
127
+ recommendedModelOptions,
128
+ servicePreviewModelOptions,
129
+ toggleRecommendedModel,
130
+ updatingRecommendedModelValue,
106
131
  selectedModel,
107
132
  modelForQuery: selectedModelWithService,
108
133
  setSelectedModel,
@@ -11,29 +11,49 @@ const normalizeView = (value: string): ChatHeaderView => {
11
11
  }
12
12
 
13
13
  export function useChatView() {
14
- const { values: queryValues, update: updateQuery } = useQueryParams<{ view: string }>({
15
- keys: ['view'],
14
+ const { values: queryValues, update: updateQuery } = useQueryParams<{ terminal: string; view: string }>({
15
+ keys: ['view', 'terminal'],
16
16
  defaults: {
17
- view: 'history'
17
+ view: 'history',
18
+ terminal: ''
18
19
  },
19
20
  omit: {
20
- view: (value) => value === 'history'
21
+ view: (value) => value === 'history',
22
+ terminal: (value) => value === '' || value === 'false'
21
23
  }
22
24
  })
23
25
 
24
- const activeView = normalizeView(queryValues.view)
26
+ const legacyTerminalView = queryValues.view === 'terminal'
27
+ const activeView = normalizeView(legacyTerminalView ? 'history' : queryValues.view)
28
+ const isTerminalOpen = queryValues.terminal === 'true' || legacyTerminalView
29
+
25
30
  const setActiveView = useCallback((view: ChatHeaderView) => {
26
31
  updateQuery({ view })
27
32
  }, [updateQuery])
28
33
 
34
+ const setIsTerminalOpen = useCallback((nextOpen: boolean) => {
35
+ updateQuery({ terminal: nextOpen ? 'true' : 'false' })
36
+ }, [updateQuery])
37
+
29
38
  useEffect(() => {
30
39
  if (activeView !== queryValues.view) {
31
40
  updateQuery({ view: activeView })
32
41
  }
33
42
  }, [activeView, queryValues.view, updateQuery])
34
43
 
44
+ useEffect(() => {
45
+ if (legacyTerminalView) {
46
+ updateQuery({
47
+ view: 'history',
48
+ terminal: 'true'
49
+ })
50
+ }
51
+ }, [legacyTerminalView, updateQuery])
52
+
35
53
  return {
36
54
  activeView,
37
- setActiveView
55
+ isTerminalOpen,
56
+ setActiveView,
57
+ setIsTerminalOpen
38
58
  }
39
59
  }
@@ -0,0 +1,69 @@
1
+ import { useMemo } from 'react'
2
+
3
+ import type { Config } from '@vibe-forge/types'
4
+
5
+ import { useGlobalShortcut } from '../useGlobalShortcut'
6
+
7
+ type ComposerShortcutConfig = NonNullable<Config['shortcuts']>
8
+
9
+ export const composerControlShortcutDefaults = {
10
+ switchModel: 'mod+shift+m',
11
+ switchEffort: 'mod+t',
12
+ switchPermissionMode: 'mod+p'
13
+ } satisfies Pick<ComposerShortcutConfig, 'switchModel' | 'switchEffort' | 'switchPermissionMode'>
14
+
15
+ export const resolveComposerControlShortcuts = (
16
+ shortcuts?: Config['shortcuts']
17
+ ) => {
18
+ const resolveShortcut = (value: string | undefined, fallback: string) => {
19
+ return value != null && value.trim() !== '' ? value : fallback
20
+ }
21
+
22
+ return {
23
+ switchModel: resolveShortcut(shortcuts?.switchModel, composerControlShortcutDefaults.switchModel),
24
+ switchEffort: resolveShortcut(shortcuts?.switchEffort, composerControlShortcutDefaults.switchEffort),
25
+ switchPermissionMode: resolveShortcut(
26
+ shortcuts?.switchPermissionMode,
27
+ composerControlShortcutDefaults.switchPermissionMode
28
+ )
29
+ }
30
+ }
31
+
32
+ export const useComposerControlShortcuts = ({
33
+ enabled = true,
34
+ isMac,
35
+ shortcuts,
36
+ onSwitchModel,
37
+ onSwitchEffort,
38
+ onSwitchPermissionMode
39
+ }: {
40
+ enabled?: boolean
41
+ isMac: boolean
42
+ shortcuts?: Config['shortcuts']
43
+ onSwitchModel: (event: KeyboardEvent) => void
44
+ onSwitchEffort: (event: KeyboardEvent) => void
45
+ onSwitchPermissionMode: (event: KeyboardEvent) => void
46
+ }) => {
47
+ const resolvedShortcuts = useMemo(() => resolveComposerControlShortcuts(shortcuts), [shortcuts])
48
+
49
+ useGlobalShortcut({
50
+ shortcut: resolvedShortcuts.switchModel,
51
+ enabled,
52
+ isMac,
53
+ onTrigger: onSwitchModel
54
+ })
55
+ useGlobalShortcut({
56
+ shortcut: resolvedShortcuts.switchEffort,
57
+ enabled,
58
+ isMac,
59
+ onTrigger: onSwitchEffort
60
+ })
61
+ useGlobalShortcut({
62
+ shortcut: resolvedShortcuts.switchPermissionMode,
63
+ enabled,
64
+ isMac,
65
+ onTrigger: onSwitchPermissionMode
66
+ })
67
+
68
+ return resolvedShortcuts
69
+ }