@vibe-forge/client 0.9.0 → 0.10.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 (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-D9HxM0yv.js → arc-C1rWFTer.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-C_pTeiD4.js → blockDiagram-c4efeb88-DlZ9x70F.js} +1 -1
  9. package/dist/assets/{c4Diagram-c83219d4-_oYrCrLr.js → c4Diagram-c83219d4-BKKxi__y.js} +1 -1
  10. package/dist/assets/cameligo-Bf6VGUru.js +1 -0
  11. package/dist/assets/channel-F1aqMANO.js +1 -0
  12. package/dist/assets/{classDiagram-beda092f-9dnkgPhR.js → classDiagram-beda092f-CVGPySZq.js} +1 -1
  13. package/dist/assets/{classDiagram-v2-2358418a-BUIeZpdO.js → classDiagram-v2-2358418a-7kp8GVVj.js} +1 -1
  14. package/dist/assets/clojure-Dnu-v4kV.js +1 -0
  15. package/dist/assets/clone-B-GCuXNo.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-DuRFplcD.js → createText-1719965b-Dykv8kT9.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-B59COYVW.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-CTpq6Ih_.js → edges-96097737-CkZ1ZBro.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-CWFgdO1E.js → erDiagram-0228fc6a-281ADcRp.js} +1 -1
  33. package/dist/assets/flow9-BeJ5waoc.js +1 -0
  34. package/dist/assets/{flowDb-c6c81e3f-8Vdx29oZ.js → flowDb-c6c81e3f-BQjX_flP.js} +1 -1
  35. package/dist/assets/{flowDiagram-50d868cf-6S9wOVNl.js → flowDiagram-50d868cf-DMHZTjES.js} +1 -1
  36. package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +1 -0
  37. package/dist/assets/{flowchart-elk-definition-6af322e1-DVcZ-ZBQ.js → flowchart-elk-definition-6af322e1-CI3yz4z8.js} +1 -1
  38. package/dist/assets/freemarker2-DWnWjibn.js +3 -0
  39. package/dist/assets/fsharp-PahG7c26.js +1 -0
  40. package/dist/assets/{ganttDiagram-a2739b55-DY1QSl-x.js → ganttDiagram-a2739b55-B3IING9L.js} +1 -1
  41. package/dist/assets/{gitGraphDiagram-82fe8481-BP08OBKL.js → gitGraphDiagram-82fe8481-CnArIr_T.js} +1 -1
  42. package/dist/assets/go-acbASCJo.js +1 -0
  43. package/dist/assets/{graph-D8GCszcN.js → graph-BZ1F0Yve.js} +1 -1
  44. package/dist/assets/graphql-BxJiqAUM.js +1 -0
  45. package/dist/assets/handlebars-C1QH9qTz.js +1 -0
  46. package/dist/assets/hcl-DtV1sZF8.js +1 -0
  47. package/dist/assets/html-D1NkqHjC.js +1 -0
  48. package/dist/assets/html.worker-BLJhxQJQ.js +470 -0
  49. package/dist/assets/htmlMode-DAZCE_rA.js +1 -0
  50. package/dist/assets/{index-5325376f-OOoAXU1u.js → index-5325376f-Da9zSHjA.js} +1 -1
  51. package/dist/assets/index-C0vjF3D0.js +1511 -0
  52. package/dist/assets/index-vzEbM21t.css +32 -0
  53. package/dist/assets/{infoDiagram-8eee0895-BHgkXjhJ.js → infoDiagram-8eee0895-DYbFvRM7.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-CoMjGRHa.js +1 -0
  57. package/dist/assets/{journeyDiagram-c64418c1-BljInElp.js → journeyDiagram-c64418c1-Boebox0b.js} +1 -1
  58. package/dist/assets/json.worker-usMZ-FED.js +58 -0
  59. package/dist/assets/jsonMode-D__gAvuz.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-Cw-SwAFD.js → layout-CTcHNbHp.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-Q-uZLuUr.js → line-4AwinCz2.js} +1 -1
  66. package/dist/assets/{linear-BahOnNn3.js → linear-CeSMLzJW.js} +1 -1
  67. package/dist/assets/liquid-DZF6egdE.js +1 -0
  68. package/dist/assets/lspLanguageFeatures-6K4lv5S2.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-Cnt4ka6w.js +1 -0
  73. package/dist/assets/{mermaid.core-BR0Qh3Rd.js → mermaid.core-B0yG5s4D.js} +6 -6
  74. package/dist/assets/{mindmap-definition-8da855dc-Brq2OJUZ.js → mindmap-definition-8da855dc-KJEvXMKj.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-uhYdiXro.js → pieDiagram-a8764435-17nFAXPJ.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-DA3TtjDv.js +1 -0
  92. package/dist/assets/qsharp-Bw9ernYp.js +1 -0
  93. package/dist/assets/{quadrantDiagram-1e28029f-TwqtEe7e.js → quadrantDiagram-1e28029f-Dt4vubi-.js} +1 -1
  94. package/dist/assets/r-j7ic8hl3.js +1 -0
  95. package/dist/assets/razor-CWDJgvX_.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-DSXnPRDS.js → requirementDiagram-08caed73-H6aDyDK-.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-EADpJeAn.js → sankeyDiagram-a04cb91d-DxsVtbjI.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-C8qXvt9z.js → sequenceDiagram-c5b8d532-BHa148XJ.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-5W0ghy_S.js → stateDiagram-1ecb1508-DgwBm8LO.js} +1 -1
  115. package/dist/assets/{stateDiagram-v2-c2b004d7-BfwOQJw4.js → stateDiagram-v2-c2b004d7-BK7IQLVc.js} +1 -1
  116. package/dist/assets/{styles-b4e223ce-EkKH_ULb.js → styles-b4e223ce-DzW27Bc-.js} +1 -1
  117. package/dist/assets/{styles-ca3715f6-DwhrtqlU.js → styles-ca3715f6-Dex2GiLT.js} +1 -1
  118. package/dist/assets/{styles-d45a18b0-yqL8KH6x.js → styles-d45a18b0-B6fGtDKS.js} +1 -1
  119. package/dist/assets/{svgDrawCommon-b86b1483--5VjnjhC.js → svgDrawCommon-b86b1483-B4HYgfV5.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-CWOTwik9.js → timeline-definition-faaaa080--QSbWb25.js} +1 -1
  124. package/dist/assets/ts.worker-DGHjMaqB.js +67731 -0
  125. package/dist/assets/tsMode-ZM7ocZCH.js +11 -0
  126. package/dist/assets/twig-Csy3S7wG.js +1 -0
  127. package/dist/assets/typescript-CKWDmBCc.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-DuEUAzPi.js +1 -0
  132. package/dist/assets/{xychartDiagram-f5964ef8-CZg47H1t.js → xychartDiagram-f5964ef8-D09Zkv2K.js} +1 -1
  133. package/dist/assets/yaml-DL7QPRYk.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 +353 -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 +157 -10
  160. package/src/components/chat/messages/MessageItem.tsx +382 -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/sender-toolbar-bindings.ts +155 -0
  204. package/src/components/chat/sender/@hooks/use-model-select-browser.tsx +189 -0
  205. package/src/components/chat/sender/@hooks/use-sender-attachments.ts +143 -0
  206. package/src/components/chat/sender/@hooks/use-sender-autofocus.ts +34 -0
  207. package/src/components/chat/sender/@hooks/use-sender-completion.ts +62 -0
  208. package/src/components/chat/sender/@hooks/use-sender-composer-state.ts +34 -0
  209. package/src/components/chat/sender/@hooks/use-sender-controller.ts +193 -0
  210. package/src/components/chat/sender/@hooks/use-sender-focus-restore.ts +72 -0
  211. package/src/components/chat/sender/@hooks/use-sender-history.ts +79 -0
  212. package/src/components/chat/sender/@hooks/use-sender-keydown.ts +113 -0
  213. package/src/components/chat/sender/@hooks/use-sender-reference-actions.ts +191 -0
  214. package/src/components/chat/sender/@hooks/use-sender-reference-focus-restore.ts +21 -0
  215. package/src/components/chat/sender/@hooks/use-sender-refs.ts +19 -0
  216. package/src/components/chat/sender/@hooks/use-sender-select-overlays.ts +83 -0
  217. package/src/components/chat/sender/@hooks/use-sender-shortcuts.ts +78 -0
  218. package/src/components/chat/sender/@hooks/use-sender-submit.ts +81 -0
  219. package/src/components/chat/sender/@types/sender-composer.ts +19 -0
  220. package/src/components/chat/sender/@types/sender-editor.ts +12 -0
  221. package/src/components/chat/sender/@types/sender-props.ts +50 -0
  222. package/src/components/chat/sender/@types/sender-toolbar-types.ts +83 -0
  223. package/src/components/chat/sender/@types/sender-types.ts +21 -0
  224. package/src/components/chat/sender/@utils/sender-completion.ts +164 -0
  225. package/src/components/chat/sender/@utils/sender-constants.ts +18 -0
  226. package/src/components/chat/sender/@utils/sender-utils.ts +45 -0
  227. package/src/components/chat/sender/Sender.scss +6 -737
  228. package/src/components/chat/sender/Sender.tsx +53 -921
  229. package/src/components/chat/session-metadata.ts +55 -0
  230. package/src/components/chat/terminal/@hooks/use-terminal-instance.ts +152 -0
  231. package/src/components/chat/terminal/@hooks/use-terminal-session.ts +196 -0
  232. package/src/components/chat/terminal/ChatTerminalView.scss +62 -0
  233. package/src/components/chat/terminal/ChatTerminalView.tsx +114 -0
  234. package/src/components/chat/tools/core/ToolGroup.scss +7 -0
  235. package/src/components/chat/tools/core/ToolGroup.tsx +94 -44
  236. package/src/components/config/ConfigSectionForm.tsx +8 -1
  237. package/src/components/config/ConfigShortcutInput.tsx +9 -2
  238. package/src/components/config/configSchema.ts +12 -2
  239. package/src/components/config/record-editors/ModelServicesRecordEditor.tsx +0 -14
  240. package/src/components/dock-panel/DockPanel.scss +152 -0
  241. package/src/components/dock-panel/DockPanel.tsx +195 -0
  242. package/src/components/layout/AppShell.scss +40 -2
  243. package/src/components/layout/AppShell.tsx +25 -10
  244. package/src/components/sidebar/SessionContextMenu.scss +143 -0
  245. package/src/components/sidebar/SessionContextMenu.tsx +196 -0
  246. package/src/components/sidebar/SessionContextMenuContent.tsx +89 -0
  247. package/src/components/sidebar/SessionItem.scss +150 -67
  248. package/src/components/sidebar/SessionItem.tsx +183 -134
  249. package/src/components/sidebar/SessionList.scss +47 -17
  250. package/src/components/sidebar/SessionList.tsx +31 -16
  251. package/src/components/sidebar/SidebarHeader.scss +329 -49
  252. package/src/components/sidebar/SidebarHeader.tsx +108 -86
  253. package/src/components/sidebar/SidebarHeaderBatchActions.tsx +81 -0
  254. package/src/components/sidebar/SidebarHeaderSearchActions.tsx +176 -0
  255. package/src/components/sidebar/SidebarHeaderSelectField.tsx +24 -0
  256. package/src/components/sidebar/filter-utils.ts +23 -0
  257. package/src/components/workspace/ContextFilePicker.scss +64 -0
  258. package/src/components/workspace/ContextFilePicker.tsx +171 -0
  259. package/src/connectionManager.ts +4 -2
  260. package/src/hooks/chat/interaction-state.ts +67 -0
  261. package/src/hooks/chat/model-selector-data-builders.ts +146 -0
  262. package/src/hooks/chat/model-selector-data-option-utils.ts +62 -0
  263. package/src/hooks/chat/model-selector-data-types.ts +27 -0
  264. package/src/hooks/chat/model-selector-data.ts +109 -0
  265. package/src/hooks/chat/model-selector-recommendations.ts +69 -0
  266. package/src/hooks/chat/model-selector.ts +9 -0
  267. package/src/hooks/chat/use-chat-interaction.ts +13 -8
  268. package/src/hooks/chat/use-chat-model-adapter-selection.tsx +167 -164
  269. package/src/hooks/chat/use-chat-models.tsx +46 -23
  270. package/src/hooks/chat/use-chat-session-actions.ts +69 -23
  271. package/src/hooks/chat/use-chat-session-messages.ts +158 -60
  272. package/src/hooks/chat/use-chat-session.ts +34 -9
  273. package/src/hooks/chat/use-chat-view.ts +26 -6
  274. package/src/hooks/chat/use-composer-control-shortcuts.ts +69 -0
  275. package/src/hooks/chat/use-terminal-dock-visibility.ts +39 -0
  276. package/src/hooks/use-roving-focus-list.ts +104 -0
  277. package/src/hooks/use-sidebar-navigation.ts +9 -4
  278. package/src/hooks/use-sidebar-query-state.ts +79 -0
  279. package/src/main.tsx +6 -1
  280. package/src/resources/locales/en.json +140 -6
  281. package/src/resources/locales/zh.json +140 -6
  282. package/src/routes/ChatRoute.scss +159 -4
  283. package/src/routes/ChatRoute.tsx +72 -10
  284. package/src/runtime-config.ts +21 -0
  285. package/src/store/index.ts +1 -3
  286. package/src/styles/global.scss +12 -2
  287. package/src/utils/chat-links.ts +21 -0
  288. package/src/utils/copy.ts +18 -0
  289. package/src/utils/shortcutUtils.ts +111 -1
  290. package/src/vite-env.d.ts +1 -0
  291. package/src/ws.ts +6 -5
  292. package/vite.config.ts +71 -7
  293. package/dist/assets/channel-Di8KkPU4.js +0 -1
  294. package/dist/assets/clone-B065n6L-.js +0 -1
  295. package/dist/assets/flowDiagram-v2-4f6560a1-nj-oVLPK.js +0 -1
  296. package/dist/assets/index-BZe1Qtye.css +0 -1
  297. package/dist/assets/index-DV3eI2aD.js +0 -557
  298. package/src/components/chat/sender/CompletionMenu.scss +0 -70
  299. package/src/components/chat/sender/CompletionMenu.tsx +0 -58
  300. /package/src/components/chat/{sender/ThinkingStatus.scss → ThinkingStatus.scss} +0 -0
  301. /package/src/components/chat/sender/{interaction-request.ts → @core/interaction-request.ts} +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,7 +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 { applyInteractionStateEvent } from './interaction-state'
