dmux 2.2.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +412 -179
- package/dist/DmuxApp.js.map +1 -1
- package/dist/MergePane.d.ts.map +1 -1
- package/dist/MergePane.js +4 -15
- package/dist/MergePane.js.map +1 -1
- package/dist/PaneAnalyzer.d.ts +45 -0
- package/dist/PaneAnalyzer.d.ts.map +1 -0
- package/dist/PaneAnalyzer.js +278 -0
- package/dist/PaneAnalyzer.js.map +1 -0
- package/dist/_plugin-vue_export-helper.css +1 -0
- package/dist/actions/index.d.ts +19 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +54 -0
- package/dist/actions/index.js.map +1 -0
- package/dist/actions/paneActions.d.ts +45 -0
- package/dist/actions/paneActions.d.ts.map +1 -0
- package/dist/actions/paneActions.js +932 -0
- package/dist/actions/paneActions.js.map +1 -0
- package/dist/actions/types.d.ts +101 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/actions/types.js +129 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/adapters/apiActionHandler.d.ts +64 -0
- package/dist/adapters/apiActionHandler.d.ts.map +1 -0
- package/dist/adapters/apiActionHandler.js +170 -0
- package/dist/adapters/apiActionHandler.js.map +1 -0
- package/dist/adapters/tuiActionHandler.d.ts +57 -0
- package/dist/adapters/tuiActionHandler.d.ts.map +1 -0
- package/dist/adapters/tuiActionHandler.js +152 -0
- package/dist/adapters/tuiActionHandler.js.map +1 -0
- package/dist/chunks/_plugin-vue_export-helper-Cvoq67hi.js +28 -0
- package/dist/components/ActionChoiceDialog.d.ts +16 -0
- package/dist/components/ActionChoiceDialog.d.ts.map +1 -0
- package/dist/components/ActionChoiceDialog.js +29 -0
- package/dist/components/ActionChoiceDialog.js.map +1 -0
- package/dist/components/ActionConfirmDialog.d.ts +16 -0
- package/dist/components/ActionConfirmDialog.d.ts.map +1 -0
- package/dist/components/ActionConfirmDialog.js +31 -0
- package/dist/components/ActionConfirmDialog.js.map +1 -0
- package/dist/components/ActionInputDialog.d.ts +16 -0
- package/dist/components/ActionInputDialog.d.ts.map +1 -0
- package/dist/components/ActionInputDialog.js +49 -0
- package/dist/components/ActionInputDialog.js.map +1 -0
- package/dist/components/ActionProgressDialog.d.ts +13 -0
- package/dist/components/ActionProgressDialog.d.ts.map +1 -0
- package/dist/components/ActionProgressDialog.js +20 -0
- package/dist/components/ActionProgressDialog.js.map +1 -0
- package/dist/components/FooterHelp.d.ts +2 -0
- package/dist/components/FooterHelp.d.ts.map +1 -1
- package/dist/components/FooterHelp.js +9 -2
- package/dist/components/FooterHelp.js.map +1 -1
- package/dist/components/KebabMenu.d.ts +10 -0
- package/dist/components/KebabMenu.d.ts.map +1 -0
- package/dist/components/KebabMenu.js +18 -0
- package/dist/components/KebabMenu.js.map +1 -0
- package/dist/components/LoadingIndicator.d.ts.map +1 -1
- package/dist/components/LoadingIndicator.js +5 -5
- package/dist/components/LoadingIndicator.js.map +1 -1
- package/dist/components/PaneCard.d.ts +1 -0
- package/dist/components/PaneCard.d.ts.map +1 -1
- package/dist/components/PaneCard.js +21 -20
- package/dist/components/PaneCard.js.map +1 -1
- package/dist/components/PanesGrid.d.ts +1 -0
- package/dist/components/PanesGrid.d.ts.map +1 -1
- package/dist/components/PanesGrid.js +5 -4
- package/dist/components/PanesGrid.js.map +1 -1
- package/dist/components/QRCode.d.ts +7 -0
- package/dist/components/QRCode.d.ts.map +1 -0
- package/dist/components/QRCode.js +19 -0
- package/dist/components/QRCode.js.map +1 -0
- package/dist/components/Spinner.d.ts +10 -0
- package/dist/components/Spinner.d.ts.map +1 -0
- package/dist/components/Spinner.js +15 -0
- package/dist/components/Spinner.js.map +1 -0
- package/dist/dashboard.html +14 -0
- package/dist/dashboard.js +2 -0
- package/dist/hooks/useActionSystem.d.ts +31 -0
- package/dist/hooks/useActionSystem.d.ts.map +1 -0
- package/dist/hooks/useActionSystem.js +95 -0
- package/dist/hooks/useActionSystem.js.map +1 -0
- package/dist/hooks/useAgentStatus.d.ts +4 -3
- package/dist/hooks/useAgentStatus.d.ts.map +1 -1
- package/dist/hooks/useAgentStatus.js +45 -194
- package/dist/hooks/useAgentStatus.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +2 -1
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +46 -100
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +5 -3
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
- package/dist/hooks/useTerminalWidth.js +18 -1
- package/dist/hooks/useTerminalWidth.js.map +1 -1
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
- package/dist/hooks/useWorktreeActions.js +4 -0
- package/dist/hooks/useWorktreeActions.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/server/actionsApi.d.ts +37 -0
- package/dist/server/actionsApi.d.ts.map +1 -0
- package/dist/server/actionsApi.js +256 -0
- package/dist/server/actionsApi.js.map +1 -0
- package/dist/server/embedded-assets.d.ts +13 -0
- package/dist/server/embedded-assets.d.ts.map +1 -0
- package/dist/server/embedded-assets.js +5012 -0
- package/dist/server/embedded-assets.js.map +1 -0
- package/dist/server/index.d.ts +21 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +99 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/routes.d.ts +3 -0
- package/dist/server/routes.d.ts.map +1 -0
- package/dist/server/routes.js +672 -0
- package/dist/server/routes.js.map +1 -0
- package/dist/server/static.d.ts +6 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +3040 -0
- package/dist/server/static.js.map +1 -0
- package/dist/services/ConfigWatcher.d.ts +20 -0
- package/dist/services/ConfigWatcher.d.ts.map +1 -0
- package/dist/services/ConfigWatcher.js +75 -0
- package/dist/services/ConfigWatcher.js.map +1 -0
- package/dist/services/PaneWorkerManager.d.ts +69 -0
- package/dist/services/PaneWorkerManager.d.ts.map +1 -0
- package/dist/services/PaneWorkerManager.js +272 -0
- package/dist/services/PaneWorkerManager.js.map +1 -0
- package/dist/services/StatusDetector.d.ts +87 -0
- package/dist/services/StatusDetector.d.ts.map +1 -0
- package/dist/services/StatusDetector.js +387 -0
- package/dist/services/StatusDetector.js.map +1 -0
- package/dist/services/TerminalDiffer.d.ts +85 -0
- package/dist/services/TerminalDiffer.d.ts.map +1 -0
- package/dist/services/TerminalDiffer.js +499 -0
- package/dist/services/TerminalDiffer.js.map +1 -0
- package/dist/services/TerminalStreamer.d.ts +80 -0
- package/dist/services/TerminalStreamer.d.ts.map +1 -0
- package/dist/services/TerminalStreamer.js +490 -0
- package/dist/services/TerminalStreamer.js.map +1 -0
- package/dist/services/TunnelService.d.ts +9 -0
- package/dist/services/TunnelService.d.ts.map +1 -0
- package/dist/services/TunnelService.js +34 -0
- package/dist/services/TunnelService.js.map +1 -0
- package/dist/services/WorkerMessageBus.d.ts +48 -0
- package/dist/services/WorkerMessageBus.d.ts.map +1 -0
- package/dist/services/WorkerMessageBus.js +120 -0
- package/dist/services/WorkerMessageBus.js.map +1 -0
- package/dist/shared/StateManager.d.ts +34 -0
- package/dist/shared/StateManager.d.ts.map +1 -0
- package/dist/shared/StateManager.js +108 -0
- package/dist/shared/StateManager.js.map +1 -0
- package/dist/shared/StreamProtocol.d.ts +75 -0
- package/dist/shared/StreamProtocol.d.ts.map +1 -0
- package/dist/shared/StreamProtocol.js +37 -0
- package/dist/shared/StreamProtocol.js.map +1 -0
- package/dist/terminal.html +17 -0
- package/dist/terminal.js +3 -0
- package/dist/types.d.ts +21 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agentDetection.d.ts +18 -0
- package/dist/utils/agentDetection.d.ts.map +1 -0
- package/dist/utils/agentDetection.js +73 -0
- package/dist/utils/agentDetection.js.map +1 -0
- package/dist/utils/aiMerge.d.ts +35 -0
- package/dist/utils/aiMerge.d.ts.map +1 -0
- package/dist/utils/aiMerge.js +298 -0
- package/dist/utils/aiMerge.js.map +1 -0
- package/dist/utils/conflictResolutionPane.d.ts +19 -0
- package/dist/utils/conflictResolutionPane.d.ts.map +1 -0
- package/dist/utils/conflictResolutionPane.js +214 -0
- package/dist/utils/conflictResolutionPane.js.map +1 -0
- package/dist/utils/mergeExecution.d.ts +52 -0
- package/dist/utils/mergeExecution.d.ts.map +1 -0
- package/dist/utils/mergeExecution.js +192 -0
- package/dist/utils/mergeExecution.js.map +1 -0
- package/dist/utils/mergeValidation.d.ts +67 -0
- package/dist/utils/mergeValidation.d.ts.map +1 -0
- package/dist/utils/mergeValidation.js +213 -0
- package/dist/utils/mergeValidation.js.map +1 -0
- package/dist/utils/paneCreation.d.ts +17 -0
- package/dist/utils/paneCreation.d.ts.map +1 -0
- package/dist/utils/paneCreation.js +274 -0
- package/dist/utils/paneCreation.js.map +1 -0
- package/dist/utils/port.d.ts +5 -0
- package/dist/utils/port.d.ts.map +1 -0
- package/dist/utils/port.js +54 -0
- package/dist/utils/port.js.map +1 -0
- package/dist/workers/PaneWorker.d.ts +2 -0
- package/dist/workers/PaneWorker.d.ts.map +1 -0
- package/dist/workers/PaneWorker.js +362 -0
- package/dist/workers/PaneWorker.js.map +1 -0
- package/dist/workers/WorkerMessages.d.ts +36 -0
- package/dist/workers/WorkerMessages.d.ts.map +1 -0
- package/dist/workers/WorkerMessages.js +9 -0
- package/dist/workers/WorkerMessages.js.map +1 -0
- package/package.json +19 -5
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standardized Pane Actions
|
|
3
|
+
*
|
|
4
|
+
* Core action implementations that work across all interfaces.
|
|
5
|
+
* Each action returns a standardized ActionResult that interfaces can handle.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Generate commit message with timeout and error handling
|
|
10
|
+
* Returns null if it fails, so caller can fall back to manual input
|
|
11
|
+
*/
|
|
12
|
+
async function generateCommitMessageSafe(repoPath, timeoutMs = 15000) {
|
|
13
|
+
try {
|
|
14
|
+
const { generateCommitMessage } = await import('../utils/aiMerge.js');
|
|
15
|
+
// Race between generation and timeout
|
|
16
|
+
const result = await Promise.race([
|
|
17
|
+
generateCommitMessage(repoPath),
|
|
18
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeoutMs)),
|
|
19
|
+
]);
|
|
20
|
+
if (!result) {
|
|
21
|
+
console.error('[generateCommitMessageSafe] AI generation returned null');
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error('[generateCommitMessageSafe] Error or timeout:', error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* View/Jump to a pane
|
|
32
|
+
*/
|
|
33
|
+
export async function viewPane(pane, context) {
|
|
34
|
+
try {
|
|
35
|
+
execSync(`tmux select-pane -t '${pane.paneId}'`, { stdio: 'pipe' });
|
|
36
|
+
return {
|
|
37
|
+
type: 'navigation',
|
|
38
|
+
message: `Jumped to pane: ${pane.slug}`,
|
|
39
|
+
targetPaneId: pane.id,
|
|
40
|
+
dismissable: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'error',
|
|
46
|
+
message: 'Failed to jump to pane - it may have been closed',
|
|
47
|
+
dismissable: true,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Close a pane - presents options for how to close
|
|
53
|
+
*/
|
|
54
|
+
export async function closePane(pane, context) {
|
|
55
|
+
const options = [
|
|
56
|
+
{
|
|
57
|
+
id: 'kill_only',
|
|
58
|
+
label: 'Just close pane',
|
|
59
|
+
description: 'Keep worktree and branch',
|
|
60
|
+
default: true,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
if (pane.worktreePath) {
|
|
64
|
+
options.push({
|
|
65
|
+
id: 'kill_and_clean',
|
|
66
|
+
label: 'Close and remove worktree',
|
|
67
|
+
description: 'Delete worktree but keep branch',
|
|
68
|
+
danger: true,
|
|
69
|
+
}, {
|
|
70
|
+
id: 'kill_clean_branch',
|
|
71
|
+
label: 'Close and delete everything',
|
|
72
|
+
description: 'Remove worktree and delete branch',
|
|
73
|
+
danger: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
type: 'choice',
|
|
78
|
+
title: 'Close Pane',
|
|
79
|
+
message: `How do you want to close "${pane.slug}"?`,
|
|
80
|
+
options,
|
|
81
|
+
onSelect: async (optionId) => {
|
|
82
|
+
return executeCloseOption(pane, context, optionId);
|
|
83
|
+
},
|
|
84
|
+
dismissable: true,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Execute the selected close option
|
|
89
|
+
*/
|
|
90
|
+
async function executeCloseOption(pane, context, option) {
|
|
91
|
+
try {
|
|
92
|
+
// Kill the tmux pane
|
|
93
|
+
try {
|
|
94
|
+
execSync(`tmux kill-pane -t '${pane.paneId}'`, { stdio: 'pipe' });
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Pane might already be dead
|
|
98
|
+
}
|
|
99
|
+
// Handle worktree cleanup based on option
|
|
100
|
+
if (pane.worktreePath && (option === 'kill_and_clean' || option === 'kill_clean_branch')) {
|
|
101
|
+
const mainRepoPath = pane.worktreePath.replace(/\/\.dmux\/worktrees\/[^/]+$/, '');
|
|
102
|
+
try {
|
|
103
|
+
execSync(`git worktree remove "${pane.worktreePath}" --force`, {
|
|
104
|
+
stdio: 'pipe',
|
|
105
|
+
cwd: mainRepoPath,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Worktree might already be removed
|
|
110
|
+
}
|
|
111
|
+
// Delete branch if requested
|
|
112
|
+
if (option === 'kill_clean_branch') {
|
|
113
|
+
try {
|
|
114
|
+
execSync(`git branch -D ${pane.slug}`, {
|
|
115
|
+
stdio: 'pipe',
|
|
116
|
+
cwd: mainRepoPath,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Branch might not exist or already deleted
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Remove from panes list
|
|
125
|
+
const updatedPanes = context.panes.filter(p => p.id !== pane.id);
|
|
126
|
+
await context.savePanes(updatedPanes);
|
|
127
|
+
if (context.onPaneRemove) {
|
|
128
|
+
context.onPaneRemove(pane.id);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
type: 'success',
|
|
132
|
+
message: `Pane "${pane.slug}" closed successfully`,
|
|
133
|
+
dismissable: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
return {
|
|
138
|
+
type: 'error',
|
|
139
|
+
message: `Failed to close pane: ${error}`,
|
|
140
|
+
dismissable: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Merge a worktree into the main branch with comprehensive pre-checks
|
|
146
|
+
*/
|
|
147
|
+
export async function mergePane(pane, context, params) {
|
|
148
|
+
if (!pane.worktreePath) {
|
|
149
|
+
return {
|
|
150
|
+
type: 'error',
|
|
151
|
+
message: 'This pane has no worktree to merge',
|
|
152
|
+
dismissable: true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// Import merge utilities dynamically
|
|
156
|
+
const { validateMerge } = await import('../utils/mergeValidation.js');
|
|
157
|
+
const mainRepoPath = pane.worktreePath.replace(/\/\.dmux\/worktrees\/[^/]+$/, '');
|
|
158
|
+
// Run pre-merge validation
|
|
159
|
+
const validation = validateMerge(mainRepoPath, pane.worktreePath, pane.slug);
|
|
160
|
+
// If there are issues, present them to the user
|
|
161
|
+
if (!validation.canMerge) {
|
|
162
|
+
return handleMergeIssues(pane, context, validation, mainRepoPath);
|
|
163
|
+
}
|
|
164
|
+
// No issues detected, proceed with merge confirmation
|
|
165
|
+
return {
|
|
166
|
+
type: 'confirm',
|
|
167
|
+
title: 'Merge Worktree',
|
|
168
|
+
message: `Merge "${pane.slug}" into ${validation.mainBranch}?`,
|
|
169
|
+
confirmLabel: 'Merge',
|
|
170
|
+
cancelLabel: 'Cancel',
|
|
171
|
+
onConfirm: async () => {
|
|
172
|
+
return executeMerge(pane, context, validation.mainBranch, mainRepoPath);
|
|
173
|
+
},
|
|
174
|
+
onCancel: async () => {
|
|
175
|
+
return {
|
|
176
|
+
type: 'info',
|
|
177
|
+
message: 'Merge cancelled',
|
|
178
|
+
dismissable: true,
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Handle detected merge issues
|
|
185
|
+
*/
|
|
186
|
+
async function handleMergeIssues(pane, context, validation, mainRepoPath) {
|
|
187
|
+
const { issues, mainBranch } = validation;
|
|
188
|
+
const { commitChanges, stashChanges } = await import('../utils/mergeValidation.js');
|
|
189
|
+
const { generateCommitMessage } = await import('../utils/aiMerge.js');
|
|
190
|
+
// Group issues by type for clearer handling
|
|
191
|
+
const mainDirty = issues.find((i) => i.type === 'main_dirty');
|
|
192
|
+
const worktreeUncommitted = issues.find((i) => i.type === 'worktree_uncommitted');
|
|
193
|
+
const mergeConflict = issues.find((i) => i.type === 'merge_conflict');
|
|
194
|
+
const nothingToMerge = issues.find((i) => i.type === 'nothing_to_merge');
|
|
195
|
+
// Handle "nothing to merge" case
|
|
196
|
+
if (nothingToMerge) {
|
|
197
|
+
return {
|
|
198
|
+
type: 'info',
|
|
199
|
+
message: 'No new commits to merge',
|
|
200
|
+
dismissable: true,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
// Handle main branch dirty
|
|
204
|
+
if (mainDirty) {
|
|
205
|
+
return {
|
|
206
|
+
type: 'choice',
|
|
207
|
+
title: 'Main Branch Has Uncommitted Changes',
|
|
208
|
+
message: `${mainBranch} has uncommitted changes in:\n${mainDirty.files.slice(0, 5).join('\n')}${mainDirty.files.length > 5 ? '\n...' : ''}`,
|
|
209
|
+
options: [
|
|
210
|
+
{
|
|
211
|
+
id: 'commit_automatic',
|
|
212
|
+
label: 'AI commit (automatic)',
|
|
213
|
+
description: 'Auto-generate and commit immediately',
|
|
214
|
+
default: true,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
id: 'commit_ai_editable',
|
|
218
|
+
label: 'AI commit (editable)',
|
|
219
|
+
description: 'Generate message from diff, edit before commit',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'commit_manual',
|
|
223
|
+
label: 'Manual commit message',
|
|
224
|
+
description: 'Write your own commit message',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'stash_main',
|
|
228
|
+
label: 'Stash changes in main',
|
|
229
|
+
description: 'Temporarily stash uncommitted changes',
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
id: 'cancel',
|
|
233
|
+
label: 'Cancel merge',
|
|
234
|
+
description: 'Resolve manually later',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
onSelect: async (optionId) => {
|
|
238
|
+
if (optionId === 'cancel') {
|
|
239
|
+
return { type: 'info', message: 'Merge cancelled', dismissable: true };
|
|
240
|
+
}
|
|
241
|
+
if (optionId === 'stash_main') {
|
|
242
|
+
const result = stashChanges(mainRepoPath);
|
|
243
|
+
if (!result.success) {
|
|
244
|
+
return { type: 'error', message: `Stash failed: ${result.error}`, dismissable: true };
|
|
245
|
+
}
|
|
246
|
+
// Retry merge after stashing
|
|
247
|
+
return mergePane(pane, context, { mainBranch });
|
|
248
|
+
}
|
|
249
|
+
if (optionId === 'commit_ai_editable') {
|
|
250
|
+
try {
|
|
251
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
252
|
+
const { getComprehensiveDiff } = await import('../utils/aiMerge.js');
|
|
253
|
+
// Stage all changes first
|
|
254
|
+
const stageResult = stageAllChanges(mainRepoPath);
|
|
255
|
+
if (!stageResult.success) {
|
|
256
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
257
|
+
}
|
|
258
|
+
// Get diff and generate message
|
|
259
|
+
const { diff, summary } = getComprehensiveDiff(mainRepoPath);
|
|
260
|
+
console.error('[commit_ai_editable] Calling generateCommitMessageSafe for main repo');
|
|
261
|
+
const generatedMessage = await generateCommitMessageSafe(mainRepoPath);
|
|
262
|
+
console.error('[commit_ai_editable] Generated message:', generatedMessage);
|
|
263
|
+
// If AI generation failed, fall back to manual with explanation
|
|
264
|
+
if (!generatedMessage) {
|
|
265
|
+
console.error('[commit_ai_editable] Falling back to manual input');
|
|
266
|
+
return {
|
|
267
|
+
type: 'input',
|
|
268
|
+
title: 'Enter Commit Message',
|
|
269
|
+
message: `⚠️ Auto-generation failed or timed out. Please write a commit message manually.\n\nFiles changed:\n${summary}`,
|
|
270
|
+
placeholder: 'feat: add new feature',
|
|
271
|
+
onSubmit: async (message) => {
|
|
272
|
+
if (!message || !message.trim()) {
|
|
273
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
274
|
+
}
|
|
275
|
+
const result = commitChanges(mainRepoPath, message.trim());
|
|
276
|
+
if (!result.success) {
|
|
277
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
278
|
+
}
|
|
279
|
+
// Retry merge after committing
|
|
280
|
+
return mergePane(pane, context, { mainBranch });
|
|
281
|
+
},
|
|
282
|
+
dismissable: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
console.error('[commit_ai_editable] Returning editable input with generated message');
|
|
286
|
+
return {
|
|
287
|
+
type: 'input',
|
|
288
|
+
title: 'Review & Edit Commit Message',
|
|
289
|
+
message: `Files changed:\n${summary}\n\nGenerated message (edit as needed):`,
|
|
290
|
+
placeholder: 'feat: add new feature',
|
|
291
|
+
defaultValue: generatedMessage,
|
|
292
|
+
onSubmit: async (message) => {
|
|
293
|
+
if (!message || !message.trim()) {
|
|
294
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
295
|
+
}
|
|
296
|
+
const result = commitChanges(mainRepoPath, message.trim());
|
|
297
|
+
if (!result.success) {
|
|
298
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
299
|
+
}
|
|
300
|
+
// Retry merge after committing
|
|
301
|
+
return mergePane(pane, context, { mainBranch });
|
|
302
|
+
},
|
|
303
|
+
dismissable: true,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
console.error('[commit_ai_editable] Unexpected error:', error);
|
|
308
|
+
return {
|
|
309
|
+
type: 'error',
|
|
310
|
+
message: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
|
|
311
|
+
dismissable: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (optionId === 'commit_automatic') {
|
|
316
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
317
|
+
const { getComprehensiveDiff } = await import('../utils/aiMerge.js');
|
|
318
|
+
// Stage all changes first
|
|
319
|
+
const stageResult = stageAllChanges(mainRepoPath);
|
|
320
|
+
if (!stageResult.success) {
|
|
321
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
322
|
+
}
|
|
323
|
+
// Generate commit message
|
|
324
|
+
const message = await generateCommitMessageSafe(mainRepoPath);
|
|
325
|
+
// If AI generation failed, fall back to manual input
|
|
326
|
+
if (!message) {
|
|
327
|
+
const { summary } = getComprehensiveDiff(mainRepoPath);
|
|
328
|
+
return {
|
|
329
|
+
type: 'input',
|
|
330
|
+
title: 'Enter Commit Message',
|
|
331
|
+
message: `⚠️ Auto-generation failed or timed out. Please write a commit message manually.\n\nFiles changed:\n${summary}`,
|
|
332
|
+
placeholder: 'feat: add new feature',
|
|
333
|
+
onSubmit: async (manualMessage) => {
|
|
334
|
+
if (!manualMessage || !manualMessage.trim()) {
|
|
335
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
336
|
+
}
|
|
337
|
+
const result = commitChanges(mainRepoPath, manualMessage.trim());
|
|
338
|
+
if (!result.success) {
|
|
339
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
340
|
+
}
|
|
341
|
+
// Retry merge after committing
|
|
342
|
+
return mergePane(pane, context, { mainBranch });
|
|
343
|
+
},
|
|
344
|
+
dismissable: true,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const result = commitChanges(mainRepoPath, message);
|
|
348
|
+
if (!result.success) {
|
|
349
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
350
|
+
}
|
|
351
|
+
// Retry merge after committing
|
|
352
|
+
return mergePane(pane, context, { mainBranch });
|
|
353
|
+
}
|
|
354
|
+
if (optionId === 'commit_manual') {
|
|
355
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
356
|
+
// Stage all changes first
|
|
357
|
+
const stageResult = stageAllChanges(mainRepoPath);
|
|
358
|
+
if (!stageResult.success) {
|
|
359
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
type: 'input',
|
|
363
|
+
title: 'Enter Commit Message',
|
|
364
|
+
message: 'Write a commit message for the changes in main:',
|
|
365
|
+
placeholder: 'feat: add new feature',
|
|
366
|
+
onSubmit: async (message) => {
|
|
367
|
+
if (!message || !message.trim()) {
|
|
368
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
369
|
+
}
|
|
370
|
+
const result = commitChanges(mainRepoPath, message.trim());
|
|
371
|
+
if (!result.success) {
|
|
372
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
373
|
+
}
|
|
374
|
+
// Retry merge after committing
|
|
375
|
+
return mergePane(pane, context, { mainBranch });
|
|
376
|
+
},
|
|
377
|
+
dismissable: true,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { type: 'info', message: 'Unknown option', dismissable: true };
|
|
381
|
+
},
|
|
382
|
+
dismissable: true,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// Handle worktree uncommitted changes
|
|
386
|
+
if (worktreeUncommitted) {
|
|
387
|
+
return {
|
|
388
|
+
type: 'choice',
|
|
389
|
+
title: 'Worktree Has Uncommitted Changes',
|
|
390
|
+
message: `Changes in:\n${worktreeUncommitted.files.slice(0, 5).join('\n')}${worktreeUncommitted.files.length > 5 ? '\n...' : ''}`,
|
|
391
|
+
options: [
|
|
392
|
+
{
|
|
393
|
+
id: 'commit_automatic',
|
|
394
|
+
label: 'AI commit (automatic)',
|
|
395
|
+
description: 'Auto-generate and commit immediately',
|
|
396
|
+
default: true,
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
id: 'commit_ai_editable',
|
|
400
|
+
label: 'AI commit (editable)',
|
|
401
|
+
description: 'Generate message from diff, edit before commit',
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: 'commit_manual',
|
|
405
|
+
label: 'Manual commit message',
|
|
406
|
+
description: 'Write your own commit message',
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'cancel',
|
|
410
|
+
label: 'Cancel merge',
|
|
411
|
+
description: 'Resolve manually later',
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
onSelect: async (optionId) => {
|
|
415
|
+
if (optionId === 'cancel') {
|
|
416
|
+
return { type: 'info', message: 'Merge cancelled', dismissable: true };
|
|
417
|
+
}
|
|
418
|
+
if (optionId === 'commit_ai_editable') {
|
|
419
|
+
try {
|
|
420
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
421
|
+
const { getComprehensiveDiff } = await import('../utils/aiMerge.js');
|
|
422
|
+
// Stage all changes first
|
|
423
|
+
const stageResult = stageAllChanges(pane.worktreePath);
|
|
424
|
+
if (!stageResult.success) {
|
|
425
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
426
|
+
}
|
|
427
|
+
// Get diff and generate message
|
|
428
|
+
const { diff, summary } = getComprehensiveDiff(pane.worktreePath);
|
|
429
|
+
console.error('[worktree commit_ai_editable] Calling generateCommitMessageSafe');
|
|
430
|
+
const generatedMessage = await generateCommitMessageSafe(pane.worktreePath);
|
|
431
|
+
console.error('[worktree commit_ai_editable] Generated message:', generatedMessage);
|
|
432
|
+
// If AI generation failed, fall back to manual with explanation
|
|
433
|
+
if (!generatedMessage) {
|
|
434
|
+
console.error('[worktree commit_ai_editable] Falling back to manual input');
|
|
435
|
+
return {
|
|
436
|
+
type: 'input',
|
|
437
|
+
title: 'Enter Commit Message',
|
|
438
|
+
message: `⚠️ Auto-generation failed or timed out. Please write a commit message manually.\n\nFiles changed:\n${summary}`,
|
|
439
|
+
placeholder: 'feat: add new feature',
|
|
440
|
+
onSubmit: async (message) => {
|
|
441
|
+
if (!message || !message.trim()) {
|
|
442
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
443
|
+
}
|
|
444
|
+
const result = commitChanges(pane.worktreePath, message.trim());
|
|
445
|
+
if (!result.success) {
|
|
446
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
447
|
+
}
|
|
448
|
+
// Retry merge after committing
|
|
449
|
+
return mergePane(pane, context, { mainBranch });
|
|
450
|
+
},
|
|
451
|
+
dismissable: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
console.error('[worktree commit_ai_editable] Returning editable input with generated message');
|
|
455
|
+
return {
|
|
456
|
+
type: 'input',
|
|
457
|
+
title: 'Review & Edit Commit Message',
|
|
458
|
+
message: `Files changed:\n${summary}\n\nGenerated message (edit as needed):`,
|
|
459
|
+
placeholder: 'feat: add new feature',
|
|
460
|
+
defaultValue: generatedMessage,
|
|
461
|
+
onSubmit: async (message) => {
|
|
462
|
+
if (!message || !message.trim()) {
|
|
463
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
464
|
+
}
|
|
465
|
+
const result = commitChanges(pane.worktreePath, message.trim());
|
|
466
|
+
if (!result.success) {
|
|
467
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
468
|
+
}
|
|
469
|
+
// Retry merge after committing
|
|
470
|
+
return mergePane(pane, context, { mainBranch });
|
|
471
|
+
},
|
|
472
|
+
dismissable: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error('[worktree commit_ai_editable] Unexpected error:', error);
|
|
477
|
+
return {
|
|
478
|
+
type: 'error',
|
|
479
|
+
message: `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
|
|
480
|
+
dismissable: true,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (optionId === 'commit_automatic') {
|
|
485
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
486
|
+
const { getComprehensiveDiff } = await import('../utils/aiMerge.js');
|
|
487
|
+
// Stage all changes first
|
|
488
|
+
const stageResult = stageAllChanges(pane.worktreePath);
|
|
489
|
+
if (!stageResult.success) {
|
|
490
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
491
|
+
}
|
|
492
|
+
const message = await generateCommitMessageSafe(pane.worktreePath);
|
|
493
|
+
// If AI generation failed, fall back to manual input
|
|
494
|
+
if (!message) {
|
|
495
|
+
const { summary } = getComprehensiveDiff(pane.worktreePath);
|
|
496
|
+
return {
|
|
497
|
+
type: 'input',
|
|
498
|
+
title: 'Enter Commit Message',
|
|
499
|
+
message: `⚠️ Auto-generation failed or timed out. Please write a commit message manually.\n\nFiles changed:\n${summary}`,
|
|
500
|
+
placeholder: 'feat: add new feature',
|
|
501
|
+
onSubmit: async (manualMessage) => {
|
|
502
|
+
if (!manualMessage || !manualMessage.trim()) {
|
|
503
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
504
|
+
}
|
|
505
|
+
const result = commitChanges(pane.worktreePath, manualMessage.trim());
|
|
506
|
+
if (!result.success) {
|
|
507
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
508
|
+
}
|
|
509
|
+
// Retry merge after committing
|
|
510
|
+
return mergePane(pane, context, { mainBranch });
|
|
511
|
+
},
|
|
512
|
+
dismissable: true,
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const result = commitChanges(pane.worktreePath, message);
|
|
516
|
+
if (!result.success) {
|
|
517
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
518
|
+
}
|
|
519
|
+
// Retry merge after committing
|
|
520
|
+
return mergePane(pane, context, { mainBranch });
|
|
521
|
+
}
|
|
522
|
+
if (optionId === 'commit_manual') {
|
|
523
|
+
const { stageAllChanges } = await import('../utils/mergeValidation.js');
|
|
524
|
+
// Stage all changes first
|
|
525
|
+
const stageResult = stageAllChanges(pane.worktreePath);
|
|
526
|
+
if (!stageResult.success) {
|
|
527
|
+
return { type: 'error', message: `Failed to stage changes: ${stageResult.error}`, dismissable: true };
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
type: 'input',
|
|
531
|
+
title: 'Enter Commit Message',
|
|
532
|
+
message: 'Write a commit message for the changes:',
|
|
533
|
+
placeholder: 'feat: add new feature',
|
|
534
|
+
onSubmit: async (message) => {
|
|
535
|
+
if (!message || !message.trim()) {
|
|
536
|
+
return { type: 'error', message: 'Commit message cannot be empty', dismissable: true };
|
|
537
|
+
}
|
|
538
|
+
const result = commitChanges(pane.worktreePath, message.trim());
|
|
539
|
+
if (!result.success) {
|
|
540
|
+
return { type: 'error', message: `Commit failed: ${result.error}`, dismissable: true };
|
|
541
|
+
}
|
|
542
|
+
// Retry merge after committing
|
|
543
|
+
return mergePane(pane, context, { mainBranch });
|
|
544
|
+
},
|
|
545
|
+
dismissable: true,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
return { type: 'info', message: 'Unknown option', dismissable: true };
|
|
549
|
+
},
|
|
550
|
+
dismissable: true,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
// Handle merge conflicts
|
|
554
|
+
if (mergeConflict) {
|
|
555
|
+
return {
|
|
556
|
+
type: 'choice',
|
|
557
|
+
title: 'Merge Conflicts Detected',
|
|
558
|
+
message: `Conflicts will occur in:\n${mergeConflict.files.slice(0, 5).join('\n')}${mergeConflict.files.length > 5 ? '\n...' : ''}`,
|
|
559
|
+
options: [
|
|
560
|
+
{
|
|
561
|
+
id: 'ai_merge',
|
|
562
|
+
label: 'Try AI-assisted merge',
|
|
563
|
+
description: 'Let AI intelligently combine both versions',
|
|
564
|
+
default: true,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: 'manual_merge',
|
|
568
|
+
label: 'Manual resolution',
|
|
569
|
+
description: 'Jump to pane to resolve conflicts',
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
id: 'cancel',
|
|
573
|
+
label: 'Cancel merge',
|
|
574
|
+
description: 'Do nothing',
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
onSelect: async (optionId) => {
|
|
578
|
+
if (optionId === 'cancel') {
|
|
579
|
+
return { type: 'info', message: 'Merge cancelled', dismissable: true };
|
|
580
|
+
}
|
|
581
|
+
if (optionId === 'manual_merge') {
|
|
582
|
+
// Start the merge process and let user resolve manually
|
|
583
|
+
return executeMergeWithConflictHandling(pane, context, mainBranch, mainRepoPath, 'manual');
|
|
584
|
+
}
|
|
585
|
+
if (optionId === 'ai_merge') {
|
|
586
|
+
// Attempt AI-assisted merge via new pane
|
|
587
|
+
return createConflictResolutionPaneForMerge(pane, context, mainBranch, mainRepoPath);
|
|
588
|
+
}
|
|
589
|
+
return { type: 'info', message: 'Unknown option', dismissable: true };
|
|
590
|
+
},
|
|
591
|
+
dismissable: true,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
// Generic issue display
|
|
595
|
+
return {
|
|
596
|
+
type: 'error',
|
|
597
|
+
title: 'Merge Issues Detected',
|
|
598
|
+
message: issues.map((i) => i.message).join('\n'),
|
|
599
|
+
dismissable: true,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Create a new pane for AI-assisted conflict resolution
|
|
604
|
+
*/
|
|
605
|
+
async function createConflictResolutionPaneForMerge(pane, context, targetBranch, targetRepoPath) {
|
|
606
|
+
// First, check which agents are available
|
|
607
|
+
const { findClaudeCommand, findOpencodeCommand } = await import('../utils/agentDetection.js');
|
|
608
|
+
const availableAgents = [];
|
|
609
|
+
if (await findClaudeCommand())
|
|
610
|
+
availableAgents.push('claude');
|
|
611
|
+
if (await findOpencodeCommand())
|
|
612
|
+
availableAgents.push('opencode');
|
|
613
|
+
if (availableAgents.length === 0) {
|
|
614
|
+
return {
|
|
615
|
+
type: 'error',
|
|
616
|
+
message: 'No AI agents available. Please install claude or opencode.',
|
|
617
|
+
dismissable: true,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
// If multiple agents available, ask user to choose
|
|
621
|
+
if (availableAgents.length > 1) {
|
|
622
|
+
return {
|
|
623
|
+
type: 'choice',
|
|
624
|
+
title: 'Choose AI Agent for Conflict Resolution',
|
|
625
|
+
message: 'Which agent would you like to use to resolve merge conflicts?',
|
|
626
|
+
options: availableAgents.map(agent => ({
|
|
627
|
+
id: agent,
|
|
628
|
+
label: agent === 'claude' ? 'Claude Code' : 'OpenCode',
|
|
629
|
+
description: agent === 'claude' ? 'Anthropic Claude' : 'Open-source alternative',
|
|
630
|
+
default: agent === 'claude',
|
|
631
|
+
})),
|
|
632
|
+
onSelect: async (agentId) => {
|
|
633
|
+
return createAndLaunchConflictPane(pane, context, targetBranch, targetRepoPath, agentId);
|
|
634
|
+
},
|
|
635
|
+
dismissable: true,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// Only one agent available, use it directly
|
|
639
|
+
return createAndLaunchConflictPane(pane, context, targetBranch, targetRepoPath, availableAgents[0]);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Actually create and launch the conflict resolution pane
|
|
643
|
+
*/
|
|
644
|
+
async function createAndLaunchConflictPane(pane, context, targetBranch, targetRepoPath, agent) {
|
|
645
|
+
try {
|
|
646
|
+
const { createConflictResolutionPane } = await import('../utils/conflictResolutionPane.js');
|
|
647
|
+
// Create the new pane
|
|
648
|
+
const conflictPane = await createConflictResolutionPane({
|
|
649
|
+
sourceBranch: pane.slug,
|
|
650
|
+
targetBranch,
|
|
651
|
+
targetRepoPath,
|
|
652
|
+
agent,
|
|
653
|
+
projectName: context.projectName,
|
|
654
|
+
existingPanes: context.panes,
|
|
655
|
+
});
|
|
656
|
+
// Add the new pane to the panes list
|
|
657
|
+
const updatedPanes = [...context.panes, conflictPane];
|
|
658
|
+
await context.savePanes(updatedPanes);
|
|
659
|
+
// Notify about the new pane
|
|
660
|
+
if (context.onPaneUpdate) {
|
|
661
|
+
context.onPaneUpdate(conflictPane);
|
|
662
|
+
}
|
|
663
|
+
return {
|
|
664
|
+
type: 'navigation',
|
|
665
|
+
title: 'Conflict Resolution Pane Created',
|
|
666
|
+
message: `Created pane "${conflictPane.slug}" with ${agent} to help resolve conflicts. Switch to it to see the AI working.`,
|
|
667
|
+
targetPaneId: conflictPane.id,
|
|
668
|
+
dismissable: true,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
catch (error) {
|
|
672
|
+
return {
|
|
673
|
+
type: 'error',
|
|
674
|
+
message: `Failed to create conflict resolution pane: ${error instanceof Error ? error.message : String(error)}`,
|
|
675
|
+
dismissable: true,
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Execute merge with conflict handling
|
|
681
|
+
*/
|
|
682
|
+
async function executeMergeWithConflictHandling(pane, context, mainBranch, mainRepoPath, strategy) {
|
|
683
|
+
const { mergeMainIntoWorktree, getConflictingFiles, completeMerge } = await import('../utils/mergeExecution.js');
|
|
684
|
+
// Step 1: Merge main into worktree
|
|
685
|
+
const result = mergeMainIntoWorktree(pane.worktreePath, mainBranch);
|
|
686
|
+
if (!result.success && result.needsManualResolution) {
|
|
687
|
+
if (strategy === 'ai') {
|
|
688
|
+
// Try AI resolution
|
|
689
|
+
const { aiResolveAllConflicts } = await import('../utils/aiMerge.js');
|
|
690
|
+
const aiResult = await aiResolveAllConflicts(pane.worktreePath, result.conflictFiles || []);
|
|
691
|
+
if (aiResult.success) {
|
|
692
|
+
// AI resolved all conflicts, complete the merge
|
|
693
|
+
const completeResult = completeMerge(pane.worktreePath, 'Merge with AI-resolved conflicts');
|
|
694
|
+
if (completeResult.success) {
|
|
695
|
+
// Continue with the second phase of merge
|
|
696
|
+
return executeMerge(pane, context, mainBranch, mainRepoPath);
|
|
697
|
+
}
|
|
698
|
+
else {
|
|
699
|
+
return {
|
|
700
|
+
type: 'error',
|
|
701
|
+
message: `Failed to complete merge: ${completeResult.error}`,
|
|
702
|
+
dismissable: true,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
else {
|
|
707
|
+
// AI couldn't resolve, fall back to manual
|
|
708
|
+
return {
|
|
709
|
+
type: 'error',
|
|
710
|
+
title: 'AI Merge Failed',
|
|
711
|
+
message: `AI couldn't resolve conflicts in: ${aiResult.failedFiles.join(', ')}.\nPlease resolve manually.`,
|
|
712
|
+
dismissable: true,
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
// Manual resolution - jump to pane
|
|
718
|
+
return {
|
|
719
|
+
type: 'navigation',
|
|
720
|
+
title: 'Manual Conflict Resolution',
|
|
721
|
+
message: `Conflicts in: ${result.conflictFiles?.join(', ')}.\nResolve in the pane, then try merge again.`,
|
|
722
|
+
targetPaneId: pane.id,
|
|
723
|
+
dismissable: true,
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (!result.success) {
|
|
728
|
+
return {
|
|
729
|
+
type: 'error',
|
|
730
|
+
message: `Merge failed: ${result.error}`,
|
|
731
|
+
dismissable: true,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
// No conflicts, proceed with the main merge
|
|
735
|
+
return executeMerge(pane, context, mainBranch, mainRepoPath);
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Execute the actual merge operation (called after all pre-checks pass)
|
|
739
|
+
*/
|
|
740
|
+
async function executeMerge(pane, context, mainBranch, mainRepoPath) {
|
|
741
|
+
const { mergeWorktreeIntoMain, cleanupAfterMerge } = await import('../utils/mergeExecution.js');
|
|
742
|
+
// Step 2: Merge worktree into main
|
|
743
|
+
const result = mergeWorktreeIntoMain(mainRepoPath, pane.slug);
|
|
744
|
+
if (!result.success) {
|
|
745
|
+
return {
|
|
746
|
+
type: 'error',
|
|
747
|
+
title: 'Merge Failed',
|
|
748
|
+
message: `Failed to merge into ${mainBranch}: ${result.error}`,
|
|
749
|
+
dismissable: true,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
// Merge successful! Ask about cleanup
|
|
753
|
+
return {
|
|
754
|
+
type: 'confirm',
|
|
755
|
+
title: 'Merge Complete',
|
|
756
|
+
message: `Successfully merged "${pane.slug}" into ${mainBranch}. Close the pane and cleanup worktree?`,
|
|
757
|
+
confirmLabel: 'Yes, close it',
|
|
758
|
+
cancelLabel: 'No, keep it',
|
|
759
|
+
onConfirm: async () => {
|
|
760
|
+
// Cleanup worktree and branch
|
|
761
|
+
const cleanup = cleanupAfterMerge(mainRepoPath, pane.worktreePath, pane.slug);
|
|
762
|
+
if (!cleanup.success) {
|
|
763
|
+
return {
|
|
764
|
+
type: 'error',
|
|
765
|
+
message: `Merge succeeded but cleanup failed: ${cleanup.error}`,
|
|
766
|
+
dismissable: true,
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
// Close the pane
|
|
770
|
+
return executeCloseOption(pane, context, 'kill_only');
|
|
771
|
+
},
|
|
772
|
+
onCancel: async () => {
|
|
773
|
+
return {
|
|
774
|
+
type: 'success',
|
|
775
|
+
message: 'Merge complete. Pane kept open.',
|
|
776
|
+
dismissable: true,
|
|
777
|
+
};
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Rename a pane
|
|
783
|
+
*/
|
|
784
|
+
export async function renamePane(pane, context) {
|
|
785
|
+
return {
|
|
786
|
+
type: 'input',
|
|
787
|
+
title: 'Rename Pane',
|
|
788
|
+
message: 'Enter new name for pane:',
|
|
789
|
+
placeholder: pane.slug,
|
|
790
|
+
defaultValue: pane.slug,
|
|
791
|
+
onSubmit: async (value) => {
|
|
792
|
+
if (!value || value === pane.slug) {
|
|
793
|
+
return {
|
|
794
|
+
type: 'info',
|
|
795
|
+
message: 'Rename cancelled',
|
|
796
|
+
dismissable: true,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
// Update pane name
|
|
800
|
+
const updatedPane = {
|
|
801
|
+
...pane,
|
|
802
|
+
slug: value,
|
|
803
|
+
};
|
|
804
|
+
// Update tmux pane title
|
|
805
|
+
try {
|
|
806
|
+
execSync(`tmux select-pane -t '${pane.paneId}' -T "${value}"`, { stdio: 'pipe' });
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
// Ignore if title update fails
|
|
810
|
+
}
|
|
811
|
+
// Update in panes list
|
|
812
|
+
const updatedPanes = context.panes.map(p => p.id === pane.id ? updatedPane : p);
|
|
813
|
+
await context.savePanes(updatedPanes);
|
|
814
|
+
if (context.onPaneUpdate) {
|
|
815
|
+
context.onPaneUpdate(updatedPane);
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
type: 'success',
|
|
819
|
+
message: `Renamed to "${value}"`,
|
|
820
|
+
dismissable: true,
|
|
821
|
+
};
|
|
822
|
+
},
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Copy worktree path to clipboard
|
|
827
|
+
*/
|
|
828
|
+
export async function copyPath(pane, context) {
|
|
829
|
+
if (!pane.worktreePath) {
|
|
830
|
+
return {
|
|
831
|
+
type: 'error',
|
|
832
|
+
message: 'This pane has no worktree path',
|
|
833
|
+
dismissable: true,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
// Try to copy to clipboard (works on macOS)
|
|
838
|
+
execSync(`echo "${pane.worktreePath}" | pbcopy`, { stdio: 'pipe' });
|
|
839
|
+
return {
|
|
840
|
+
type: 'success',
|
|
841
|
+
message: `Path copied: ${pane.worktreePath}`,
|
|
842
|
+
dismissable: true,
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
catch {
|
|
846
|
+
// If clipboard copy fails, just show the path
|
|
847
|
+
return {
|
|
848
|
+
type: 'info',
|
|
849
|
+
message: `Path: ${pane.worktreePath}`,
|
|
850
|
+
dismissable: true,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Open worktree in external editor
|
|
856
|
+
*/
|
|
857
|
+
export async function openInEditor(pane, context, params) {
|
|
858
|
+
if (!pane.worktreePath) {
|
|
859
|
+
return {
|
|
860
|
+
type: 'error',
|
|
861
|
+
message: 'This pane has no worktree to open',
|
|
862
|
+
dismissable: true,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
const editor = params?.editor || process.env.EDITOR || 'code';
|
|
866
|
+
try {
|
|
867
|
+
execSync(`${editor} "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
868
|
+
return {
|
|
869
|
+
type: 'success',
|
|
870
|
+
message: `Opened in ${editor}`,
|
|
871
|
+
dismissable: true,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
return {
|
|
876
|
+
type: 'error',
|
|
877
|
+
message: `Failed to open in editor: ${error}`,
|
|
878
|
+
dismissable: true,
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Duplicate a pane (create a new pane with the same prompt)
|
|
884
|
+
*/
|
|
885
|
+
export async function duplicatePane(pane, context) {
|
|
886
|
+
return {
|
|
887
|
+
type: 'confirm',
|
|
888
|
+
title: 'Duplicate Pane',
|
|
889
|
+
message: `Create a new pane with the same prompt as "${pane.slug}"?`,
|
|
890
|
+
confirmLabel: 'Duplicate',
|
|
891
|
+
cancelLabel: 'Cancel',
|
|
892
|
+
onConfirm: async () => {
|
|
893
|
+
// This would trigger the new pane creation flow with the same prompt
|
|
894
|
+
return {
|
|
895
|
+
type: 'info',
|
|
896
|
+
message: 'Duplication not yet implemented',
|
|
897
|
+
data: { action: 'create_pane', prompt: pane.prompt, agent: pane.agent },
|
|
898
|
+
dismissable: true,
|
|
899
|
+
};
|
|
900
|
+
},
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Toggle autopilot mode for a pane
|
|
905
|
+
*/
|
|
906
|
+
export async function toggleAutopilot(pane, context) {
|
|
907
|
+
try {
|
|
908
|
+
// Toggle the autopilot setting
|
|
909
|
+
const newAutopilotState = !pane.autopilot;
|
|
910
|
+
// Update the pane
|
|
911
|
+
const updatedPanes = context.panes.map(p => p.id === pane.id ? { ...p, autopilot: newAutopilotState } : p);
|
|
912
|
+
// Save the updated panes
|
|
913
|
+
await context.savePanes(updatedPanes);
|
|
914
|
+
// Notify about the update
|
|
915
|
+
if (context.onPaneUpdate) {
|
|
916
|
+
context.onPaneUpdate({ ...pane, autopilot: newAutopilotState });
|
|
917
|
+
}
|
|
918
|
+
return {
|
|
919
|
+
type: 'success',
|
|
920
|
+
message: `Autopilot ${newAutopilotState ? 'enabled' : 'disabled'} for "${pane.slug}"`,
|
|
921
|
+
dismissable: true,
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
catch (error) {
|
|
925
|
+
return {
|
|
926
|
+
type: 'error',
|
|
927
|
+
message: `Failed to toggle autopilot: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
928
|
+
dismissable: true,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
//# sourceMappingURL=paneActions.js.map
|