@vibe-forge/client 0.11.0 → 0.11.2

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 (97) hide show
  1. package/cli.cjs +6 -1
  2. package/dist/assets/{arc-M4HYfcHs.js → arc-De_WjPJ3.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CUrDjrxj.js → blockDiagram-c4efeb88-C4aR2zTE.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BMEtqlFp.js → c4Diagram-c83219d4-BZH3rq_m.js} +1 -1
  5. package/dist/assets/channel-BvERb8WU.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BOmDJ0Ml.js → classDiagram-beda092f-BzJgBrIK.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-BODzX2MB.js → classDiagram-v2-2358418a-5ZtXcnT3.js} +1 -1
  8. package/dist/assets/clone-B9_0v-6Y.js +1 -0
  9. package/dist/assets/{createText-1719965b-B9Dd8zcR.js → createText-1719965b-DUVvEtmR.js} +1 -1
  10. package/dist/assets/{cssMode-DLxG92Ot.js → cssMode-GoTNjuXX.js} +1 -1
  11. package/dist/assets/{edges-96097737-CuZFd43m.js → edges-96097737-Dd7m4Cvs.js} +1 -1
  12. package/dist/assets/{erDiagram-0228fc6a-8g9lu2-Z.js → erDiagram-0228fc6a-DxqFlG_f.js} +1 -1
  13. package/dist/assets/{flowDb-c6c81e3f-BlBS1tdN.js → flowDb-c6c81e3f-DU0C5kCI.js} +1 -1
  14. package/dist/assets/{flowDiagram-50d868cf-u6mWflpF.js → flowDiagram-50d868cf-Di1uDa_X.js} +1 -1
  15. package/dist/assets/flowDiagram-v2-4f6560a1-LpS8Kb00.js +1 -0
  16. package/dist/assets/{flowchart-elk-definition-6af322e1-BDqI2NFr.js → flowchart-elk-definition-6af322e1-CwG8aty5.js} +1 -1
  17. package/dist/assets/{freemarker2-tVtpTMPu.js → freemarker2-j39cqTlI.js} +1 -1
  18. package/dist/assets/{ganttDiagram-a2739b55-CDQjx9Wu.js → ganttDiagram-a2739b55-baO_lzL-.js} +1 -1
  19. package/dist/assets/{gitGraphDiagram-82fe8481-DUHFKRVA.js → gitGraphDiagram-82fe8481-COoHjYMf.js} +1 -1
  20. package/dist/assets/{graph-2HKPi5B_.js → graph-KxESr4M5.js} +1 -1
  21. package/dist/assets/{handlebars-D00tgNd8.js → handlebars-BgjdZO8G.js} +1 -1
  22. package/dist/assets/{html-B-TDzBiR.js → html-Ba7tYObe.js} +1 -1
  23. package/dist/assets/{htmlMode-ClycqSTM.js → htmlMode-Bztvbig1.js} +1 -1
  24. package/dist/assets/{index-5325376f-DPrJpRQ-.js → index-5325376f-BMTAx2mL.js} +1 -1
  25. package/dist/assets/index-C1oh0w9H.css +32 -0
  26. package/dist/assets/{index-CAHZZEoo.js → index-Pm_kLJvG.js} +330 -326
  27. package/dist/assets/{infoDiagram-8eee0895-Co5tS1I5.js → infoDiagram-8eee0895-CC74qbHY.js} +1 -1
  28. package/dist/assets/{javascript-zbkwarmb.js → javascript-C1e1cllX.js} +1 -1
  29. package/dist/assets/{journeyDiagram-c64418c1-k_qioHgy.js → journeyDiagram-c64418c1-C4MyOdE6.js} +1 -1
  30. package/dist/assets/{jsonMode-C3CSpzBF.js → jsonMode-BC98AlvF.js} +1 -1
  31. package/dist/assets/{layout-CjOXKxvs.js → layout-CxAyTlr7.js} +1 -1
  32. package/dist/assets/{line-C-XnQrKR.js → line-DhaUfI71.js} +1 -1
  33. package/dist/assets/{linear-C7MMERzS.js → linear-MYukzldK.js} +1 -1
  34. package/dist/assets/{liquid-5G37EU6K.js → liquid-DahfJEYl.js} +1 -1
  35. package/dist/assets/{lspLanguageFeatures-zaDMuhCE.js → lspLanguageFeatures-BWDJcswW.js} +1 -1
  36. package/dist/assets/{mdx-Bc-LY0gi.js → mdx-BELlF_FD.js} +1 -1
  37. package/dist/assets/{mermaid.core-CechbHof.js → mermaid.core-BrQnSGSY.js} +4 -4
  38. package/dist/assets/{mindmap-definition-8da855dc-ejftCDGb.js → mindmap-definition-8da855dc-B0FoxTiy.js} +1 -1
  39. package/dist/assets/{pieDiagram-a8764435-DY__X3Qj.js → pieDiagram-a8764435-Ddr2cjSL.js} +1 -1
  40. package/dist/assets/{python-vK2Ff2J5.js → python--C9if_AD.js} +1 -1
  41. package/dist/assets/{quadrantDiagram-1e28029f-azIZCv_2.js → quadrantDiagram-1e28029f-BlEs7Mrl.js} +1 -1
  42. package/dist/assets/{razor-BipjBJKu.js → razor-B9U9JxKn.js} +1 -1
  43. package/dist/assets/{requirementDiagram-08caed73-C4EB0Xs2.js → requirementDiagram-08caed73-kEFOAu2v.js} +1 -1
  44. package/dist/assets/{sankeyDiagram-a04cb91d-PNhR6YWu.js → sankeyDiagram-a04cb91d-BBghez8I.js} +1 -1
  45. package/dist/assets/{sequenceDiagram-c5b8d532-4c-qV-Ri.js → sequenceDiagram-c5b8d532-CJqgzdUE.js} +1 -1
  46. package/dist/assets/{stateDiagram-1ecb1508-CnURumPE.js → stateDiagram-1ecb1508-BER4XEI6.js} +1 -1
  47. package/dist/assets/{stateDiagram-v2-c2b004d7-DR2qHTPg.js → stateDiagram-v2-c2b004d7-EBV2vSks.js} +1 -1
  48. package/dist/assets/{styles-b4e223ce-B2PWXT_i.js → styles-b4e223ce-k0eswZsE.js} +1 -1
  49. package/dist/assets/{styles-ca3715f6-DEhgVF5H.js → styles-ca3715f6-Ckr7GA-0.js} +1 -1
  50. package/dist/assets/{styles-d45a18b0-DyzccA5F.js → styles-d45a18b0-C1bpSwV3.js} +1 -1
  51. package/dist/assets/{svgDrawCommon-b86b1483-C_1tMhxp.js → svgDrawCommon-b86b1483-CDtKpGvy.js} +1 -1
  52. package/dist/assets/{timeline-definition-faaaa080-FdaC0dQH.js → timeline-definition-faaaa080-BeGR-vua.js} +1 -1
  53. package/dist/assets/{tsMode-CrMC5T3_.js → tsMode-D_gJXIy3.js} +1 -1
  54. package/dist/assets/{typescript-CRfPu8v7.js → typescript-BoKcNXkN.js} +1 -1
  55. package/dist/assets/{xml-jlRvQfFI.js → xml-DZvURlJ-.js} +1 -1
  56. package/dist/assets/{xychartDiagram-f5964ef8-sxjv75h9.js → xychartDiagram-f5964ef8-DxfeLuYV.js} +1 -1
  57. package/dist/assets/{yaml-B47_IHOH.js → yaml-CTC8PAGY.js} +1 -1
  58. package/dist/index.html +2 -2
  59. package/package.json +9 -9
  60. package/src/api/git.ts +78 -0
  61. package/src/api.ts +24 -0
  62. package/src/components/chat/ChatHeader.tsx +4 -0
  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/tools/DefaultTool.tsx +40 -31
  81. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +1 -15
  82. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +8 -1
  83. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +10 -95
  84. package/src/components/chat/tools/core/ToolCallBox.scss +18 -0
  85. package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
  86. package/src/components/chat/tools/core/tool-display.ts +12 -1
  87. package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
  88. package/src/components/chat/tools/core/tool-summary.ts +18 -6
  89. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +0 -7
  90. package/src/hooks/chat/session-view-cache.ts +80 -0
  91. package/src/hooks/chat/use-chat-session-messages.ts +124 -30
  92. package/src/resources/locales/en.json +72 -4
  93. package/src/resources/locales/zh.json +72 -4
  94. package/dist/assets/channel-Cj3Cp2OJ.js +0 -1
  95. package/dist/assets/clone-B7Q9B1dS.js +0 -1
  96. package/dist/assets/flowDiagram-v2-4f6560a1-G3v545eF.js +0 -1
  97. package/dist/assets/index-Di7lePfb.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