9
+ import type { ChatErrorBannerState } from './interaction-state'
10
+ import {
11
+ applyInteractionStateEvent,
12
+ findLatestFatalError,
13
+ getFatalSessionError,
14
+ restoreInteractionStateFromHistory
15
+ } from './interaction-state'
10
16
  import type { ChatEffort } from './use-chat-effort'
11
17
  import type { PermissionMode } from './use-chat-permission-mode'
12
18
 
@@ -57,20 +63,118 @@ export function useChatSessionMessages({
57
63
  const [messages, setMessages] = useState<ChatMessage[]>([])
58
64
  const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null)
59
65
  const [isReady, setIsReady] = useState(false)
60
- const [connectionError, setConnectionError] = useState<string | null>(null)
66
+ const [errorBanner, setErrorBanner] = useState<ChatErrorBannerState | null>(null)
61
67
  const [retryCount, setRetryCount] = useState(0)
62
68
  const isInitialLoadRef = useRef<boolean>(true)
63
69
  const lastConnectedModelRef = useRef<string | undefined>(undefined)
64
70
  const lastConnectedEffortRef = useRef<string | undefined>(undefined)
65
71
  const lastConnectedPermissionModeRef = useRef<string | undefined>(undefined)
66
72
  const lastConnectedAdapterRef = useRef<string | undefined>(undefined)
73
+ const lastObservedSessionStatusRef = useRef<Session['status'] | undefined>(session?.status)
67
74
  const expectedCloseRef = useRef(false)
68
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])
69
173
 
