@vibe-forge/client 0.11.2 → 1.0.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 (232) hide show
  1. package/cli.cjs +6 -11
  2. package/dist/assets/{arc-De_WjPJ3.js → arc-CbOXL0l9.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-C4aR2zTE.js → blockDiagram-c4efeb88-CqxINvsS.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BZH3rq_m.js → c4Diagram-c83219d4-BKazU0hb.js} +1 -1
  5. package/dist/assets/channel-Dnopc5A6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BzJgBrIK.js → classDiagram-beda092f-fAFX5BpB.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-5ZtXcnT3.js → classDiagram-v2-2358418a-w1VkNGJj.js} +1 -1
  8. package/dist/assets/clone-sQthahUA.js +1 -0
  9. package/dist/assets/{createText-1719965b-DUVvEtmR.js → createText-1719965b-CEinakVP.js} +1 -1
  10. package/dist/assets/{cssMode-GoTNjuXX.js → cssMode-DPqRki4y.js} +1 -1
  11. package/dist/assets/{edges-96097737-Dd7m4Cvs.js → edges-96097737-Cb0F1_3K.js} +1 -1
  12. package/dist/assets/{erDiagram-0228fc6a-DxqFlG_f.js → erDiagram-0228fc6a-C-N2fx-J.js} +1 -1
  13. package/dist/assets/{flowDb-c6c81e3f-DU0C5kCI.js → flowDb-c6c81e3f-D1Xz_8Gf.js} +1 -1
  14. package/dist/assets/{flowDiagram-50d868cf-Di1uDa_X.js → flowDiagram-50d868cf-DyPSZyAj.js} +1 -1
  15. package/dist/assets/flowDiagram-v2-4f6560a1-OazrdWQO.js +1 -0
  16. package/dist/assets/{flowchart-elk-definition-6af322e1-CwG8aty5.js → flowchart-elk-definition-6af322e1-Dr1DDXwE.js} +1 -1
  17. package/dist/assets/{freemarker2-j39cqTlI.js → freemarker2-C3DvPFaK.js} +1 -1
  18. package/dist/assets/{ganttDiagram-a2739b55-baO_lzL-.js → ganttDiagram-a2739b55-DmvY1GRj.js} +1 -1
  19. package/dist/assets/{gitGraphDiagram-82fe8481-COoHjYMf.js → gitGraphDiagram-82fe8481-CoXfPYYi.js} +1 -1
  20. package/dist/assets/{graph-KxESr4M5.js → graph-BkDQy7Qt.js} +1 -1
  21. package/dist/assets/{handlebars-BgjdZO8G.js → handlebars-BcTFdqjl.js} +1 -1
  22. package/dist/assets/{html-Ba7tYObe.js → html-Dg-O6XFr.js} +1 -1
  23. package/dist/assets/{htmlMode-Bztvbig1.js → htmlMode-B_wqYWvn.js} +1 -1
  24. package/dist/assets/{index-5325376f-BMTAx2mL.js → index-5325376f-kxPTR3_e.js} +1 -1
  25. package/dist/assets/index-o93dlo92.css +32 -0
  26. package/dist/assets/{index-Pm_kLJvG.js → index-wkhI4dr6.js} +350 -329
  27. package/dist/assets/{infoDiagram-8eee0895-CC74qbHY.js → infoDiagram-8eee0895-BEvqkwPI.js} +1 -1
  28. package/dist/assets/{javascript-C1e1cllX.js → javascript-DhlOH8_z.js} +1 -1
  29. package/dist/assets/{journeyDiagram-c64418c1-C4MyOdE6.js → journeyDiagram-c64418c1-gKtLYmmp.js} +1 -1
  30. package/dist/assets/{jsonMode-BC98AlvF.js → jsonMode-DxTbF9OD.js} +1 -1
  31. package/dist/assets/{layout-CxAyTlr7.js → layout-CDaZEk6E.js} +1 -1
  32. package/dist/assets/{line-DhaUfI71.js → line-DNRQu8iq.js} +1 -1
  33. package/dist/assets/{linear-MYukzldK.js → linear-Cph9Z6_j.js} +1 -1
  34. package/dist/assets/{liquid-DahfJEYl.js → liquid-ByZ6JgRG.js} +1 -1
  35. package/dist/assets/{lspLanguageFeatures-BWDJcswW.js → lspLanguageFeatures-DzvhkgnM.js} +1 -1
  36. package/dist/assets/{mdx-BELlF_FD.js → mdx-D8RGHTl6.js} +1 -1
  37. package/dist/assets/{mermaid.core-BrQnSGSY.js → mermaid.core-BgcryF__.js} +4 -4
  38. package/dist/assets/{mindmap-definition-8da855dc-B0FoxTiy.js → mindmap-definition-8da855dc-WrxK0FcB.js} +1 -1
  39. package/dist/assets/{pieDiagram-a8764435-Ddr2cjSL.js → pieDiagram-a8764435-VsZBsiQy.js} +1 -1
  40. package/dist/assets/{python--C9if_AD.js → python-CXVtk_cg.js} +1 -1
  41. package/dist/assets/{quadrantDiagram-1e28029f-BlEs7Mrl.js → quadrantDiagram-1e28029f-BVlgwOvU.js} +1 -1
  42. package/dist/assets/{razor-B9U9JxKn.js → razor-0tind7h2.js} +1 -1
  43. package/dist/assets/{requirementDiagram-08caed73-kEFOAu2v.js → requirementDiagram-08caed73-CpPMPoYp.js} +1 -1
  44. package/dist/assets/{sankeyDiagram-a04cb91d-BBghez8I.js → sankeyDiagram-a04cb91d-Cm5nnRmc.js} +1 -1
  45. package/dist/assets/{sequenceDiagram-c5b8d532-CJqgzdUE.js → sequenceDiagram-c5b8d532-DpMlJvJB.js} +1 -1
  46. package/dist/assets/{stateDiagram-1ecb1508-BER4XEI6.js → stateDiagram-1ecb1508-DU1zc7vq.js} +1 -1
  47. package/dist/assets/{stateDiagram-v2-c2b004d7-EBV2vSks.js → stateDiagram-v2-c2b004d7-D-0RgmAp.js} +1 -1
  48. package/dist/assets/{styles-b4e223ce-k0eswZsE.js → styles-b4e223ce-BSO-yNWV.js} +1 -1
  49. package/dist/assets/{styles-ca3715f6-Ckr7GA-0.js → styles-ca3715f6-CHnsn2Ro.js} +1 -1
  50. package/dist/assets/{styles-d45a18b0-C1bpSwV3.js → styles-d45a18b0-B-rVGjEq.js} +1 -1
  51. package/dist/assets/{svgDrawCommon-b86b1483-CDtKpGvy.js → svgDrawCommon-b86b1483-CA3Pl89f.js} +1 -1
  52. package/dist/assets/{timeline-definition-faaaa080-BeGR-vua.js → timeline-definition-faaaa080-BcihLR6s.js} +1 -1
  53. package/dist/assets/{tsMode-D_gJXIy3.js → tsMode-D9GGa5Ur.js} +1 -1
  54. package/dist/assets/{typescript-BoKcNXkN.js → typescript-BT9CK_EL.js} +1 -1
  55. package/dist/assets/{xml-DZvURlJ-.js → xml-DNO75J-T.js} +1 -1
  56. package/dist/assets/{xychartDiagram-f5964ef8-DxfeLuYV.js → xychartDiagram-f5964ef8-DJTwe32X.js} +1 -1
  57. package/dist/assets/{yaml-CTC8PAGY.js → yaml-7CVzhiP2.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/package.json +13 -10
  60. package/src/api/git.ts +12 -0
  61. package/src/api/sessions.ts +131 -4
  62. package/src/api/types.ts +2 -1
  63. package/src/api.ts +13 -0
  64. package/src/components/ArchiveView.scss +143 -54
  65. package/src/components/ArchiveView.tsx +181 -167
  66. package/src/components/CodeBlock.scss +5 -0
  67. package/src/components/ConfigView.scss +142 -31
  68. package/src/components/ConfigView.tsx +161 -86
  69. package/src/components/MarkdownContent.tsx +7 -0
  70. package/src/components/NavRail.scss +248 -0
  71. package/src/components/NavRail.tsx +80 -107
  72. package/src/components/NavRailCompact.tsx +107 -0
  73. package/src/components/NavRailCompactMoreSheet.tsx +141 -0
  74. package/src/components/ShortcutTooltip.tsx +4 -2
  75. package/src/components/Sidebar.scss +51 -0
  76. package/src/components/Sidebar.tsx +43 -16
  77. package/src/components/automation-view/RuleFormPanel.scss +40 -13
  78. package/src/components/automation-view/RuleSidebar.scss +73 -47
  79. package/src/components/automation-view/RuleSidebar.tsx +9 -13
  80. package/src/components/automation-view/RunHistoryPanel.scss +141 -13
  81. package/src/components/automation-view/RunHistoryPanel.tsx +203 -161
  82. package/src/components/automation-view/TaskList.scss +44 -13
  83. package/src/components/automation-view/TriggerList.scss +46 -14
  84. package/src/components/automation-view/index.scss +82 -10
  85. package/src/components/automation-view/index.tsx +108 -55
  86. package/src/components/benchmark-view/BenchmarkCasePanel.scss +36 -16
  87. package/src/components/benchmark-view/BenchmarkSidebar.scss +44 -22
  88. package/src/components/benchmark-view/BenchmarkSidebar.tsx +0 -6
  89. package/src/components/benchmark-view/BenchmarkView.scss +63 -20
  90. package/src/components/benchmark-view/index.tsx +71 -34
  91. package/src/components/chat/AGENTS.md +14 -2
  92. package/src/components/chat/ChatComposerCard.scss +77 -0
  93. package/src/components/chat/ChatComposerCard.tsx +59 -0
  94. package/src/components/chat/ChatHeader.scss +187 -0
  95. package/src/components/chat/ChatHeader.tsx +209 -57
  96. package/src/components/chat/ChatHistoryView.tsx +279 -52
  97. package/src/components/chat/ChatTimelineView.scss +94 -1
  98. package/src/components/chat/ChatTimelineView.tsx +42 -0
  99. package/src/components/chat/CurrentTodoList.scss +210 -200
  100. package/src/components/chat/CurrentTodoList.tsx +116 -48
  101. package/src/components/chat/NewSessionGuide.scss +139 -1
  102. package/src/components/chat/NewSessionGuide.tsx +57 -100
  103. package/src/components/chat/NewSessionGuideCompactPanel.tsx +130 -0
  104. package/src/components/chat/NewSessionGuideGrid.tsx +141 -0
  105. package/src/components/chat/QueuedMessagesCard.scss +195 -0
  106. package/src/components/chat/QueuedMessagesCard.tsx +170 -0
  107. package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +61 -56
  108. package/src/components/chat/git-controls/BranchSwitcherResults.tsx +167 -0
  109. package/src/components/chat/git-controls/BranchTreeEntries.tsx +99 -0
  110. package/src/components/chat/git-controls/ChatGitControls.scss +437 -5
  111. package/src/components/chat/git-controls/ChatGitControls.tsx +136 -109
  112. package/src/components/chat/git-controls/DraftGitControls.tsx +91 -0
  113. package/src/components/chat/git-controls/GitOperationsDropdown.tsx +10 -2
  114. package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +301 -28
  115. package/src/components/chat/git-controls/git-branch-tree.ts +148 -0
  116. package/src/components/chat/git-controls/use-chat-draft-git-controls.ts +168 -0
  117. package/src/components/chat/git-controls/use-chat-git-controls.ts +76 -3
  118. package/src/components/chat/messages/MessageContextMenu.tsx +3 -1
  119. package/src/components/chat/messages/MessageItem.scss +78 -4
  120. package/src/components/chat/messages/MessageItem.tsx +47 -3
  121. package/src/components/chat/sender/@components/adapter-select/AdapterSelectControl.scss +23 -0
  122. package/src/components/chat/sender/@components/reference-actions/ReferenceActionsControl.scss +17 -0
  123. package/src/components/chat/sender/@components/reference-actions/ReferencePermissionActionsPopover.tsx +4 -1
  124. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.scss +167 -30
  125. package/src/components/chat/sender/@components/sender-attachments/SenderAttachments.tsx +95 -23
  126. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +10 -0
  127. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.scss +161 -45
  128. package/src/components/chat/sender/@components/sender-interaction-panel/SenderInteractionPanel.tsx +310 -71
  129. package/src/components/chat/sender/@components/sender-monaco-editor/SenderMonacoEditor.tsx +18 -0
  130. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-monaco-editor.ts +86 -9
  131. package/src/components/chat/sender/@components/sender-monaco-editor/use-sender-monaco-theme.ts +52 -3
  132. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.scss +110 -1
  133. package/src/components/chat/sender/@components/sender-submit-action/SenderSubmitAction.tsx +137 -17
  134. package/src/components/chat/sender/@components/sender-toolbar/SenderSelectBase.scss +21 -0
  135. package/src/components/chat/sender/@components/sender-toolbar/SenderSelectShared.scss +21 -0
  136. package/src/components/chat/sender/@components/sender-toolbar/SenderToolbar.scss +63 -0
  137. package/src/components/chat/sender/@components/sender-toolbar/SenderToolbar.tsx +12 -6
  138. package/src/components/chat/sender/@core/build-sender-controller-result.ts +6 -0
  139. package/src/components/chat/sender/@core/build-sender-toolbar.ts +25 -2
  140. package/src/components/chat/sender/@core/create-sender-toolbar-handlers.ts +9 -2
  141. package/src/components/chat/sender/@core/get-sender-runtime-state.ts +1 -1
  142. package/src/components/chat/sender/@core/interaction-request.ts +2 -2
  143. package/src/components/chat/sender/@core/sender-toolbar-bindings.ts +28 -4
  144. package/src/components/chat/sender/@hooks/use-model-select-browser.tsx +4 -2
  145. package/src/components/chat/sender/@hooks/use-sender-controller.ts +56 -11
  146. package/src/components/chat/sender/@hooks/use-sender-keydown.ts +64 -0
  147. package/src/components/chat/sender/@hooks/use-sender-shortcuts.ts +16 -1
  148. package/src/components/chat/sender/@hooks/use-sender-submit.ts +16 -8
  149. package/src/components/chat/sender/@types/sender-props.ts +20 -3
  150. package/src/components/chat/sender/@types/sender-toolbar-types.ts +12 -1
  151. package/src/components/chat/sender/Sender.scss +4 -1
  152. package/src/components/chat/sender/Sender.tsx +3 -12
  153. package/src/components/chat/session-timeline-panel/EventList.scss +88 -0
  154. package/src/components/chat/session-timeline-panel/EventList.tsx +99 -47
  155. package/src/components/chat/session-timeline-panel/gantt.ts +23 -7
  156. package/src/components/chat/session-timeline-panel/git-graph.ts +6 -1
  157. package/src/components/chat/session-timeline-panel/index.scss +14 -1
  158. package/src/components/chat/session-timeline-panel/index.tsx +86 -10
  159. package/src/components/chat/session-timeline-panel/types.ts +4 -0
  160. package/src/components/chat/status-bar/ChatStatusBar.scss +27 -0
  161. package/src/components/chat/status-bar/ChatStatusBar.tsx +39 -0
  162. package/src/components/chat/terminal/ChatTerminalView.tsx +6 -0
  163. package/src/components/chat/tools/core/ToolCallBox.scss +19 -0
  164. package/src/components/chat/tools/core/ToolGroup.scss +32 -0
  165. package/src/components/chat/tools/task/components/TaskToolCard.scss +59 -1
  166. package/src/components/config/ConfigEditors.scss +20 -6
  167. package/src/components/config/ConfigFieldRow.scss +57 -17
  168. package/src/components/config/ConfigSectionForm.scss +10 -4
  169. package/src/components/config/ConfigSectionPanel.tsx +18 -11
  170. package/src/components/config/configSchema.ts +1 -0
  171. package/src/components/config/record-editors/RecordEditors.scss +42 -9
  172. package/src/components/dock-panel/DockPanel.scss +6 -2
  173. package/src/components/dock-panel/DockPanel.tsx +12 -16
  174. package/src/components/knowledge-base/KnowledgeBaseView.scss +180 -6
  175. package/src/components/knowledge-base/KnowledgeBaseView.tsx +98 -26
  176. package/src/components/knowledge-base/components/ActionButton.scss +4 -0
  177. package/src/components/knowledge-base/components/EmptyState.scss +5 -8
  178. package/src/components/knowledge-base/components/EntitiesTab.tsx +8 -2
  179. package/src/components/knowledge-base/components/EntityItem.scss +10 -3
  180. package/src/components/knowledge-base/components/FilterBar.scss +13 -2
  181. package/src/components/knowledge-base/components/FlowsTab.tsx +8 -2
  182. package/src/components/knowledge-base/components/KnowledgeBaseHeader.scss +2 -23
  183. package/src/components/knowledge-base/components/KnowledgeBaseHeader.tsx +0 -5
  184. package/src/components/knowledge-base/components/KnowledgeList.scss +15 -6
  185. package/src/components/knowledge-base/components/LoadingState.scss +4 -0
  186. package/src/components/knowledge-base/components/RuleItem.scss +86 -0
  187. package/src/components/knowledge-base/components/RuleItem.tsx +2 -0
  188. package/src/components/knowledge-base/components/RulesTab.tsx +8 -2
  189. package/src/components/knowledge-base/components/SectionHeader.scss +3 -18
  190. package/src/components/knowledge-base/components/SectionHeader.tsx +3 -7
  191. package/src/components/knowledge-base/components/SkillsTab.tsx +8 -3
  192. package/src/components/knowledge-base/components/SpecItem.scss +16 -7
  193. package/src/components/layout/@hooks/use-mobile-sidebar-modal.ts +190 -0
  194. package/src/components/layout/AppShell.scss +106 -6
  195. package/src/components/layout/AppShell.tsx +118 -10
  196. package/src/components/layout/PageShell.scss +41 -0
  197. package/src/components/layout/PageShell.tsx +32 -0
  198. package/src/components/layout/mobile-sidebar-constants.ts +1 -0
  199. package/src/components/nav-rail-compact-config.ts +114 -0
  200. package/src/components/nav-rail-items.tsx +181 -0
  201. package/src/components/sidebar/SessionContextMenu.tsx +3 -1
  202. package/src/components/sidebar/SessionItem.scss +62 -0
  203. package/src/components/sidebar/SessionItem.tsx +97 -52
  204. package/src/components/sidebar/SessionList.tsx +6 -0
  205. package/src/components/sidebar/SidebarHeader.scss +49 -0
  206. package/src/components/sidebar/SidebarHeader.tsx +27 -5
  207. package/src/components/sidebar/SidebarHeaderBatchActions.tsx +8 -4
  208. package/src/components/sidebar/SidebarHeaderSearchActions.tsx +6 -3
  209. package/src/components/sidebar/SidebarUtilityFooter.tsx +69 -0
  210. package/src/components/workspace/ContextFilePicker.tsx +12 -4
  211. package/src/hooks/chat/chat-session-workspace-draft.ts +25 -0
  212. package/src/hooks/chat/session-view-cache.ts +4 -1
  213. package/src/hooks/chat/use-chat-adapter.ts +5 -1
  214. package/src/hooks/chat/use-chat-model-adapter-selection.tsx +5 -1
  215. package/src/hooks/chat/use-chat-scroll.ts +24 -7
  216. package/src/hooks/chat/use-chat-session-actions.ts +118 -6
  217. package/src/hooks/chat/use-chat-session-messages.ts +20 -1
  218. package/src/hooks/chat/use-chat-session.ts +2 -0
  219. package/src/hooks/use-responsive-layout.ts +115 -0
  220. package/src/main.tsx +8 -0
  221. package/src/resources/adapters.ts +15 -0
  222. package/src/resources/locales/en.json +84 -1
  223. package/src/resources/locales/zh.json +84 -1
  224. package/src/routes/ChatRoute.scss +152 -9
  225. package/src/routes/ChatRoute.tsx +31 -34
  226. package/src/store/index.ts +2 -0
  227. package/dist/assets/channel-BvERb8WU.js +0 -1
  228. package/dist/assets/clone-B9_0v-6Y.js +0 -1
  229. package/dist/assets/flowDiagram-v2-4f6560a1-LpS8Kb00.js +0 -1
  230. package/dist/assets/index-C1oh0w9H.css +0 -32
  231. package/src/components/chat/ThinkingStatus.scss +0 -70
  232. package/src/components/chat/ThinkingStatus.tsx +0 -13
@@ -1,65 +1,338 @@
1
- import { Button, Dropdown } from 'antd'
1
+ /* eslint-disable max-lines */
2
+
3
+ import { Button, Dropdown, Empty, Input, Switch } from 'antd'
4
+ import { useEffect, useMemo, useState } from 'react'
2
5
  import { useTranslation } from 'react-i18next'
3
6
 
4
- import type { GitWorktreeSummary } from '@vibe-forge/types'
7
+ import type { GitWorktreeSummary, SessionWorkspace } from '@vibe-forge/types'
5
8
 
6
9
  import { formatGitWorktreePathLabel } from './git-branch-utils'
7
10
 
11
+ interface DraftWorktreeMenuMode {
12
+ type: 'draft'
13
+ createWorktree: boolean
14
+ disabled?: boolean
15
+ onCreateWorktreeChange: (checked: boolean) => void
16
+ }
17
+
18
+ interface SessionWorktreeMenuMode {
19
+ type: 'session'
20
+ isBusy: boolean
21
+ canCreateManagedWorktree: boolean
22
+ canTransferToLocal: boolean
23
+ onCreateManagedWorktree: () => void
24
+ onTransferToLocal: () => void
25
+ }
26
+
27
+ const getWorkspaceKindIcon = (kind: SessionWorkspace['kind']) => {
28
+ switch (kind) {
29
+ case 'managed_worktree':
30
+ return 'account_tree'
31
+ case 'external_workspace':
32
+ return 'folder_open'
33
+ default:
34
+ return 'folder'
35
+ }
36
+ }
37
+
38
+ const getWorkspaceKindLabel = (kind: SessionWorkspace['kind'], t: (key: string) => string) => {
39
+ switch (kind) {
40
+ case 'managed_worktree':
41
+ return t('chat.sessionWorkspaceManaged')
42
+ case 'external_workspace':
43
+ return t('chat.sessionWorkspaceExternal')
44
+ default:
45
+ return t('chat.sessionWorkspaceShared')
46
+ }
47
+ }
48
+
49
+ const getDraftStrategyIcon = (createWorktree: boolean) => (
50
+ createWorktree ? 'create_new_folder' : 'folder_open'
51
+ )
52
+
53
+ const getDraftStrategyLabel = (createWorktree: boolean, t: (key: string) => string) => (
54
+ createWorktree
55
+ ? t('chat.sessionWorkspaceDraftStrategyManaged')
56
+ : t('chat.sessionWorkspaceDraftStrategyLocal')
57
+ )
58
+
59
+ const getWorktreeMenuTitle = (
60
+ mode: DraftWorktreeMenuMode | SessionWorktreeMenuMode,
61
+ t: (key: string) => string
62
+ ) => (
63
+ mode.type === 'draft'
64
+ ? t('chat.sessionWorkspaceMenuWorktreeList')
65
+ : t('chat.sessionWorkspaceMenuCurrentWorktree')
66
+ )
67
+
68
+ const getWorkspaceStateLabel = (
69
+ state: SessionWorkspace['state'],
70
+ t: (key: string) => string
71
+ ) => {
72
+ switch (state) {
73
+ case 'provisioning':
74
+ return t('chat.sessionWorkspaceStateProvisioning')
75
+ case 'deleting':
76
+ return t('chat.sessionWorkspaceStateDeleting')
77
+ case 'deleted':
78
+ return t('chat.sessionWorkspaceStateDeleted')
79
+ case 'broken':
80
+ return t('chat.sessionWorkspaceStateBroken')
81
+ default:
82
+ return t('chat.sessionWorkspaceStateReady')
83
+ }
84
+ }
85
+
86
+ const joinWorkspaceSummary = (parts: Array<string | null | undefined>) =>
87
+ parts.filter(part => part != null && part.trim() !== '').join(' · ')
88
+
89
+ const filterGitWorktrees = (worktrees: GitWorktreeSummary[], query: string) => {
90
+ const keyword = query.trim().toLowerCase()
91
+ if (keyword === '') {
92
+ return worktrees
93
+ }
94
+
95
+ return worktrees.filter(worktree => {
96
+ const pathLabel = formatGitWorktreePathLabel(worktree.path).toLowerCase()
97
+ const branchLabel = worktree.branchName?.toLowerCase() ?? ''
98
+ const fullPath = worktree.path.toLowerCase()
99
+ return pathLabel.includes(keyword) || branchLabel.includes(keyword) || fullPath.includes(keyword)
100
+ })
101
+ }
102
+
8
103
  export function GitWorktreeDropdown({
104
+ compact = false,
9
105
  open,
106
+ workspace,
10
107
  worktrees,
108
+ currentBranch,
109
+ mode,
110
+ placement = 'bottomLeft',
11
111
  onOpenChange
12
112
  }: {
113
+ compact?: boolean
13
114
  open: boolean
115
+ workspace?: SessionWorkspace
14
116
  worktrees: GitWorktreeSummary[]
117
+ currentBranch?: string | null
118
+ mode: DraftWorktreeMenuMode | SessionWorktreeMenuMode
119
+ placement?: 'bottomLeft' | 'topLeft'
15
120
  onOpenChange: (open: boolean) => void
16
121
  }) {
17
122
  const { t } = useTranslation()
123
+ const [isWorktreeSubmenuOpen, setIsWorktreeSubmenuOpen] = useState(false)
124
+ const [worktreeQuery, setWorktreeQuery] = useState('')
125
+
126
+ useEffect(() => {
127
+ if (!open) {
128
+ setIsWorktreeSubmenuOpen(false)
129
+ setWorktreeQuery('')
130
+ }
131
+ }, [open])
132
+
133
+ const currentWorkspaceTitle = useMemo(() => {
134
+ if (workspace?.workspaceFolder?.trim()) {
135
+ return formatGitWorktreePathLabel(workspace.workspaceFolder)
136
+ }
137
+ const currentWorktree = worktrees.find(item => item.isCurrent)
138
+ return currentWorktree != null
139
+ ? formatGitWorktreePathLabel(currentWorktree.path)
140
+ : t('chat.gitWorktree')
141
+ }, [t, workspace?.workspaceFolder, worktrees])
142
+
143
+ const currentWorkspaceSubtitle = useMemo(() => {
144
+ if (workspace == null) {
145
+ return currentBranch?.trim() || t('chat.gitDetachedHead')
146
+ }
147
+
148
+ return joinWorkspaceSummary([
149
+ getWorkspaceKindLabel(workspace.kind, t),
150
+ currentBranch?.trim() || t('chat.gitDetachedHead'),
151
+ workspace.state !== 'ready' ? getWorkspaceStateLabel(workspace.state, t) : null
152
+ ])
153
+ }, [currentBranch, t, workspace])
154
+
155
+ const filteredWorktrees = useMemo(
156
+ () => filterGitWorktrees(worktrees, worktreeQuery),
157
+ [worktreeQuery, worktrees]
158
+ )
159
+ const worktreeMenuTitle = getWorktreeMenuTitle(mode, t)
160
+
161
+ const triggerLabel = mode.type === 'draft'
162
+ ? getDraftStrategyLabel(mode.createWorktree, t)
163
+ : currentWorkspaceTitle
164
+ const triggerIcon = mode.type === 'draft'
165
+ ? getDraftStrategyIcon(mode.createWorktree)
166
+ : workspace != null
167
+ ? getWorkspaceKindIcon(workspace.kind)
168
+ : 'account_tree'
169
+ const triggerTitle = mode.type === 'draft'
170
+ ? triggerLabel
171
+ : workspace != null
172
+ ? workspace.workspaceFolder
173
+ : currentWorkspaceSubtitle
18
174
 
19
175
  return (
20
176
  <Dropdown
21
177
  open={open}
22
- placement='bottomLeft'
178
+ placement={placement}
23
179
  trigger={['click']}
24
180
  onOpenChange={onOpenChange}
25
181
  dropdownRender={() => (
26
- <div className='chat-header-git__overlay chat-header-git__overlay--worktrees'>
27
- <div className='chat-header-git__worktree-list'>
28
- {worktrees.map(worktree => (
182
+ <div className='chat-header-git__menu-shell'>
183
+ <div className='chat-header-git__overlay chat-header-git__overlay--worktree-root'>
184
+ <button
185
+ type='button'
186
+ className={`chat-header-git__menu-row ${isWorktreeSubmenuOpen ? 'is-active' : ''}`}
187
+ onClick={() => setIsWorktreeSubmenuOpen(value => !value)}
188
+ >
189
+ <span className='chat-header-git__menu-row-main'>
190
+ <span className='chat-header-git__row-icon material-symbols-rounded'>
191
+ {workspace != null ? getWorkspaceKindIcon(workspace.kind) : 'account_tree'}
192
+ </span>
193
+ <span className='chat-header-git__menu-row-title'>
194
+ {worktreeMenuTitle}
195
+ </span>
196
+ </span>
197
+ <span className='chat-header-git__menu-row-trailing'>
198
+ {mode.type === 'session' && (
199
+ <span className='chat-header-git__menu-row-value' title={workspace?.workspaceFolder}>
200
+ {currentWorkspaceTitle}
201
+ </span>
202
+ )}
203
+ <span className='material-symbols-rounded'>chevron_right</span>
204
+ </span>
205
+ </button>
206
+
207
+ {mode.type === 'draft' && (
29
208
  <div
30
- key={worktree.path}
31
- className='chat-header-git__worktree-row'
32
- title={worktree.path}
209
+ className={`chat-header-git__menu-row chat-header-git__menu-row--toggle ${
210
+ mode.disabled ? 'is-disabled' : ''
211
+ }`}
33
212
  >
34
- <div className='chat-header-git__branch-row-main'>
35
- <span className='chat-header-git__row-icon material-symbols-rounded'>folder_open</span>
36
- <span className='chat-header-git__row-copy'>
37
- <span className='chat-header-git__row-title'>
38
- {formatGitWorktreePathLabel(worktree.path)}
39
- </span>
40
- <span className='chat-header-git__row-subtitle'>
41
- {worktree.branchName?.trim() || t('chat.gitDetachedHead')}
42
- </span>
213
+ <span className='chat-header-git__menu-row-main'>
214
+ <span className='chat-header-git__row-icon material-symbols-rounded'>
215
+ {getDraftStrategyIcon(true)}
43
216
  </span>
44
- </div>
45
- {worktree.isCurrent && (
46
- <span className='chat-header-git__row-state material-symbols-rounded'>check</span>
47
- )}
217
+ <span className='chat-header-git__menu-row-title'>
218
+ {t('chat.sessionWorkspaceMenuLaunchInWorktree')}
219
+ </span>
220
+ </span>
221
+ <Switch
222
+ checked={mode.createWorktree}
223
+ disabled={mode.disabled}
224
+ size='small'
225
+ onChange={mode.onCreateWorktreeChange}
226
+ onClick={(checked, event) => {
227
+ event?.stopPropagation()
228
+ }}
229
+ />
48
230
  </div>
49
- ))}
231
+ )}
232
+
233
+ {mode.type === 'session' && mode.canTransferToLocal && (
234
+ <button
235
+ type='button'
236
+ className='chat-header-git__menu-row'
237
+ disabled={mode.isBusy}
238
+ onClick={mode.onTransferToLocal}
239
+ >
240
+ <span className='chat-header-git__menu-row-main'>
241
+ <span className='chat-header-git__row-icon material-symbols-rounded'>drive_export</span>
242
+ <span className='chat-header-git__menu-row-title'>
243
+ {t('chat.sessionWorkspaceMenuTransferToLocal')}
244
+ </span>
245
+ </span>
246
+ </button>
247
+ )}
248
+
249
+ {mode.type === 'session' && mode.canCreateManagedWorktree && (
250
+ <button
251
+ type='button'
252
+ className='chat-header-git__menu-row'
253
+ disabled={mode.isBusy}
254
+ onClick={mode.onCreateManagedWorktree}
255
+ >
256
+ <span className='chat-header-git__menu-row-main'>
257
+ <span className='chat-header-git__row-icon material-symbols-rounded'>add</span>
258
+ <span className='chat-header-git__menu-row-title'>
259
+ {t('chat.sessionWorkspaceMenuCreateWorktree')}
260
+ </span>
261
+ </span>
262
+ </button>
263
+ )}
50
264
  </div>
265
+
266
+ {isWorktreeSubmenuOpen && (
267
+ <div className='chat-header-git__overlay chat-header-git__overlay--worktree-submenu'>
268
+ <div className='chat-header-git__worktree-submenu-body'>
269
+ {filteredWorktrees.length > 0
270
+ ? (
271
+ <div className='chat-header-git__worktree-list'>
272
+ {filteredWorktrees.map(worktree => (
273
+ <div
274
+ key={worktree.path}
275
+ className='chat-header-git__worktree-row'
276
+ title={worktree.path}
277
+ >
278
+ <div className='chat-header-git__worktree-row-main'>
279
+ <span className='chat-header-git__row-icon material-symbols-rounded'>folder_open</span>
280
+ <span className='chat-header-git__row-copy'>
281
+ <span className='chat-header-git__row-title'>
282
+ {formatGitWorktreePathLabel(worktree.path)}
283
+ </span>
284
+ <span className='chat-header-git__row-subtitle'>
285
+ {worktree.branchName?.trim() || t('chat.gitDetachedHead')}
286
+ </span>
287
+ </span>
288
+ </div>
289
+ {mode.type === 'session' && worktree.isCurrent && (
290
+ <span className='chat-header-git__worktree-chip'>
291
+ {t('chat.sessionWorkspaceCurrentSession')}
292
+ </span>
293
+ )}
294
+ </div>
295
+ ))}
296
+ </div>
297
+ )
298
+ : (
299
+ <div className='chat-header-git__empty chat-header-git__empty--worktrees'>
300
+ <Empty
301
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
302
+ description={t('chat.sessionWorkspaceNoWorktrees')}
303
+ />
304
+ </div>
305
+ )}
306
+ </div>
307
+
308
+ <Input
309
+ allowClear
310
+ autoFocus
311
+ className='chat-header-git__search'
312
+ placeholder={t('chat.sessionWorkspaceSearchWorktrees')}
313
+ prefix={<span className='material-symbols-rounded'>search</span>}
314
+ value={worktreeQuery}
315
+ onChange={(event) => setWorktreeQuery(event.target.value)}
316
+ onMouseDown={(event) => {
317
+ event.stopPropagation()
318
+ }}
319
+ />
320
+ </div>
321
+ )}
51
322
  </div>
52
323
  )}
53
324
  >
54
325
  <Button
55
326
  type='text'
56
- className={`chat-header-git__trigger ${open ? 'is-open' : ''}`}
57
- title={t('chat.gitWorktree')}
58
- aria-label={t('chat.gitWorktree')}
327
+ className={`chat-header-git__trigger chat-header-git__trigger--worktree ${open ? 'is-open' : ''} ${
328
+ mode.type === 'session' && mode.isBusy ? 'is-disabled' : ''
329
+ } ${compact ? 'is-compact' : ''}`.trim()}
330
+ title={triggerTitle}
331
+ aria-label={t('chat.sessionWorkspace')}
59
332
  >
60
333
  <span className='chat-header-git__trigger-main'>
61
- <span className='material-symbols-rounded'>account_tree</span>
62
- <span className='chat-header-git__trigger-label'>{t('chat.gitWorktree')}</span>
334
+ <span className='material-symbols-rounded'>{triggerIcon}</span>
335
+ <span className='chat-header-git__trigger-label'>{triggerLabel}</span>
63
336
  </span>
64
337
  <span className='chat-header-git__trigger-chevron material-symbols-rounded'>expand_more</span>
65
338
  </Button>
@@ -0,0 +1,148 @@
1
+ import type { GitBranchSummary } from '@vibe-forge/types'
2
+
3
+ export type GitBranchDisplayMode = 'flat' | 'tree'
4
+
5
+ export interface GitBranchTreeFolder {
6
+ entries: GitBranchTreeEntry[]
7
+ hasCurrentBranch: boolean
8
+ key: string
9
+ label: string
10
+ }
11
+
12
+ export type GitBranchTreeEntry =
13
+ | {
14
+ type: 'branch'
15
+ branch: GitBranchSummary
16
+ label: string
17
+ subtitle?: string
18
+ }
19
+ | {
20
+ type: 'folder'
21
+ folder: GitBranchTreeFolder
22
+ }
23
+
24
+ interface BranchTreeDraftFolder {
25
+ branches: GitBranchSummary[]
26
+ children: Map<string, BranchTreeDraftFolder>
27
+ hasCurrentBranch: boolean
28
+ key: string
29
+ label: string
30
+ }
31
+
32
+ const createDraftFolder = (key: string, label: string): BranchTreeDraftFolder => ({
33
+ branches: [],
34
+ children: new Map(),
35
+ hasCurrentBranch: false,
36
+ key,
37
+ label
38
+ })
39
+
40
+ const getBranchSegments = (branch: GitBranchSummary) => (
41
+ branch.kind === 'remote'
42
+ ? [branch.remoteName ?? 'remote', ...branch.localName.split('/').filter(Boolean)]
43
+ : branch.name.split('/').filter(Boolean)
44
+ )
45
+
46
+ export const getGitBranchTreeLabel = (branch: GitBranchSummary) => {
47
+ const segments = getBranchSegments(branch)
48
+ return segments.at(-1) ?? branch.localName ?? branch.name
49
+ }
50
+
51
+ const sortBranchEntries = (left: GitBranchSummary, right: GitBranchSummary) => {
52
+ if (left.isCurrent !== right.isCurrent) {
53
+ return left.isCurrent ? -1 : 1
54
+ }
55
+
56
+ return getGitBranchTreeLabel(left).localeCompare(getGitBranchTreeLabel(right))
57
+ }
58
+
59
+ const finalizeDraftFolder = (folder: BranchTreeDraftFolder): GitBranchTreeFolder => {
60
+ const folderEntries = Array.from(folder.children.values())
61
+ .sort((left, right) => {
62
+ if (left.hasCurrentBranch !== right.hasCurrentBranch) {
63
+ return left.hasCurrentBranch ? -1 : 1
64
+ }
65
+
66
+ return left.label.localeCompare(right.label)
67
+ })
68
+ .map(child => ({
69
+ type: 'folder' as const,
70
+ folder: finalizeDraftFolder(child)
71
+ }))
72
+
73
+ const branchEntries = [...folder.branches]
74
+ .sort(sortBranchEntries)
75
+ .map(branch => ({
76
+ type: 'branch' as const,
77
+ branch,
78
+ label: getGitBranchTreeLabel(branch)
79
+ }))
80
+
81
+ return {
82
+ entries: [...folderEntries, ...branchEntries],
83
+ hasCurrentBranch: folder.hasCurrentBranch,
84
+ key: folder.key,
85
+ label: folder.label
86
+ }
87
+ }
88
+
89
+ export const buildGitBranchTree = (branches: GitBranchSummary[], keyPrefix = '') => {
90
+ const root = createDraftFolder(keyPrefix, 'root')
91
+
92
+ for (const branch of branches) {
93
+ const segments = getBranchSegments(branch)
94
+ const leafLabel = segments.pop()
95
+ const branchIsCurrent = branch.isCurrent === true
96
+ const pathParts = keyPrefix === '' ? [] : [keyPrefix]
97
+ root.hasCurrentBranch = root.hasCurrentBranch || branchIsCurrent
98
+ if (leafLabel == null || leafLabel.trim() === '') {
99
+ root.branches.push(branch)
100
+ continue
101
+ }
102
+
103
+ let cursor = root
104
+ for (const segment of segments) {
105
+ pathParts.push(segment)
106
+ const existing = cursor.children.get(segment)
107
+ if (existing != null) {
108
+ cursor = existing
109
+ cursor.hasCurrentBranch = cursor.hasCurrentBranch || branchIsCurrent
110
+ continue
111
+ }
112
+
113
+ const nextFolder = createDraftFolder(pathParts.join('/'), segment)
114
+ nextFolder.hasCurrentBranch = branchIsCurrent
115
+ cursor.children.set(segment, nextFolder)
116
+ cursor = nextFolder
117
+ }
118
+
119
+ cursor.hasCurrentBranch = cursor.hasCurrentBranch || branchIsCurrent
120
+ cursor.branches.push(branch)
121
+ }
122
+
123
+ return finalizeDraftFolder(root).entries
124
+ }
125
+
126
+ export const collectGitBranchTreeFolderKeys = (entries: GitBranchTreeEntry[]): string[] => {
127
+ return entries.flatMap(entry => {
128
+ if (entry.type !== 'folder') {
129
+ return []
130
+ }
131
+
132
+ return [entry.folder.key, ...collectGitBranchTreeFolderKeys(entry.folder.entries)]
133
+ })
134
+ }
135
+
136
+ export const getGitBranchTreeFolderKeysForBranch = (branch: GitBranchSummary, keyPrefix = '') => {
137
+ const segments = getBranchSegments(branch)
138
+ segments.pop()
139
+
140
+ const folderKeys = keyPrefix === '' ? [] : [keyPrefix]
141
+ const pathParts = keyPrefix === '' ? [] : [keyPrefix]
142
+ for (const segment of segments) {
143
+ pathParts.push(segment)
144
+ folderKeys.push(pathParts.join('/'))
145
+ }
146
+
147
+ return folderKeys
148
+ }
@@ -0,0 +1,168 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import { useTranslation } from 'react-i18next'
3
+ import useSWR from 'swr'
4
+
5
+ import type { GitBranchSummary, GitRepositoryState } from '@vibe-forge/types'
6
+
7
+ import { getWorkspaceGitState, listWorkspaceGitBranches, listWorkspaceGitWorktrees } from '#~/api'
8
+ import type { ChatSessionWorkspaceDraft } from '#~/hooks/chat/chat-session-workspace-draft'
9
+
10
+ import {
11
+ filterGitBranches,
12
+ getGitBranchCheckoutBlockedPath,
13
+ getGitBranchViewState,
14
+ hasExactGitBranchMatch
15
+ } from './git-branch-utils'
16
+ import { getGitControlState } from './git-operation-utils'
17
+ import { getGitWorktreeViewState } from './git-worktree-utils'
18
+
19
+ const EMPTY_REPO_STATE: GitRepositoryState = {
20
+ available: false,
21
+ cwd: ''
22
+ }
23
+
24
+ export function useChatDraftGitControls({
25
+ draft,
26
+ onChange
27
+ }: {
28
+ draft: ChatSessionWorkspaceDraft
29
+ onChange: (nextDraft: ChatSessionWorkspaceDraft) => void
30
+ }) {
31
+ const { t } = useTranslation()
32
+ const [branchMenuOpen, setBranchMenuOpen] = useState(false)
33
+ const [worktreeMenuOpen, setWorktreeMenuOpen] = useState(false)
34
+ const [shouldLoadBranches, setShouldLoadBranches] = useState(false)
35
+ const [branchQuery, setBranchQuery] = useState('')
36
+
37
+ const { data: repoState } = useSWR<GitRepositoryState>(
38
+ 'workspace-git-state',
39
+ () => getWorkspaceGitState(),
40
+ { revalidateOnFocus: false }
41
+ )
42
+ const { data: branchData, isLoading: isBranchListLoading } = useSWR(
43
+ shouldLoadBranches ? 'workspace-git-branches' : null,
44
+ () => listWorkspaceGitBranches(),
45
+ { revalidateOnFocus: false }
46
+ )
47
+ const { data: worktreeData } = useSWR(
48
+ repoState?.available === true ? 'workspace-git-worktrees' : null,
49
+ () => listWorkspaceGitWorktrees(),
50
+ { revalidateOnFocus: false }
51
+ )
52
+
53
+ useEffect(() => {
54
+ if (!branchMenuOpen) {
55
+ setBranchQuery('')
56
+ }
57
+ }, [branchMenuOpen])
58
+
59
+ const allBranches = branchData?.branches ?? []
60
+ const filteredBranches = useMemo(() => filterGitBranches(allBranches, branchQuery), [allBranches, branchQuery])
61
+ const currentWorktreePath = draft.createWorktree ? '' : repoState?.repositoryRoot ?? ''
62
+ const { availableLocalBranches, hasResults: hasBranchResults, remoteBranches } = useMemo(
63
+ () => getGitBranchViewState(filteredBranches, allBranches, currentWorktreePath),
64
+ [allBranches, currentWorktreePath, filteredBranches]
65
+ )
66
+ const canCreateBranch = branchQuery.trim() !== '' && !hasExactGitBranchMatch(allBranches, branchQuery)
67
+ const worktreeViewState = useMemo(() =>
68
+ getGitWorktreeViewState({
69
+ currentBranch: repoState?.currentBranch,
70
+ enabled: repoState?.available === true,
71
+ repositoryRoot: repoState?.repositoryRoot,
72
+ worktrees: worktreeData?.worktrees
73
+ }), [repoState?.available, repoState?.currentBranch, repoState?.repositoryRoot, worktreeData?.worktrees])
74
+ const { currentBranchLabel: repoBranchLabel } = getGitControlState(repoState, false, {
75
+ detachedHead: t('chat.gitDetachedHead'),
76
+ pushNeedsSyncOrForce: t('chat.gitPushNeedsSyncOrForce'),
77
+ pushUnavailable: t('common.operationFailed')
78
+ })
79
+
80
+ const currentBranchLabel = draft.branch == null
81
+ ? repoBranchLabel
82
+ : draft.branch.mode === 'create'
83
+ ? t('chat.sessionWorkspaceDraftCreateBranchLabel', { branch: draft.branch.name })
84
+ : draft.branch.name
85
+
86
+ const handleDraftChange = (nextDraft: ChatSessionWorkspaceDraft) => {
87
+ onChange(nextDraft)
88
+ }
89
+
90
+ const handleBranchSwitch = (branch: GitBranchSummary) => {
91
+ handleDraftChange({
92
+ ...draft,
93
+ branch: {
94
+ mode: 'checkout',
95
+ name: branch.name,
96
+ kind: branch.kind
97
+ }
98
+ })
99
+ setBranchMenuOpen(false)
100
+ }
101
+
102
+ const handleCreateBranch = (name: string) => {
103
+ const trimmedName = name.trim()
104
+ if (trimmedName === '') {
105
+ return
106
+ }
107
+
108
+ handleDraftChange({
109
+ ...draft,
110
+ branch: {
111
+ mode: 'create',
112
+ name: trimmedName
113
+ }
114
+ })
115
+ setBranchMenuOpen(false)
116
+ }
117
+
118
+ const handleCreateWorktreeChange = (checked: boolean) => {
119
+ if (draft.branch == null || draft.branch.mode === 'create') {
120
+ handleDraftChange({
121
+ ...draft,
122
+ createWorktree: checked
123
+ })
124
+ return
125
+ }
126
+
127
+ const selectedBranch = allBranches.find(branch =>
128
+ branch.kind === draft.branch?.kind && branch.name === draft.branch?.name
129
+ )
130
+ if (selectedBranch == null) {
131
+ handleDraftChange({
132
+ ...draft,
133
+ createWorktree: checked
134
+ })
135
+ return
136
+ }
137
+
138
+ const nextCurrentWorktreePath = checked ? '' : repoState?.repositoryRoot ?? ''
139
+ const blockedPath = getGitBranchCheckoutBlockedPath(selectedBranch, allBranches, nextCurrentWorktreePath)
140
+ handleDraftChange({
141
+ ...draft,
142
+ createWorktree: checked,
143
+ branch: blockedPath == null ? draft.branch : undefined
144
+ })
145
+ }
146
+
147
+ return {
148
+ availableLocalBranches,
149
+ branchMenuOpen,
150
+ branchQuery,
151
+ canCreateBranch,
152
+ currentBranchLabel,
153
+ handleBranchSwitch,
154
+ handleCreateBranch,
155
+ handleCreateWorktreeChange,
156
+ hasBranchResults,
157
+ isBranchListLoading,
158
+ repoState: repoState ?? EMPTY_REPO_STATE,
159
+ remoteBranches,
160
+ setBranchMenuOpen,
161
+ setBranchQuery,
162
+ setShouldLoadBranches,
163
+ setWorktreeMenuOpen,
164
+ shouldLoadBranches,
165
+ worktreeMenuOpen,
166
+ worktrees: worktreeViewState.worktrees
167
+ }
168
+ }