@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,106 @@
1
+ import { Button, Modal } from 'antd'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import { GitCommitToggleRow } from './GitCommitModalParts'
5
+
6
+ export function GitPushModal({
7
+ blockedMessage,
8
+ currentBranchLabel,
9
+ forcePush,
10
+ hasUpstream,
11
+ open,
12
+ pending,
13
+ upstreamLabel,
14
+ onCancel,
15
+ onPush,
16
+ onToggleForcePush
17
+ }: {
18
+ blockedMessage: string
19
+ currentBranchLabel: string
20
+ forcePush: boolean
21
+ hasUpstream: boolean
22
+ open: boolean
23
+ pending: boolean
24
+ upstreamLabel: string
25
+ onCancel: () => void
26
+ onPush: () => void
27
+ onToggleForcePush: (checked: boolean) => void
28
+ }) {
29
+ const { t } = useTranslation()
30
+
31
+ return (
32
+ <Modal
33
+ centered
34
+ className='chat-header-git__commit-modal'
35
+ closeIcon={<span className='material-symbols-rounded'>close</span>}
36
+ destroyOnHidden
37
+ footer={null}
38
+ open={open}
39
+ title={null}
40
+ width={456}
41
+ onCancel={onCancel}
42
+ >
43
+ <div className='chat-header-git__commit-sheet'>
44
+ <div className='chat-header-git__commit-icon'>
45
+ <span className='material-symbols-rounded'>upload</span>
46
+ </div>
47
+ <div className='chat-header-git__commit-title'>
48
+ {t('chat.gitPushPanelTitle')}
49
+ </div>
50
+
51
+ <div className='chat-header-git__commit-summary-grid'>
52
+ <div className='chat-header-git__commit-summary-label'>{t('chat.gitCommitPanelBranch')}</div>
53
+ <div className='chat-header-git__commit-summary-value chat-header-git__commit-summary-value--branch'>
54
+ <span className='material-symbols-rounded'>call_split</span>
55
+ <span>{currentBranchLabel}</span>
56
+ </div>
57
+
58
+ <div className='chat-header-git__commit-summary-label'>{t('chat.gitPushPanelUpstream')}</div>
59
+ <div className='chat-header-git__commit-summary-value chat-header-git__commit-summary-value--branch'>
60
+ <span className='material-symbols-rounded'>cloud_upload</span>
61
+ <span>{upstreamLabel}</span>
62
+ </div>
63
+ </div>
64
+
65
+ {!hasUpstream && (
66
+ <div className='chat-header-git__overlay-meta'>
67
+ {t('chat.gitPushPanelUpstreamHint')}
68
+ </div>
69
+ )}
70
+
71
+ <div className='chat-header-git__commit-toggles'>
72
+ <GitCommitToggleRow
73
+ checked={forcePush}
74
+ description={t('chat.gitForcePushDescription')}
75
+ disabled={pending}
76
+ title={t('chat.gitForcePush')}
77
+ onChange={onToggleForcePush}
78
+ />
79
+ </div>
80
+
81
+ {forcePush && (
82
+ <div className='chat-header-git__overlay-meta'>
83
+ {t('chat.gitForcePushHint')}
84
+ </div>
85
+ )}
86
+ {blockedMessage !== '' && (
87
+ <div className='chat-header-git__overlay-meta'>
88
+ {blockedMessage}
89
+ </div>
90
+ )}
91
+
92
+ <div className='chat-header-git__commit-footer'>
93
+ <Button
94
+ className='chat-header-git__commit-submit'
95
+ disabled={blockedMessage !== ''}
96
+ loading={pending}
97
+ type='primary'
98
+ onClick={onPush}
99
+ >
100
+ {t('common.continue')}
101
+ </Button>
102
+ </div>
103
+ </div>
104
+ </Modal>
105
+ )
106
+ }
@@ -0,0 +1,68 @@
1
+ import { Button, Dropdown } from 'antd'
2
+ import { useTranslation } from 'react-i18next'
3
+
4
+ import type { GitWorktreeSummary } from '@vibe-forge/types'
5
+
6
+ import { formatGitWorktreePathLabel } from './git-branch-utils'
7
+
8
+ export function GitWorktreeDropdown({
9
+ open,
10
+ worktrees,
11
+ onOpenChange
12
+ }: {
13
+ open: boolean
14
+ worktrees: GitWorktreeSummary[]
15
+ onOpenChange: (open: boolean) => void
16
+ }) {
17
+ const { t } = useTranslation()
18
+
19
+ return (
20
+ <Dropdown
21
+ open={open}
22
+ placement='bottomLeft'
23
+ trigger={['click']}
24
+ onOpenChange={onOpenChange}
25
+ 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 => (
29
+ <div
30
+ key={worktree.path}
31
+ className='chat-header-git__worktree-row'
32
+ title={worktree.path}
33
+ >
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>
43
+ </span>
44
+ </div>
45
+ {worktree.isCurrent && (
46
+ <span className='chat-header-git__row-state material-symbols-rounded'>check</span>
47
+ )}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ )}
53
+ >
54
+ <Button
55
+ type='text'
56
+ className={`chat-header-git__trigger ${open ? 'is-open' : ''}`}
57
+ title={t('chat.gitWorktree')}
58
+ aria-label={t('chat.gitWorktree')}
59
+ >
60
+ <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>
63
+ </span>
64
+ <span className='chat-header-git__trigger-chevron material-symbols-rounded'>expand_more</span>
65
+ </Button>
66
+ </Dropdown>
67
+ )
68
+ }
@@ -0,0 +1,88 @@
1
+ import type { GitBranchSummary } from '@vibe-forge/types'
2
+
3
+ const normalizeBranchText = (value: string) => value.trim().toLowerCase()
4
+ const normalizeWorktreePath = (value: string) => value.trim().replace(/[/\\]+$/, '').replace(/\\/g, '/')
5
+
6
+ export const formatGitWorktreePathLabel = (value: string) => {
7
+ const segments = normalizeWorktreePath(value).split('/').filter(Boolean)
8
+ if (segments.length >= 2) {
9
+ return segments.slice(-2).join('/')
10
+ }
11
+ return segments[0] ?? value.trim()
12
+ }
13
+
14
+ export const isGitBranchCheckedOutInOtherWorktree = (
15
+ branch: GitBranchSummary,
16
+ currentWorktreePath: string
17
+ ) => {
18
+ if (branch.kind !== 'local' || branch.worktreePath == null || branch.worktreePath.trim() === '') {
19
+ return false
20
+ }
21
+
22
+ return normalizeWorktreePath(branch.worktreePath) !== normalizeWorktreePath(currentWorktreePath)
23
+ }
24
+
25
+ export const getGitBranchCheckoutBlockedPath = (
26
+ branch: GitBranchSummary,
27
+ branches: GitBranchSummary[],
28
+ currentWorktreePath: string
29
+ ) => {
30
+ if (branch.kind === 'local') {
31
+ return isGitBranchCheckedOutInOtherWorktree(branch, currentWorktreePath)
32
+ ? branch.worktreePath?.trim() ?? null
33
+ : null
34
+ }
35
+
36
+ const localPeer = branches.find(item => item.kind === 'local' && item.localName === branch.localName)
37
+ if (localPeer == null) {
38
+ return null
39
+ }
40
+
41
+ return isGitBranchCheckedOutInOtherWorktree(localPeer, currentWorktreePath)
42
+ ? localPeer.worktreePath?.trim() ?? null
43
+ : null
44
+ }
45
+
46
+ export const getGitBranchViewState = (
47
+ visibleBranches: GitBranchSummary[],
48
+ allBranches: GitBranchSummary[],
49
+ currentWorktreePath: string
50
+ ) => {
51
+ const localBranches = visibleBranches.filter(branch => branch.kind === 'local')
52
+ const isSwitchableBranch = (branch: GitBranchSummary) =>
53
+ getGitBranchCheckoutBlockedPath(branch, allBranches, currentWorktreePath) == null
54
+ const availableLocalBranches = localBranches.filter(isSwitchableBranch)
55
+ const remoteBranches = visibleBranches.filter(branch => branch.kind === 'remote' && isSwitchableBranch(branch))
56
+
57
+ return {
58
+ availableLocalBranches,
59
+ hasResults: availableLocalBranches.length > 0 || remoteBranches.length > 0,
60
+ remoteBranches
61
+ }
62
+ }
63
+
64
+ const getBranchSearchTokens = (branch: GitBranchSummary) => {
65
+ const tokens = [branch.name, branch.localName]
66
+ if (branch.remoteName != null && branch.remoteName !== '') {
67
+ tokens.push(branch.remoteName, `${branch.remoteName}/${branch.localName}`)
68
+ }
69
+ return tokens.map(token => normalizeBranchText(token))
70
+ }
71
+
72
+ export const filterGitBranches = (branches: GitBranchSummary[], query: string) => {
73
+ const normalizedQuery = normalizeBranchText(query)
74
+ if (normalizedQuery === '') {
75
+ return branches
76
+ }
77
+
78
+ return branches.filter(branch => getBranchSearchTokens(branch).some(token => token.includes(normalizedQuery)))
79
+ }
80
+
81
+ export const hasExactGitBranchMatch = (branches: GitBranchSummary[], query: string) => {
82
+ const normalizedQuery = normalizeBranchText(query)
83
+ if (normalizedQuery === '') {
84
+ return false
85
+ }
86
+
87
+ return branches.some(branch => getBranchSearchTokens(branch).includes(normalizedQuery))
88
+ }
@@ -0,0 +1,79 @@
1
+ import type { GitChangeSummary, GitRepositoryState } from '@vibe-forge/types'
2
+
3
+ import { isGitOperationDisabled } from './git-operation-utils'
4
+
5
+ export type GitCommitNextStep = 'commit' | 'commit-and-push'
6
+ export type GitCommitBlockedReason =
7
+ | 'amend-unavailable'
8
+ | 'message-required'
9
+ | 'no-changes'
10
+ | 'no-staged-changes'
11
+ | null
12
+
13
+ const EMPTY_GIT_CHANGE_SUMMARY: GitChangeSummary = {
14
+ changedFiles: 0,
15
+ additions: 0,
16
+ deletions: 0
17
+ }
18
+
19
+ export const getGitCommitSummary = (
20
+ repoState: GitRepositoryState,
21
+ includeUnstagedChanges: boolean
22
+ ): GitChangeSummary => {
23
+ return includeUnstagedChanges
24
+ ? repoState.workingTreeSummary ?? EMPTY_GIT_CHANGE_SUMMARY
25
+ : repoState.stagedSummary ?? EMPTY_GIT_CHANGE_SUMMARY
26
+ }
27
+
28
+ export const getGitCommitBlockedReason = (
29
+ repoState: GitRepositoryState,
30
+ options: {
31
+ includeUnstagedChanges: boolean
32
+ amend: boolean
33
+ commitMessage: string
34
+ }
35
+ ): GitCommitBlockedReason => {
36
+ const hasCommitChanges = options.includeUnstagedChanges
37
+ ? repoState.hasChanges === true
38
+ : repoState.hasStagedChanges === true
39
+ const hasMessage = options.commitMessage.trim() !== ''
40
+
41
+ if (options.amend && repoState.headCommit == null) {
42
+ return 'amend-unavailable'
43
+ }
44
+
45
+ if (options.amend) {
46
+ return hasCommitChanges || hasMessage
47
+ ? null
48
+ : options.includeUnstagedChanges
49
+ ? 'no-changes'
50
+ : 'no-staged-changes'
51
+ }
52
+
53
+ if (!hasCommitChanges) {
54
+ return options.includeUnstagedChanges
55
+ ? 'no-changes'
56
+ : 'no-staged-changes'
57
+ }
58
+
59
+ return hasMessage ? null : 'message-required'
60
+ }
61
+
62
+ export const canSubmitGitCommit = (
63
+ repoState: GitRepositoryState,
64
+ options: {
65
+ includeUnstagedChanges: boolean
66
+ amend: boolean
67
+ commitMessage: string
68
+ }
69
+ ) => {
70
+ return getGitCommitBlockedReason(repoState, options) == null
71
+ }
72
+
73
+ export const canGitCommitAndPush = (repoState: GitRepositoryState) => {
74
+ return !isGitOperationDisabled(repoState, 'push')
75
+ }
76
+
77
+ export const isGitCommitMessageRequired = (amend: boolean) => {
78
+ return !amend
79
+ }
@@ -0,0 +1,69 @@
1
+ import type { GitRepositoryState } from '@vibe-forge/types'
2
+
3
+ import { pushSessionGitBranch } from '#~/api'
4
+
5
+ export const runGitControlMutation = async <TAction extends string>(options: {
6
+ action: TAction
7
+ notifyError: (error: unknown) => void
8
+ notifySuccess: (message: string) => void
9
+ onSuccess?: () => void
10
+ refreshGitState: (nextRepo?: GitRepositoryState) => Promise<void>
11
+ setPendingAction: (action: TAction | null) => void
12
+ successMessage: string
13
+ task: () => Promise<{ repo: GitRepositoryState }>
14
+ }) => {
15
+ const {
16
+ action,
17
+ notifyError,
18
+ notifySuccess,
19
+ onSuccess,
20
+ refreshGitState,
21
+ setPendingAction,
22
+ successMessage,
23
+ task
24
+ } = options
25
+
26
+ setPendingAction(action)
27
+ try {
28
+ const result = await task()
29
+ await refreshGitState(result.repo)
30
+ onSuccess?.()
31
+ notifySuccess(successMessage)
32
+ } catch (error) {
33
+ notifyError(error)
34
+ } finally {
35
+ setPendingAction(null)
36
+ }
37
+ }
38
+
39
+ export const runSessionGitPush = async (options: {
40
+ blockedMessage: string
41
+ blockedReason: string | null
42
+ force: boolean
43
+ notifyBlocked: (message: string) => void
44
+ onSuccess?: () => void
45
+ repoState: GitRepositoryState | undefined
46
+ runMutation: (
47
+ action: 'push',
48
+ task: () => Promise<{ repo: GitRepositoryState }>,
49
+ successMessage: string,
50
+ onSuccess?: () => void
51
+ ) => Promise<void>
52
+ sessionId: string
53
+ successMessage: string
54
+ }) => {
55
+ if (options.repoState?.available !== true) {
56
+ return
57
+ }
58
+ if (options.blockedReason != null) {
59
+ options.notifyBlocked(options.blockedMessage)
60
+ return
61
+ }
62
+
63
+ await options.runMutation(
64
+ 'push',
65
+ () => pushSessionGitBranch(options.sessionId, { force: options.force }),
66
+ options.successMessage,
67
+ options.onSuccess
68
+ )
69
+ }
@@ -0,0 +1,98 @@
1
+ import type { GitRepositoryState } from '@vibe-forge/types'
2
+
3
+ export type GitOperationKind = 'commit' | 'push' | 'sync'
4
+ export type GitPushBlockedReason = 'behind-upstream' | 'push-unavailable' | null
5
+
6
+ const hasUpstream = (repoState: GitRepositoryState) => repoState.upstream != null && repoState.upstream.trim() !== ''
7
+
8
+ const hasRemote = (repoState: GitRepositoryState) => hasUpstream(repoState) || (repoState.remotes?.length ?? 0) > 0
9
+
10
+ const hasBranch = (repoState: GitRepositoryState) =>
11
+ repoState.currentBranch != null && repoState.currentBranch.trim() !== ''
12
+
13
+ export const isGitOperationDisabled = (
14
+ repoState: GitRepositoryState,
15
+ kind: GitOperationKind
16
+ ) => {
17
+ switch (kind) {
18
+ case 'commit':
19
+ return repoState.hasChanges !== true
20
+ case 'push':
21
+ case 'sync':
22
+ return !hasBranch(repoState) || !hasRemote(repoState)
23
+ }
24
+ }
25
+
26
+ export const getGitPushBlockedReason = (
27
+ repoState: GitRepositoryState,
28
+ forcePush: boolean
29
+ ): GitPushBlockedReason => {
30
+ if (isGitOperationDisabled(repoState, 'push')) {
31
+ return 'push-unavailable'
32
+ }
33
+
34
+ if ((repoState.behind ?? 0) > 0 && !forcePush) {
35
+ return 'behind-upstream'
36
+ }
37
+
38
+ return null
39
+ }
40
+
41
+ export const getGitControlState = (
42
+ repoState: GitRepositoryState | undefined,
43
+ forcePush: boolean,
44
+ labels: {
45
+ detachedHead: string
46
+ pushNeedsSyncOrForce: string
47
+ pushUnavailable: string
48
+ }
49
+ ) => {
50
+ const currentBranchName = repoState?.available === true
51
+ ? repoState.currentBranch?.trim() ?? ''
52
+ : ''
53
+ const currentBranchLabel = currentBranchName !== ''
54
+ ? currentBranchName
55
+ : labels.detachedHead
56
+ const pushBlockedReason = repoState?.available === true
57
+ ? getGitPushBlockedReason(repoState, forcePush)
58
+ : 'push-unavailable'
59
+
60
+ return {
61
+ currentBranchLabel,
62
+ pushBlockedReason,
63
+ pushBlockedMessage: pushBlockedReason === 'behind-upstream'
64
+ ? labels.pushNeedsSyncOrForce
65
+ : pushBlockedReason == null
66
+ ? ''
67
+ : labels.pushUnavailable
68
+ }
69
+ }
70
+
71
+ export const getPrimaryGitOperationKind = (
72
+ repoState: GitRepositoryState
73
+ ): GitOperationKind | null => {
74
+ if (repoState.hasChanges === true) {
75
+ return 'commit'
76
+ }
77
+
78
+ if ((repoState.behind ?? 0) > 0 && !isGitOperationDisabled(repoState, 'sync')) {
79
+ return 'sync'
80
+ }
81
+
82
+ if (
83
+ ((repoState.ahead ?? 0) > 0 || !hasUpstream(repoState)) &&
84
+ !isGitOperationDisabled(repoState, 'push')
85
+ ) {
86
+ return 'push'
87
+ }
88
+
89
+ if (!isGitOperationDisabled(repoState, 'sync')) {
90
+ return 'sync'
91
+ }
92
+
93
+ if (!isGitOperationDisabled(repoState, 'push')) {
94
+ return 'push'
95
+ }
96
+
97
+ return null
98
+ }
@@ -0,0 +1,49 @@
1
+ import type { GitWorktreeSummary } from '@vibe-forge/types'
2
+
3
+ const sortGitWorktrees = (worktrees: GitWorktreeSummary[]) => {
4
+ return [...worktrees].sort((left, right) => {
5
+ if (left.isCurrent !== right.isCurrent) {
6
+ return left.isCurrent ? -1 : 1
7
+ }
8
+
9
+ return left.path.localeCompare(right.path)
10
+ })
11
+ }
12
+
13
+ const getCurrentWorktreeFallback = (repositoryRoot?: string, currentBranch?: string | null): GitWorktreeSummary[] => {
14
+ const path = repositoryRoot?.trim() ?? ''
15
+ if (path === '') {
16
+ return []
17
+ }
18
+
19
+ const branchName = currentBranch?.trim() || null
20
+ return [{
21
+ path,
22
+ branchName,
23
+ isCurrent: true,
24
+ isDetached: branchName == null
25
+ }]
26
+ }
27
+
28
+ export const getGitWorktreeViewState = (input: {
29
+ currentBranch?: string | null
30
+ enabled: boolean
31
+ repositoryRoot?: string
32
+ worktrees?: GitWorktreeSummary[]
33
+ }) => {
34
+ if (!input.enabled) {
35
+ return {
36
+ showWorktreeButton: false,
37
+ worktrees: []
38
+ }
39
+ }
40
+
41
+ const worktrees = input.worktrees != null && input.worktrees.length > 0
42
+ ? sortGitWorktrees(input.worktrees)
43
+ : getCurrentWorktreeFallback(input.repositoryRoot, input.currentBranch)
44
+
45
+ return {
46
+ showWorktreeButton: true,
47
+ worktrees
48
+ }
49
+ }