70
174
  const retryConnection = useCallback(() => {
71
175
  if (session?.id == null || session.id === '') return
72
176
  expectedCloseRef.current = true
73
- setConnectionError(null)
177
+ setErrorBanner(null)
74
178
  connectionManager.close(session.id)
75
179
  setRetryCount((count) => count + 1)
76
180
  }, [session?.id])
@@ -79,7 +183,7 @@ export function useChatSessionMessages({
79
183
  setMessages([])
80
184
  setSessionInfo(null)
81
185
  setIsReady(false)
82
- setConnectionError(null)
186
+ setErrorBanner(null)
83
187
  setInteractionRequest(null)
84
188
  interactionRequestRef.current = null
85
189
  isInitialLoadRef.current = true
@@ -90,63 +194,32 @@ export function useChatSessionMessages({
90
194
  lastConnectedEffortRef.current = undefined
91
195
  lastConnectedPermissionModeRef.current = undefined
92
196
  lastConnectedAdapterRef.current = undefined
197
+ clearScheduledReconciles()
93
198
  return
94
199
  }
95
200
 
96
- let isDisposed = false
201
+ void refreshHistory()
97
202
 
98
- const fetchHistory = async () => {
99
- try {
100
- const res = await getSessionMessages(session.id)
101
- if (isDisposed) return
102
- const events = res.messages as WSEvent[]
103
-
104
- if (res.session) {
105
- const updatedSession = res.session
106
- void mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
107
- if (prev?.sessions == null) return prev
108
- const newSessions = prev.sessions.map((s: Session) =>
109
- s.id === updatedSession.id ? { ...s, ...updatedSession } : s
110
- )
111
- return { ...prev, sessions: newSessions }
112
- }, false)
113
- }
114
-
115
- interactionRequestRef.current = res.interaction ?? null
116
- setInteractionRequest(interactionRequestRef.current)
117
-
118
- let currentMessages: ChatMessage[] = []
119
- let currentSessionInfo: SessionInfo | null = null
120
-
121
- for (const data of events) {
122
- currentMessages = applyMessageEvent(currentMessages, data)
123
- currentMessages = applyToolResultEvent(currentMessages, data)
124
- if (data.type === 'session_info') {
125
- if (data.info != null && data.info.type !== 'summary') {
126
- currentSessionInfo = data.info
127
- }
128
- }
129
- }
130
-
131
- setMessages(currentMessages)
132
- setSessionInfo(currentSessionInfo)
203
+ return () => {
204
+ clearScheduledReconciles()
205
+ }
206
+ }, [clearScheduledReconciles, refreshHistory, session?.id, setInteractionRequest])
133
207
 
134
- setTimeout(() => {
135
- if (isDisposed) return
136
- setIsReady(true)
137
- isInitialLoadRef.current = false
138
- }, 100)
139
- } catch (err) {
140
- console.error('Failed to fetch history messages:', err)
141
- }
208
+ useEffect(() => {
209
+ if (session?.id == null || session.id === '') {
210
+ lastObservedSessionStatusRef.current = undefined
211
+ return
142
212
  }
143
213
 
144
- void fetchHistory()
214
+ const previousStatus = lastObservedSessionStatusRef.current
215
+ lastObservedSessionStatusRef.current = session.status
145
216
 
146
- return () => {
147
- isDisposed = true
217
+ if (previousStatus == null || previousStatus === session.status) {
218
+ return
148
219
  }
149
- }, [mutate, session?.id, setInteractionRequest])
220
+
221
+ void refreshHistory({ updateReadiness: false })
222
+ }, [refreshHistory, session?.id, session?.status])
150
223
 
