@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.
- package/cli.cjs +6 -1
- package/dist/assets/{arc-M4HYfcHs.js → arc-De_WjPJ3.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-CUrDjrxj.js → blockDiagram-c4efeb88-C4aR2zTE.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-BMEtqlFp.js → c4Diagram-c83219d4-BZH3rq_m.js} +1 -1
- package/dist/assets/channel-BvERb8WU.js +1 -0
- package/dist/assets/{classDiagram-beda092f-BOmDJ0Ml.js → classDiagram-beda092f-BzJgBrIK.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-BODzX2MB.js → classDiagram-v2-2358418a-5ZtXcnT3.js} +1 -1
- package/dist/assets/clone-B9_0v-6Y.js +1 -0
- package/dist/assets/{createText-1719965b-B9Dd8zcR.js → createText-1719965b-DUVvEtmR.js} +1 -1
- package/dist/assets/{cssMode-DLxG92Ot.js → cssMode-GoTNjuXX.js} +1 -1
- package/dist/assets/{edges-96097737-CuZFd43m.js → edges-96097737-Dd7m4Cvs.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-8g9lu2-Z.js → erDiagram-0228fc6a-DxqFlG_f.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-BlBS1tdN.js → flowDb-c6c81e3f-DU0C5kCI.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-u6mWflpF.js → flowDiagram-50d868cf-Di1uDa_X.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-LpS8Kb00.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-BDqI2NFr.js → flowchart-elk-definition-6af322e1-CwG8aty5.js} +1 -1
- package/dist/assets/{freemarker2-tVtpTMPu.js → freemarker2-j39cqTlI.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-CDQjx9Wu.js → ganttDiagram-a2739b55-baO_lzL-.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-DUHFKRVA.js → gitGraphDiagram-82fe8481-COoHjYMf.js} +1 -1
- package/dist/assets/{graph-2HKPi5B_.js → graph-KxESr4M5.js} +1 -1
- package/dist/assets/{handlebars-D00tgNd8.js → handlebars-BgjdZO8G.js} +1 -1
- package/dist/assets/{html-B-TDzBiR.js → html-Ba7tYObe.js} +1 -1
- package/dist/assets/{htmlMode-ClycqSTM.js → htmlMode-Bztvbig1.js} +1 -1
- package/dist/assets/{index-5325376f-DPrJpRQ-.js → index-5325376f-BMTAx2mL.js} +1 -1
- package/dist/assets/index-C1oh0w9H.css +32 -0
- package/dist/assets/{index-CAHZZEoo.js → index-Pm_kLJvG.js} +330 -326
- package/dist/assets/{infoDiagram-8eee0895-Co5tS1I5.js → infoDiagram-8eee0895-CC74qbHY.js} +1 -1
- package/dist/assets/{javascript-zbkwarmb.js → javascript-C1e1cllX.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-k_qioHgy.js → journeyDiagram-c64418c1-C4MyOdE6.js} +1 -1
- package/dist/assets/{jsonMode-C3CSpzBF.js → jsonMode-BC98AlvF.js} +1 -1
- package/dist/assets/{layout-CjOXKxvs.js → layout-CxAyTlr7.js} +1 -1
- package/dist/assets/{line-C-XnQrKR.js → line-DhaUfI71.js} +1 -1
- package/dist/assets/{linear-C7MMERzS.js → linear-MYukzldK.js} +1 -1
- package/dist/assets/{liquid-5G37EU6K.js → liquid-DahfJEYl.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-zaDMuhCE.js → lspLanguageFeatures-BWDJcswW.js} +1 -1
- package/dist/assets/{mdx-Bc-LY0gi.js → mdx-BELlF_FD.js} +1 -1
- package/dist/assets/{mermaid.core-CechbHof.js → mermaid.core-BrQnSGSY.js} +4 -4
- package/dist/assets/{mindmap-definition-8da855dc-ejftCDGb.js → mindmap-definition-8da855dc-B0FoxTiy.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-DY__X3Qj.js → pieDiagram-a8764435-Ddr2cjSL.js} +1 -1
- package/dist/assets/{python-vK2Ff2J5.js → python--C9if_AD.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-azIZCv_2.js → quadrantDiagram-1e28029f-BlEs7Mrl.js} +1 -1
- package/dist/assets/{razor-BipjBJKu.js → razor-B9U9JxKn.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-C4EB0Xs2.js → requirementDiagram-08caed73-kEFOAu2v.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-PNhR6YWu.js → sankeyDiagram-a04cb91d-BBghez8I.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-4c-qV-Ri.js → sequenceDiagram-c5b8d532-CJqgzdUE.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-CnURumPE.js → stateDiagram-1ecb1508-BER4XEI6.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-DR2qHTPg.js → stateDiagram-v2-c2b004d7-EBV2vSks.js} +1 -1
- package/dist/assets/{styles-b4e223ce-B2PWXT_i.js → styles-b4e223ce-k0eswZsE.js} +1 -1
- package/dist/assets/{styles-ca3715f6-DEhgVF5H.js → styles-ca3715f6-Ckr7GA-0.js} +1 -1
- package/dist/assets/{styles-d45a18b0-DyzccA5F.js → styles-d45a18b0-C1bpSwV3.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-C_1tMhxp.js → svgDrawCommon-b86b1483-CDtKpGvy.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-FdaC0dQH.js → timeline-definition-faaaa080-BeGR-vua.js} +1 -1
- package/dist/assets/{tsMode-CrMC5T3_.js → tsMode-D_gJXIy3.js} +1 -1
- package/dist/assets/{typescript-CRfPu8v7.js → typescript-BoKcNXkN.js} +1 -1
- package/dist/assets/{xml-jlRvQfFI.js → xml-DZvURlJ-.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-sxjv75h9.js → xychartDiagram-f5964ef8-DxfeLuYV.js} +1 -1
- package/dist/assets/{yaml-B47_IHOH.js → yaml-CTC8PAGY.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +9 -9
- package/src/api/git.ts +78 -0
- package/src/api.ts +24 -0
- package/src/components/chat/ChatHeader.tsx +4 -0
- package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +157 -0
- package/src/components/chat/git-controls/ChatGitControls.scss +616 -0
- package/src/components/chat/git-controls/ChatGitControls.tsx +151 -0
- package/src/components/chat/git-controls/GitCommitModal.tsx +199 -0
- package/src/components/chat/git-controls/GitCommitModalParts.tsx +151 -0
- package/src/components/chat/git-controls/GitOperationsDropdown.tsx +123 -0
- package/src/components/chat/git-controls/GitPushModal.tsx +106 -0
- package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +68 -0
- package/src/components/chat/git-controls/git-branch-utils.ts +88 -0
- package/src/components/chat/git-controls/git-commit-utils.ts +79 -0
- package/src/components/chat/git-controls/git-mutation-utils.ts +69 -0
- package/src/components/chat/git-controls/git-operation-utils.ts +98 -0
- package/src/components/chat/git-controls/git-worktree-utils.ts +49 -0
- package/src/components/chat/git-controls/use-chat-git-commit.ts +185 -0
- package/src/components/chat/git-controls/use-chat-git-controls.ts +200 -0
- package/src/components/chat/git-controls/use-chat-git-push-state.ts +19 -0
- package/src/components/chat/git-controls/use-chat-git-worktrees.ts +39 -0
- package/src/components/chat/tools/DefaultTool.tsx +40 -31
- package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +1 -15
- package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +8 -1
- package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +10 -95
- package/src/components/chat/tools/core/ToolCallBox.scss +18 -0
- package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
- package/src/components/chat/tools/core/tool-display.ts +12 -1
- package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
- package/src/components/chat/tools/core/tool-summary.ts +18 -6
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +0 -7
- package/src/hooks/chat/session-view-cache.ts +80 -0
- package/src/hooks/chat/use-chat-session-messages.ts +124 -30
- package/src/resources/locales/en.json +72 -4
- package/src/resources/locales/zh.json +72 -4
- package/dist/assets/channel-Cj3Cp2OJ.js +0 -1
- package/dist/assets/clone-B7Q9B1dS.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-G3v545eF.js +0 -1
- 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
|
+
}
|