+ }
@@ -1,16 +1,16 @@
1
- import { Tooltip } from 'antd'
1
+ import { useMemo } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
 
4
4
  import type { ChatMessageContent } from '@vibe-forge/core'
5
5
 
6
- import { CodeBlock } from '#~/components/CodeBlock'
7
- import { safeJsonStringify } from '#~/utils/safe-serialize'
8
-
9
6
  import { ToolCallBox } from './core/ToolCallBox'
7
+ import { ToolDiffViewer } from './core/ToolDiffViewer'
10
8
  import { ToolResultContent } from './core/ToolResultContent'
11
9
  import { ToolSummaryHeader } from './core/ToolSummaryHeader'
10
+ import { buildGenericToolPresentation } from './core/generic-tool-presentation'
12
11
  import { hasMeaningfulToolValue } from './core/tool-content-presence'
13
- import { TOOL_TOOLTIP_PROPS, getToolSectionIcon, getToolTargetPresentation } from './core/tool-display'
12
+ import { getToolTargetPresentation } from './core/tool-display'
13
+ import { ToolInlineFields, renderToolBlockField } from './core/tool-field-sections'
14
14
  import { getToolPrimaryText, getToolTitleText } from './core/tool-summary'
15
15
 
16
16
  export function DefaultTool({
@@ -21,11 +21,18 @@ export function DefaultTool({
21
21
  resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
22
22
  }) {
23
23
  const { t } = useTranslation()
24
- const hasCallDetails = hasMeaningfulToolValue(item.input)
24
+ const view = useMemo(() => buildGenericToolPresentation(item.name, item.input), [item.input, item.name])
25
+ const hasCallDetails = view.inlineFields.length > 0 || view.blockFields.length > 0 || view.diff != null
25
26
  const hasResultDetails = resultItem != null && hasMeaningfulToolValue(resultItem.content)
26
- const hasDetails = hasCallDetails || hasResultDetails
27
- const titleText = getToolTitleText(item, t)
28
- const targetPresentation = getToolTargetPresentation(getToolPrimaryText(item))
27
+ const showResultDetails = hasResultDetails && !(view.suppressSuccessResult === true && resultItem?.is_error !== true)
28
+ const hasDetails = hasCallDetails || showResultDetails
29
+ const titleText = view.titleKey != null
30
+ ? t(view.titleKey, { defaultValue: view.fallbackTitle })
31
+ : getToolTitleText(item, t)
32
+ const targetPresentation = getToolTargetPresentation(view.primary ?? getToolPrimaryText(item))
33
+ const preferMarkdown = ['webfetch', 'websearch'].includes(
34
+ item.name.split(':').pop()?.replace(/[^a-z0-9]+/gi, '').toLowerCase() ?? ''
35
+ )
29
36
  const errorMeta = resultItem?.is_error === true
30
37
  ? (
31
38
  <span className='tool-status tool-status--error'>
@@ -42,7 +49,7 @@ export function DefaultTool({
42
49
  collapsible={hasDetails}
43
50
  header={({ isExpanded, isCollapsible }) => (
44
51
  <ToolSummaryHeader
45
- icon={<span className='material-symbols-rounded'>build</span>}
52
+ icon={<span className='material-symbols-rounded'>{view.icon}</span>}
46
53
  title={titleText}
47
54
  target={targetPresentation.text}
48
55
  targetTitle={targetPresentation.title}
@@ -56,32 +63,34 @@ export function DefaultTool({
56
63
  content={hasDetails
57
64
  ? (
58
65
  <div className='tool-detail-sections'>
59
- {hasCallDetails && (
66
+ <ToolInlineFields fields={view.inlineFields} t={t} />
67
+ {view.diff != null && (
60
68
  <div className='tool-detail-section'>
61
- <div className='tool-detail-section__header'>
62
- <Tooltip title={t('chat.tools.call')} {...TOOL_TOOLTIP_PROPS}>
63
- <span className='tool-detail-section__icon material-symbols-rounded'>
64
- {getToolSectionIcon('call')}
65
- </span>
66
- </Tooltip>
67
- </div>
68
- <CodeBlock
69
- code={safeJsonStringify(item.input != null ? item.input : {}, 2)}
70
- lang='json'
71
- hideHeader={true}
69
+ <ToolDiffViewer
70
+ original={view.diff.original}
71
+ modified={view.diff.modified}
72
+ language={view.diff.language}
73
+ metaItems={(view.diff.metaItems ?? []).map(item => ({
74
+ icon: item.icon,
75
+ label: t(item.labelKey, { defaultValue: item.fallbackLabel }),
76
+ value: item.value != null && item.value !== ''
77
+ ? (item.value === 'true'
78
+ ? t('chat.tools.booleanOn')
79
+ : item.value === 'false'
80
+ ? t('chat.tools.booleanOff')
81
+ : item.value)
82
+ : undefined,
83
+ tone: item.tone
84
+ }))}
85
+ splitLabel={t('chat.tools.diffSplit')}
86
+ inlineLabel={t('chat.tools.diffInline')}
72
87
  />
73
88
  </div>
74
89
  )}
75
- {hasResultDetails && resultItem != null && (
90
+ {view.blockFields.map((field, index) => renderToolBlockField(field, index, t))}
91
+ {showResultDetails && resultItem != null && (
76
92
  <div className='tool-detail-section'>
77
- <div className='tool-detail-section__header'>
78
- <Tooltip title={t('chat.result')} {...TOOL_TOOLTIP_PROPS}>
79
- <span className='tool-detail-section__icon material-symbols-rounded'>
80
- {getToolSectionIcon('result')}
81
- </span>
82
- </Tooltip>
83
- </div>
84
- <ToolResultContent content={resultItem.content} />
93
+ <ToolResultContent content={resultItem.content} preferMarkdown={preferMarkdown} />
85
94
  </div>
86
95
  )}
87
96
  </div>
@@ -1,6 +1,5 @@
1
1
  import './GenericClaudeTool.scss'
2
2
 
3
- import { Tooltip } from 'antd'
4
3
  import React, { useMemo } from 'react'
5
4
  import { useTranslation } from 'react-i18next'
6
5
 
@@ -8,13 +7,7 @@ import { ToolCallBox } from '../core/ToolCallBox'
8
7
  import { ToolResultContent } from '../core/ToolResultContent'
9
8
  import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
10
9
  import { hasMeaningfulToolValue } from '../core/tool-content-presence'
11
- import {
12
- TOOL_TOOLTIP_PROPS,
13
- getToolFieldIcon,
14
- getToolInlineValueText,
15
- getToolSectionIcon,
16
- getToolTargetPresentation
17
- } from '../core/tool-display'
10
+ import { getToolFieldIcon, getToolInlineValueText, getToolTargetPresentation } from '../core/tool-display'
18
11
  import { defineToolRender } from '../defineToolRender'
19
12
  import { ClaudeEditDiff } from './ClaudeEditDiff'
20
13
  import { ClaudeToolInlineFields, renderClaudeBlockField } from './claude-tool-field-sections'
@@ -114,13 +107,6 @@ export const GenericClaudeTool = defineToolRender(({ item, resultItem }) => {
114
107
  {blockFields.map((field, index) => renderClaudeBlockField(field, index, t))}
115
108
  {showResultDetails && resultItem != null && (
116
109
  <div className='tool-detail-section'>
117
- <div className='tool-detail-section__header'>
118
- <Tooltip title={t('chat.result')} {...TOOL_TOOLTIP_PROPS}>
119
- <span className='tool-detail-section__icon material-symbols-rounded'>
120
- {getToolSectionIcon('result')}
121
- </span>
122
- </Tooltip>
123
- </div>
124
110
  <ToolResultContent content={resultItem.content} preferMarkdown={preferMarkdown} />
125
111
  </div>
126
112
  )}
@@ -21,7 +21,14 @@ export function buildClaudeEditToolPresentation(params: BuilderParams) {
21
21
  const data = todo as Record<string, unknown>
22
22
  const content = asString(data.content)
23
23
  const status = asString(data.status)
24
- return content != null ? [`[${status ?? 'pending'}] ${content}`] : []
24
+ const activeForm = asString(data.activeForm)
25
+ if (content == null) {
26
+ return []
27
+ }
28
+ const detail = status === 'in_progress' && activeForm != null && activeForm !== content
29
+ ? ` (${activeForm})`
30
+ : ''
31
+ return [`[${status ?? 'pending'}] ${content}${detail}`]
25
32
  })
26
33
  : undefined
27
34