151
224
  useEffect(() => {
152
225
  if (session?.id == null || session.id === '') {
@@ -176,7 +249,7 @@ export function useChatSessionMessages({
176
249
  session?.status !== 'running'
177
250
  if (modelChanged || effortChanged || permissionModeChanged || adapterChanged) {
178
251
  expectedCloseRef.current = true
179
- setConnectionError(null)
252
+ setErrorBanner(null)
180
253
  connectionManager.send(session.id, { type: 'terminate_session' })
181
254
  connectionManager.close(session.id)
182
255
  }
@@ -205,7 +278,7 @@ export function useChatSessionMessages({
205
278
  cleanup = connectionManager.connect(session.id, {
206
279
  onOpen() {
207
280
  expectedCloseRef.current = false
208
- setConnectionError(null)
281
+ setErrorBanner((current) => current?.kind === 'session' ? current : null)
209
282
  },
210
283
  onMessage(data: WSEvent) {
211
284
  if (isDisposed) return
@@ -213,9 +286,22 @@ export function useChatSessionMessages({
213
286
  if (nextInteraction !== interactionRequestRef.current) {
214
287
  interactionRequestRef.current = nextInteraction
215
288
  setInteractionRequest(nextInteraction)
289
+ if (nextInteraction != null) {
290
+ setErrorBanner(null)
291
+ }
292
+ }
293
+ if (data.type === 'interaction_response') {
294
+ reconcileAfterInteraction()
295
+ return
216
296
  }
217
297
  if (data.type === 'error') {
218
- 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
+ }
219
305
  return
220
306
  }
221
307
 
@@ -276,12 +362,15 @@ export function useChatSessionMessages({
276
362
  }
277
363
 
278
364
  if (data.type === 'interaction_request') {
279
- return
365
+ setInteractionRequest(data)
280
366
  }
281
367
  },
282
368
  onError() {
283
369
  if (isDisposed) return
284
- setConnectionError(t('chat.connectionError'))
370
+ setErrorBanner({
371
+ kind: 'connection',
372
+ message: t('chat.connectionError')
373
+ })
285
374
  },
286
375
  onClose() {
287
376
  if (isDisposed) return
@@ -289,7 +378,12 @@ export function useChatSessionMessages({
289
378
  expectedCloseRef.current = false
290
379
  return
291
380
  }
292
- setConnectionError((current) => current ?? t('chat.connectionClosed'))
381
+ setErrorBanner((current) =>
382
+ current ?? {
383
+ kind: 'connection',
384
+ message: t('chat.connectionClosed')
385
+ }
386
+ )
293
387
  }
294
388
  }, Object.keys(connectionParams).length > 0 ? connectionParams : undefined)
295
389
  }, (modelChanged || effortChanged || permissionModeChanged || adapterChanged) ? 200 : 100)
@@ -301,11 +395,14 @@ export function useChatSessionMessages({
301
395
  }
302
396
  }, [
303
397
  adapter,
398
+ clearScheduledReconciles,
304
399
  effort,
305
400
  modelForQuery,
306
401
  mutate,
307
402
  permissionMode,
403
+ reconcileAfterInteraction,
308
404
  retryCount,
405
+ refreshHistory,
309
406
  session?.id,
310
407
  session?.status,
311
408
  setInteractionRequest,
@@ -317,7 +414,8 @@ export function useChatSessionMessages({
317
414
  setMessages,
318
415
  sessionInfo,
319
416
  isReady,
320
- connectionError,
321
- retryConnection
417
+ errorBanner,
418
+ retryConnection,
419
+ reconcileAfterInteraction
322
420
  }
323
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
+ }