@vibe-forge/client 0.10.1 → 0.11.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 (133) hide show
  1. package/dist/assets/{arc-C1rWFTer.js → arc-CSepokz3.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-DlZ9x70F.js → blockDiagram-c4efeb88-D0ARcoNf.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-BKKxi__y.js → c4Diagram-c83219d4-BysYF9kP.js} +1 -1
  4. package/dist/assets/channel-CeKPk6Nd.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CVGPySZq.js → classDiagram-beda092f-BG1GhIOL.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-7kp8GVVj.js → classDiagram-v2-2358418a-Dd08uGSH.js} +1 -1
  7. package/dist/assets/clone-CrkD2PuD.js +1 -0
  8. package/dist/assets/{createText-1719965b-Dykv8kT9.js → createText-1719965b-CigPEIEn.js} +1 -1
  9. package/dist/assets/{cssMode-B59COYVW.js → cssMode-MjflyEfm.js} +1 -1
  10. package/dist/assets/{edges-96097737-CkZ1ZBro.js → edges-96097737-DuTBJJRv.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-281ADcRp.js → erDiagram-0228fc6a-Cp1bL7Y7.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-BQjX_flP.js → flowDb-c6c81e3f-BfKbhiq5.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-DMHZTjES.js → flowDiagram-50d868cf-m7gGc3PK.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-4ZU4bdp1.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-CI3yz4z8.js → flowchart-elk-definition-6af322e1-EVeTDRRK.js} +1 -1
  16. package/dist/assets/{freemarker2-DWnWjibn.js → freemarker2-Bb3-QAIN.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-B3IING9L.js → ganttDiagram-a2739b55-DslB2U0R.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-CnArIr_T.js → gitGraphDiagram-82fe8481-C-KFWMXL.js} +1 -1
  19. package/dist/assets/{graph-BZ1F0Yve.js → graph-CukaUc0o.js} +1 -1
  20. package/dist/assets/{handlebars-C1QH9qTz.js → handlebars-C4le-2Y6.js} +1 -1
  21. package/dist/assets/{html-D1NkqHjC.js → html-CjNiRs5S.js} +1 -1
  22. package/dist/assets/{htmlMode-DAZCE_rA.js → htmlMode-B73_3-We.js} +1 -1
  23. package/dist/assets/{index-5325376f-Da9zSHjA.js → index-5325376f-CVISZFPw.js} +1 -1
  24. package/dist/assets/{index-C0vjF3D0.js → index-BZosmb5_.js} +336 -336
  25. package/dist/assets/index-C1oh0w9H.css +32 -0
  26. package/dist/assets/{infoDiagram-8eee0895-DYbFvRM7.js → infoDiagram-8eee0895-DoirLE1K.js} +1 -1
  27. package/dist/assets/{javascript-CoMjGRHa.js → javascript-BDjnqJFP.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-Boebox0b.js → journeyDiagram-c64418c1-Ckn-p2CM.js} +1 -1
  29. package/dist/assets/{jsonMode-D__gAvuz.js → jsonMode-C-ftOc5j.js} +1 -1
  30. package/dist/assets/{layout-CTcHNbHp.js → layout-Z7yUG7hB.js} +1 -1
  31. package/dist/assets/{line-4AwinCz2.js → line-DPG_cfAy.js} +1 -1
  32. package/dist/assets/{linear-CeSMLzJW.js → linear--GSeVfMi.js} +1 -1
  33. package/dist/assets/{liquid-DZF6egdE.js → liquid-COiLZ9py.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-6K4lv5S2.js → lspLanguageFeatures-DGmhryFq.js} +1 -1
  35. package/dist/assets/{mdx-Cnt4ka6w.js → mdx-BpL87Gej.js} +1 -1
  36. package/dist/assets/{mermaid.core-B0yG5s4D.js → mermaid.core-Cg1CCDo6.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-KJEvXMKj.js → mindmap-definition-8da855dc-CKDof1lD.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-17nFAXPJ.js → pieDiagram-a8764435-DwvCaZVE.js} +1 -1
  39. package/dist/assets/{python-DA3TtjDv.js → python-63dBmWV_.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-Dt4vubi-.js → quadrantDiagram-1e28029f-CkzYBQpy.js} +1 -1
  41. package/dist/assets/{razor-CWDJgvX_.js → razor-C50tBqEZ.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-H6aDyDK-.js → requirementDiagram-08caed73-Brgdjqf4.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-DxsVtbjI.js → sankeyDiagram-a04cb91d-CGkYexrs.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-BHa148XJ.js → sequenceDiagram-c5b8d532-D0wE-_J8.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-DgwBm8LO.js → stateDiagram-1ecb1508-BYb3NCXZ.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-BK7IQLVc.js → stateDiagram-v2-c2b004d7-DrPqi4Pt.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-DzW27Bc-.js → styles-b4e223ce-DD66TIO4.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-Dex2GiLT.js → styles-ca3715f6-iy02LHIV.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B6fGtDKS.js → styles-d45a18b0-BgqAgJyW.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B4HYgfV5.js → svgDrawCommon-b86b1483-CDq7ugnw.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080--QSbWb25.js → timeline-definition-faaaa080-DzcLLjK0.js} +1 -1
  52. package/dist/assets/{tsMode-ZM7ocZCH.js → tsMode-BFRFI4ct.js} +1 -1
  53. package/dist/assets/{typescript-CKWDmBCc.js → typescript-CBZQRAPv.js} +1 -1
  54. package/dist/assets/{xml-DuEUAzPi.js → xml-BpWm6upt.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-D09Zkv2K.js → xychartDiagram-f5964ef8-zBN8FmLQ.js} +1 -1
  56. package/dist/assets/{yaml-DL7QPRYk.js → yaml-CqbJPiIP.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +10 -10
  59. package/src/api/git.ts +78 -0
  60. package/src/api.ts +24 -0
  61. package/src/components/chat/ChatHeader.tsx +4 -0
  62. package/src/components/chat/ChatHistoryView.tsx +22 -13
  63. package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +157 -0
  64. package/src/components/chat/git-controls/ChatGitControls.scss +616 -0
  65. package/src/components/chat/git-controls/ChatGitControls.tsx +151 -0
  66. package/src/components/chat/git-controls/GitCommitModal.tsx +199 -0
  67. package/src/components/chat/git-controls/GitCommitModalParts.tsx +151 -0
  68. package/src/components/chat/git-controls/GitOperationsDropdown.tsx +123 -0
  69. package/src/components/chat/git-controls/GitPushModal.tsx +106 -0
  70. package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +68 -0
  71. package/src/components/chat/git-controls/git-branch-utils.ts +88 -0
  72. package/src/components/chat/git-controls/git-commit-utils.ts +79 -0
  73. package/src/components/chat/git-controls/git-mutation-utils.ts +69 -0
  74. package/src/components/chat/git-controls/git-operation-utils.ts +98 -0
  75. package/src/components/chat/git-controls/git-worktree-utils.ts +49 -0
  76. package/src/components/chat/git-controls/use-chat-git-commit.ts +185 -0
  77. package/src/components/chat/git-controls/use-chat-git-controls.ts +200 -0
  78. package/src/components/chat/git-controls/use-chat-git-push-state.ts +19 -0
  79. package/src/components/chat/git-controls/use-chat-git-worktrees.ts +39 -0
  80. package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
  81. package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
  82. package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
  83. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
  84. package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
  85. package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
  86. package/src/components/chat/sender/@types/sender-props.ts +0 -3
  87. package/src/components/chat/sender/Sender.scss +0 -58
  88. package/src/components/chat/sender/Sender.tsx +0 -2
  89. package/src/components/chat/tools/DefaultTool.tsx +84 -208
  90. package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
  91. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
  92. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +119 -0
  93. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +109 -0
  94. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +83 -0
  95. package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
  96. package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
  97. package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
  98. package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
  99. package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
  100. package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
  101. package/src/components/chat/tools/adapter-claude/index.ts +24 -15
  102. package/src/components/chat/tools/core/ToolCallBox.scss +362 -36
  103. package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
  104. package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
  105. package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
  106. package/src/components/chat/tools/core/ToolGroup.scss +52 -74
  107. package/src/components/chat/tools/core/ToolGroup.tsx +25 -40
  108. package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
  109. package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
  110. package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
  111. package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
  112. package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
  113. package/src/components/chat/tools/core/tool-display.ts +203 -0
  114. package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
  115. package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
  116. package/src/components/chat/tools/core/tool-summary.ts +206 -0
  117. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +59 -53
  118. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
  119. package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
  120. package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
  121. package/src/hooks/chat/interaction-state.ts +29 -9
  122. package/src/hooks/chat/session-view-cache.ts +80 -0
  123. package/src/hooks/chat/use-chat-scroll.ts +2 -2
  124. package/src/hooks/chat/use-chat-session-messages.ts +139 -39
  125. package/src/hooks/chat/use-chat-session.ts +2 -2
  126. package/src/resources/locales/en.json +149 -0
  127. package/src/resources/locales/zh.json +149 -0
  128. package/src/routes/ChatRoute.tsx +24 -27
  129. package/src/utils/strip-ansi.ts +26 -0
  130. package/dist/assets/channel-F1aqMANO.js +0 -1
  131. package/dist/assets/clone-B-GCuXNo.js +0 -1
  132. package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +0 -1
  133. package/dist/assets/index-vzEbM21t.css +0 -32
@@ -0,0 +1,185 @@
1
+ import { App } from 'antd'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ import type { GitRepositoryState } from '@vibe-forge/types'
6
+
7
+ import { commitSessionGitChanges, getApiErrorMessage, pushSessionGitBranch } from '#~/api'
8
+
9
+ import type { GitCommitNextStep } from './git-commit-utils'
10
+ import {
11
+ canGitCommitAndPush,
12
+ canSubmitGitCommit,
13
+ getGitCommitBlockedReason,
14
+ getGitCommitSummary,
15
+ isGitCommitMessageRequired
16
+ } from './git-commit-utils'
17
+ import { getGitPushBlockedReason } from './git-operation-utils'
18
+
19
+ export function useChatGitCommit({
20
+ closeOperationsMenu,
21
+ refreshGitState,
22
+ repoState,
23
+ sessionId,
24
+ setPendingAction
25
+ }: {
26
+ closeOperationsMenu: () => void
27
+ refreshGitState: (nextRepo?: GitRepositoryState) => Promise<void>
28
+ repoState: GitRepositoryState | undefined
29
+ sessionId: string
30
+ setPendingAction: (action: 'commit' | 'commit-and-push' | null) => void
31
+ }) {
32
+ const { t } = useTranslation()
33
+ const { message } = App.useApp()
34
+ const [commitModalOpen, setCommitModalOpen] = useState(false)
35
+ const [commitMessage, setCommitMessage] = useState('')
36
+ const [commitMessageError, setCommitMessageError] = useState('')
37
+ const [commitIncludeUnstagedChanges, setCommitIncludeUnstagedChanges] = useState(true)
38
+ const [commitSkipHooks, setCommitSkipHooks] = useState(false)
39
+ const [commitAmend, setCommitAmend] = useState(false)
40
+ const [commitForcePush, setCommitForcePush] = useState(false)
41
+ const [commitNextStep, setCommitNextStep] = useState<GitCommitNextStep>('commit')
42
+
43
+ const resetCommitState = () => {
44
+ setCommitModalOpen(false)
45
+ setCommitMessage('')
46
+ setCommitMessageError('')
47
+ setCommitIncludeUnstagedChanges(true)
48
+ setCommitSkipHooks(false)
49
+ setCommitAmend(false)
50
+ setCommitForcePush(false)
51
+ setCommitNextStep('commit')
52
+ }
53
+
54
+ useEffect(() => {
55
+ resetCommitState()
56
+ }, [sessionId])
57
+
58
+ const commitSummary = useMemo(() => {
59
+ if (repoState?.available !== true) {
60
+ return null
61
+ }
62
+
63
+ return getGitCommitSummary(repoState, commitIncludeUnstagedChanges)
64
+ }, [commitIncludeUnstagedChanges, repoState])
65
+
66
+ const canCommitAndPush = repoState?.available === true && canGitCommitAndPush(repoState)
67
+ const baseCommitBlockedReason = repoState?.available === true
68
+ ? getGitCommitBlockedReason(repoState, {
69
+ includeUnstagedChanges: commitIncludeUnstagedChanges,
70
+ amend: commitAmend,
71
+ commitMessage
72
+ })
73
+ : null
74
+ const commitPushBlockedReason = repoState?.available === true && commitNextStep === 'commit-and-push'
75
+ ? getGitPushBlockedReason(repoState, commitForcePush)
76
+ : null
77
+ const canSubmitCommit = repoState?.available === true && canSubmitGitCommit(repoState, {
78
+ includeUnstagedChanges: commitIncludeUnstagedChanges,
79
+ amend: commitAmend,
80
+ commitMessage
81
+ }) && commitPushBlockedReason == null
82
+ const commitBlockedReason = baseCommitBlockedReason ?? commitPushBlockedReason
83
+ const commitBlockedMessage = commitBlockedReason == null
84
+ ? ''
85
+ : commitBlockedReason === 'amend-unavailable'
86
+ ? t('chat.gitAmendUnavailable')
87
+ : commitBlockedReason === 'message-required'
88
+ ? t('chat.gitCommitMessageRequired')
89
+ : commitBlockedReason === 'behind-upstream'
90
+ ? t('chat.gitPushNeedsSyncOrForce')
91
+ : commitIncludeUnstagedChanges
92
+ ? t('chat.gitCommitNoChanges')
93
+ : t('chat.gitCommitNoStagedChanges')
94
+
95
+ useEffect(() => {
96
+ if (commitNextStep === 'commit-and-push' && !canCommitAndPush) {
97
+ setCommitNextStep('commit')
98
+ }
99
+ }, [canCommitAndPush, commitNextStep])
100
+
101
+ const handleCommit = () => {
102
+ if (repoState?.available !== true) {
103
+ return
104
+ }
105
+
106
+ const trimmedMessage = commitMessage.trim()
107
+ if (trimmedMessage === '' && isGitCommitMessageRequired(commitAmend)) {
108
+ setCommitMessageError(t('chat.gitCommitMessageRequired'))
109
+ return
110
+ }
111
+
112
+ if (!canSubmitCommit) {
113
+ void message.error(commitBlockedMessage === '' ? t('common.operationFailed') : commitBlockedMessage)
114
+ return
115
+ }
116
+
117
+ const action = commitNextStep === 'commit-and-push' ? 'commit-and-push' : 'commit'
118
+
119
+ void (async () => {
120
+ setPendingAction(action)
121
+ try {
122
+ const commitResult = await commitSessionGitChanges(sessionId, {
123
+ message: trimmedMessage === '' ? undefined : trimmedMessage,
124
+ includeUnstagedChanges: commitIncludeUnstagedChanges,
125
+ amend: commitAmend,
126
+ skipHooks: commitSkipHooks
127
+ })
128
+
129
+ if (commitNextStep === 'commit-and-push') {
130
+ try {
131
+ const pushResult = await pushSessionGitBranch(sessionId, {
132
+ force: commitForcePush
133
+ })
134
+ await refreshGitState(pushResult.repo)
135
+ resetCommitState()
136
+ closeOperationsMenu()
137
+ void message.success(commitAmend ? t('chat.gitAmendAndPushSuccess') : t('chat.gitCommitAndPushSuccess'))
138
+ } catch (error) {
139
+ await refreshGitState()
140
+ resetCommitState()
141
+ closeOperationsMenu()
142
+ void message.error(t('chat.gitCommitPushFailedAfterCommit', {
143
+ error: getApiErrorMessage(error, t('common.operationFailed'))
144
+ }))
145
+ }
146
+ return
147
+ }
148
+
149
+ await refreshGitState(commitResult.repo)
150
+ resetCommitState()
151
+ closeOperationsMenu()
152
+ void message.success(commitAmend ? t('chat.gitAmendSuccess') : t('chat.gitCommitSuccess'))
153
+ } catch (error) {
154
+ void message.error(getApiErrorMessage(error, t('common.operationFailed')))
155
+ } finally {
156
+ setPendingAction(null)
157
+ }
158
+ })()
159
+ }
160
+
161
+ return {
162
+ canCommitAndPush,
163
+ canSubmitCommit,
164
+ commitAmend,
165
+ commitBlockedMessage,
166
+ commitForcePush,
167
+ commitIncludeUnstagedChanges,
168
+ commitMessage,
169
+ commitMessageError,
170
+ commitModalOpen,
171
+ commitNextStep,
172
+ commitSkipHooks,
173
+ commitSummary,
174
+ handleCommit,
175
+ resetCommitState,
176
+ setCommitAmend,
177
+ setCommitForcePush,
178
+ setCommitIncludeUnstagedChanges,
179
+ setCommitMessage,
180
+ setCommitMessageError,
181
+ setCommitModalOpen,
182
+ setCommitNextStep,
183
+ setCommitSkipHooks
184
+ }
185
+ }
@@ -0,0 +1,200 @@
1
+ import { App } from 'antd'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import useSWR from 'swr'
5
+
6
+ import type { GitBranchListResult, GitBranchSummary, GitRepositoryState } from '@vibe-forge/types'
7
+
8
+ import {
9
+ checkoutSessionGitBranch,
10
+ createSessionGitBranch,
11
+ getApiErrorMessage,
12
+ getSessionGitState,
13
+ listSessionGitBranches
14
+ } from '#~/api'
15
+
16
+ import { filterGitBranches, getGitBranchViewState, hasExactGitBranchMatch } from './git-branch-utils'
17
+ import { runGitControlMutation, runSessionGitPush } from './git-mutation-utils'
18
+ import { getGitControlState } from './git-operation-utils'
19
+ import { useChatGitCommit } from './use-chat-git-commit'
20
+ import { useChatGitPushState } from './use-chat-git-push-state'
21
+ import { useChatGitWorktrees } from './use-chat-git-worktrees'
22
+
23
+ type GitActionKind = 'branch-create' | 'branch-switch' | 'commit' | 'commit-and-push' | 'push' | 'sync'
24
+
25
+ export function useChatGitControls(sessionId: string) {
26
+ const { t } = useTranslation()
27
+ const { message } = App.useApp()
28
+ const [branchMenuOpen, setBranchMenuOpen] = useState(false)
29
+ const [operationsMenuOpen, setOperationsMenuOpen] = useState(false)
30
+ const [shouldLoadBranches, setShouldLoadBranches] = useState(false)
31
+ const [branchQuery, setBranchQuery] = useState('')
32
+ const [pendingAction, setPendingAction] = useState<GitActionKind | null>(null)
33
+ const push = useChatGitPushState()
34
+
35
+ const { data: repoState, mutate: mutateRepoState } = useSWR<GitRepositoryState>(
36
+ ['session-git-state', sessionId],
37
+ () => getSessionGitState(sessionId),
38
+ { revalidateOnFocus: false }
39
+ )
40
+ const { data: branchData, isLoading: isBranchListLoading, mutate: mutateBranchData } = useSWR<GitBranchListResult>(
41
+ shouldLoadBranches ? ['session-git-branches', sessionId] : null,
42
+ () => listSessionGitBranches(sessionId),
43
+ { revalidateOnFocus: false }
44
+ )
45
+ const worktree = useChatGitWorktrees({
46
+ currentBranch: repoState?.currentBranch,
47
+ enabled: repoState?.available === true,
48
+ repositoryRoot: repoState?.repositoryRoot,
49
+ sessionId
50
+ })
51
+
52
+ useEffect(() => {
53
+ setBranchMenuOpen(false)
54
+ setOperationsMenuOpen(false)
55
+ worktree.setWorktreeMenuOpen(false)
56
+ setShouldLoadBranches(false)
57
+ setBranchQuery('')
58
+ setPendingAction(null)
59
+ push.resetPushState()
60
+ }, [sessionId])
61
+
62
+ const refreshGitState = async (nextRepo?: GitRepositoryState) => {
63
+ if (nextRepo != null) {
64
+ await mutateRepoState(nextRepo, { revalidate: false })
65
+ } else {
66
+ await mutateRepoState()
67
+ }
68
+
69
+ if (shouldLoadBranches) {
70
+ await mutateBranchData()
71
+ }
72
+
73
+ if (repoState?.available === true || nextRepo?.available === true) {
74
+ await worktree.mutateWorktreeData()
75
+ }
76
+ }
77
+
78
+ const commit = useChatGitCommit({
79
+ closeOperationsMenu: () => setOperationsMenuOpen(false),
80
+ refreshGitState,
81
+ repoState,
82
+ sessionId,
83
+ setPendingAction
84
+ })
85
+
86
+ const allBranches = branchData?.branches ?? []
87
+ const filteredBranches = useMemo(() => filterGitBranches(allBranches, branchQuery), [allBranches, branchQuery])
88
+ const currentWorktreePath = repoState?.available === true ? repoState.repositoryRoot ?? '' : ''
89
+ const { availableLocalBranches, hasResults: hasBranchResults, remoteBranches } = useMemo(
90
+ () => getGitBranchViewState(filteredBranches, allBranches, currentWorktreePath),
91
+ [allBranches, filteredBranches, currentWorktreePath]
92
+ )
93
+ const canCreateBranch = branchQuery.trim() !== '' && !hasExactGitBranchMatch(allBranches, branchQuery)
94
+ const isBusy = pendingAction != null
95
+
96
+ const { currentBranchLabel, pushBlockedMessage, pushBlockedReason } = getGitControlState(repoState, push.pushForce, {
97
+ detachedHead: t('chat.gitDetachedHead'),
98
+ pushNeedsSyncOrForce: t('chat.gitPushNeedsSyncOrForce'),
99
+ pushUnavailable: t('common.operationFailed')
100
+ })
101
+
102
+ const runMutation = async (
103
+ action: Exclude<GitActionKind, 'commit' | 'commit-and-push'>,
104
+ task: () => Promise<{ repo: GitRepositoryState }>,
105
+ successMessage: string,
106
+ onSuccess?: () => void
107
+ ) =>
108
+ runGitControlMutation({
109
+ action,
110
+ notifyError: error => void message.error(getApiErrorMessage(error, t('common.operationFailed'))),
111
+ notifySuccess: nextMessage => void message.success(nextMessage),
112
+ onSuccess,
113
+ refreshGitState,
114
+ setPendingAction,
115
+ successMessage,
116
+ task
117
+ })
118
+
119
+ const closeBranchMenu = () => {
120
+ setBranchMenuOpen(false)
121
+ setBranchQuery('')
122
+ }
123
+
124
+ const handleBranchSwitch = (branch: GitBranchSummary) => {
125
+ void runMutation(
126
+ 'branch-switch',
127
+ () => checkoutSessionGitBranch(sessionId, { name: branch.name, kind: branch.kind }),
128
+ t('chat.gitSwitchBranchSuccess', { branch: branch.kind === 'local' ? branch.name : branch.localName }),
129
+ closeBranchMenu
130
+ )
131
+ }
132
+
133
+ const handleCreateBranch = (name: string) => {
134
+ const trimmedName = name.trim()
135
+ if (trimmedName === '') {
136
+ return
137
+ }
138
+
139
+ void runMutation(
140
+ 'branch-create',
141
+ () => createSessionGitBranch(sessionId, { name: trimmedName }),
142
+ t('chat.gitCreateBranchSuccess', { branch: trimmedName }),
143
+ closeBranchMenu
144
+ )
145
+ }
146
+
147
+ const handleOpenPushModal = () => {
148
+ setOperationsMenuOpen(false)
149
+ push.setPushModalOpen(true)
150
+ }
151
+
152
+ const handlePush = () => {
153
+ void runSessionGitPush({
154
+ blockedMessage: pushBlockedMessage,
155
+ blockedReason: pushBlockedReason,
156
+ force: push.pushForce,
157
+ notifyBlocked: nextMessage => void message.error(nextMessage),
158
+ onSuccess: push.resetPushState,
159
+ repoState,
160
+ runMutation,
161
+ sessionId,
162
+ successMessage: push.pushForce ? t('chat.gitForcePushSuccess') : t('chat.gitPushSuccess')
163
+ })
164
+ }
165
+
166
+ return {
167
+ branchMenuOpen,
168
+ branchQuery,
169
+ canCreateBranch,
170
+ currentBranchLabel,
171
+ handleBranchSwitch,
172
+ handleCreateBranch,
173
+ handleOpenPushModal,
174
+ handlePush,
175
+ hasBranchResults,
176
+ isBranchListLoading,
177
+ isBusy,
178
+ availableLocalBranches,
179
+ operationsMenuOpen,
180
+ pendingAction,
181
+ pushBlockedMessage,
182
+ pushForce: push.pushForce,
183
+ pushModalOpen: push.pushModalOpen,
184
+ remoteBranches,
185
+ repoState,
186
+ runMutation,
187
+ showWorktreeButton: worktree.showWorktreeButton,
188
+ worktreeMenuOpen: worktree.worktreeMenuOpen,
189
+ worktrees: worktree.worktrees,
190
+ setBranchMenuOpen,
191
+ setBranchQuery,
192
+ setOperationsMenuOpen,
193
+ setPushForce: push.setPushForce,
194
+ setPushModalOpen: push.setPushModalOpen,
195
+ setShouldLoadBranches,
196
+ setWorktreeMenuOpen: worktree.setWorktreeMenuOpen,
197
+ resetPushState: push.resetPushState,
198
+ ...commit
199
+ }
200
+ }
@@ -0,0 +1,19 @@
1
+ import { useState } from 'react'
2
+
3
+ export function useChatGitPushState() {
4
+ const [pushModalOpen, setPushModalOpen] = useState(false)
5
+ const [pushForce, setPushForce] = useState(false)
6
+
7
+ const resetPushState = () => {
8
+ setPushModalOpen(false)
9
+ setPushForce(false)
10
+ }
11
+
12
+ return {
13
+ pushForce,
14
+ pushModalOpen,
15
+ resetPushState,
16
+ setPushForce,
17
+ setPushModalOpen
18
+ }
19
+ }
@@ -0,0 +1,39 @@
1
+ import { useMemo, useState } from 'react'
2
+ import useSWR from 'swr'
3
+
4
+ import type { GitWorktreeListResult } from '@vibe-forge/types'
5
+
6
+ import { listSessionGitWorktrees } from '#~/api'
7
+ import { getGitWorktreeViewState } from './git-worktree-utils'
8
+
9
+ export function useChatGitWorktrees(input: {
10
+ currentBranch?: string | null
11
+ enabled: boolean
12
+ repositoryRoot?: string
13
+ sessionId: string
14
+ }) {
15
+ const [worktreeMenuOpen, setWorktreeMenuOpen] = useState(false)
16
+ const { data: worktreeData, mutate: mutateWorktreeData } = useSWR<GitWorktreeListResult>(
17
+ input.enabled ? ['session-git-worktrees', input.sessionId] : null,
18
+ () => listSessionGitWorktrees(input.sessionId),
19
+ { revalidateOnFocus: false }
20
+ )
21
+ const viewState = useMemo(
22
+ () =>
23
+ getGitWorktreeViewState({
24
+ currentBranch: input.currentBranch,
25
+ enabled: input.enabled,
26
+ repositoryRoot: input.repositoryRoot,
27
+ worktrees: worktreeData?.worktrees
28
+ }),
29
+ [input.currentBranch, input.enabled, input.repositoryRoot, worktreeData?.worktrees]
30
+ )
31
+
32
+ return {
33
+ mutateWorktreeData,
34
+ showWorktreeButton: viewState.showWorktreeButton,
35
+ worktreeMenuOpen,
36
+ worktrees: viewState.worktrees,
37
+ setWorktreeMenuOpen
38
+ }
39
+ }
@@ -0,0 +1,163 @@
1
+ .message-status-notice {
2
+ width: 100%;
3
+
4
+ &__card {
5
+ width: 100%;
6
+ padding: 7px 12px;
7
+ border: 0;
8
+ border-radius: 0;
9
+ background: transparent;
10
+ box-shadow: none;
11
+ }
12
+
13
+ &__icon {
14
+ font-size: 14px;
15
+ line-height: 1;
16
+ flex-shrink: 0;
17
+ }
18
+
19
+ &__content {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 4px;
23
+ min-width: 0;
24
+ flex: 1;
25
+ }
26
+
27
+ &__header {
28
+ display: flex;
29
+ align-items: flex-start;
30
+ justify-content: space-between;
31
+ gap: 6px;
32
+ min-width: 0;
33
+ }
34
+
35
+ &__title-row {
36
+ display: flex;
37
+ align-items: flex-start;
38
+ gap: 6px;
39
+ min-width: 0;
40
+ flex-wrap: wrap;
41
+ }
42
+
43
+ &__title {
44
+ font-size: 12px;
45
+ line-height: 1.3;
46
+ font-weight: 500;
47
+ }
48
+
49
+ &__badge,
50
+ &__meta {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ padding: 0;
54
+ border-radius: 0;
55
+ font-size: 10px;
56
+ line-height: 1.2;
57
+ white-space: nowrap;
58
+ }
59
+
60
+ &__badge {
61
+ opacity: .72;
62
+ }
63
+
64
+ &__meta {
65
+ font-family:
66
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono,
67
+ Courier New, monospace;
68
+ opacity: .8;
69
+ }
70
+
71
+ &__message {
72
+ color: var(--text-color);
73
+ font-size: 12px;
74
+ line-height: 1.45;
75
+ word-break: break-word;
76
+ }
77
+
78
+ &__detail {
79
+ color: var(--sub-text-color, #6b7280);
80
+ font-size: 11px;
81
+ line-height: 1.35;
82
+ word-break: break-word;
83
+ }
84
+
85
+ &__actions {
86
+ display: flex;
87
+ align-items: center;
88
+ gap: 6px;
89
+ padding-top: 1px;
90
+ }
91
+
92
+ &--error {
93
+ .message-status-notice__icon,
94
+ .message-status-notice__title {
95
+ color: #a16262;
96
+ }
97
+
98
+ .message-status-notice__badge,
99
+ .message-status-notice__meta {
100
+ color: #b07a7a;
101
+ }
102
+ }
103
+
104
+ &--warning {
105
+ .message-status-notice__icon,
106
+ .message-status-notice__title {
107
+ color: #9a7b4f;
108
+ }
109
+
110
+ .message-status-notice__badge,
111
+ .message-status-notice__meta {
112
+ color: #aa8b62;
113
+ }
114
+ }
115
+ }
116
+
117
+ html.dark {
118
+ .message-status-notice {
119
+ &--error {
120
+ .message-status-notice__icon,
121
+ .message-status-notice__title,
122
+ .message-status-notice__badge,
123
+ .message-status-notice__meta {
124
+ color: #d9b5b5;
125
+ }
126
+
127
+ .message-status-notice__badge,
128
+ .message-status-notice__meta {
129
+ background: transparent;
130
+ }
131
+
132
+ .message-status-notice__message {
133
+ color: #f3f4f6;
134
+ }
135
+
136
+ .message-status-notice__detail {
137
+ color: #cba0a0;
138
+ }
139
+ }
140
+
141
+ &--warning {
142
+ .message-status-notice__icon,
143
+ .message-status-notice__title,
144
+ .message-status-notice__badge,
145
+ .message-status-notice__meta {
146
+ color: #d7c399;
147
+ }
148
+
149
+ .message-status-notice__badge,
150
+ .message-status-notice__meta {
151
+ background: transparent;
152
+ }
153
+
154
+ .message-status-notice__message {
155
+ color: #f3f4f6;
156
+ }
157
+
158
+ .message-status-notice__detail {
159
+ color: #c8b27c;
160
+ }
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,48 @@
1
+ import './MessageStatusNotice.scss'
2
+
3
+ import { Button } from 'antd'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import type { ChatHistoryStatusNotice } from './build-chat-history-status-notices'
7
+
8
+ export function MessageStatusNotice({
9
+ notice,
10
+ onRetryConnection
11
+ }: {
12
+ notice: ChatHistoryStatusNotice
13
+ onRetryConnection: () => void
14
+ }) {
15
+ const { t } = useTranslation()
16
+
17
+ return (
18
+ <div className={`message-status-notice message-status-notice--${notice.tone} ${notice.isMock ? 'is-mock' : ''}`}>
19
+ <div className='message-status-notice__card' role='status' aria-live='polite'>
20
+ <div className='message-status-notice__content'>
21
+ <div className='message-status-notice__header'>
22
+ <div className='message-status-notice__title-row'>
23
+ <span className='material-symbols-rounded message-status-notice__icon'>{notice.icon}</span>
24
+ <div className='message-status-notice__title'>{notice.title}</div>
25
+ {notice.isMock && (
26
+ <span className='message-status-notice__badge'>{t('chat.debugMockLabel')}</span>
27
+ )}
28
+ </div>
29
+ {notice.meta != null && notice.meta !== '' && (
30
+ <span className='message-status-notice__meta'>{notice.meta}</span>
31
+ )}
32
+ </div>
33
+ <div className='message-status-notice__message'>{notice.message}</div>
34
+ {notice.detail != null && notice.detail !== '' && (
35
+ <div className='message-status-notice__detail'>{notice.detail}</div>
36
+ )}
37
+ {notice.action === 'retry-connection' && (
38
+ <div className='message-status-notice__actions'>
39
+ <Button size='small' onClick={onRetryConnection}>
40
+ {t('chat.retryConnection')}
41
+ </Button>
42
+ </div>
43
+ )}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ )
48
+ }