@co0ontty/wand 1.7.0 → 1.9.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/config.js +40 -0
- package/dist/git-worktree.d.ts +17 -1
- package/dist/git-worktree.js +244 -0
- package/dist/server-session-routes.js +153 -0
- package/dist/server.js +34 -0
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +204 -66
- package/dist/types.d.ts +51 -0
- package/dist/web-ui/content/scripts.js +802 -218
- package/dist/web-ui/content/styles.css +1085 -185
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -16,6 +16,20 @@ export const defaultConfig = () => ({
|
|
|
16
16
|
allowedCommandPrefixes: [],
|
|
17
17
|
shortcutLogMaxBytes: 10 * 1024 * 1024,
|
|
18
18
|
language: "",
|
|
19
|
+
uiPreferences: {
|
|
20
|
+
defaultPanelState: {
|
|
21
|
+
sessionsDrawerOpen: false,
|
|
22
|
+
filePanelOpen: false,
|
|
23
|
+
shortcutsExpanded: false,
|
|
24
|
+
claudeHistoryExpanded: true,
|
|
25
|
+
chatMessageExpanded: true,
|
|
26
|
+
structuredThinkingExpanded: true,
|
|
27
|
+
structuredToolGroupExpanded: false,
|
|
28
|
+
structuredInlineToolExpanded: false,
|
|
29
|
+
structuredTerminalExpanded: false,
|
|
30
|
+
structuredToolCardExpanded: false,
|
|
31
|
+
}
|
|
32
|
+
},
|
|
19
33
|
commandPresets: [
|
|
20
34
|
{
|
|
21
35
|
label: "Claude",
|
|
@@ -101,8 +115,26 @@ function normalizeStructuredChatPersona(input) {
|
|
|
101
115
|
return undefined;
|
|
102
116
|
return { user, assistant };
|
|
103
117
|
}
|
|
118
|
+
function normalizeDefaultPanelState(input) {
|
|
119
|
+
if (!input || typeof input !== "object")
|
|
120
|
+
return undefined;
|
|
121
|
+
const state = input;
|
|
122
|
+
return {
|
|
123
|
+
sessionsDrawerOpen: typeof state.sessionsDrawerOpen === "boolean" ? state.sessionsDrawerOpen : undefined,
|
|
124
|
+
filePanelOpen: typeof state.filePanelOpen === "boolean" ? state.filePanelOpen : undefined,
|
|
125
|
+
shortcutsExpanded: typeof state.shortcutsExpanded === "boolean" ? state.shortcutsExpanded : undefined,
|
|
126
|
+
claudeHistoryExpanded: typeof state.claudeHistoryExpanded === "boolean" ? state.claudeHistoryExpanded : undefined,
|
|
127
|
+
chatMessageExpanded: typeof state.chatMessageExpanded === "boolean" ? state.chatMessageExpanded : undefined,
|
|
128
|
+
structuredThinkingExpanded: typeof state.structuredThinkingExpanded === "boolean" ? state.structuredThinkingExpanded : undefined,
|
|
129
|
+
structuredToolGroupExpanded: typeof state.structuredToolGroupExpanded === "boolean" ? state.structuredToolGroupExpanded : undefined,
|
|
130
|
+
structuredInlineToolExpanded: typeof state.structuredInlineToolExpanded === "boolean" ? state.structuredInlineToolExpanded : undefined,
|
|
131
|
+
structuredTerminalExpanded: typeof state.structuredTerminalExpanded === "boolean" ? state.structuredTerminalExpanded : undefined,
|
|
132
|
+
structuredToolCardExpanded: typeof state.structuredToolCardExpanded === "boolean" ? state.structuredToolCardExpanded : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
104
135
|
function mergeWithDefaults(input) {
|
|
105
136
|
const defaults = defaultConfig();
|
|
137
|
+
const normalizedPanelState = normalizeDefaultPanelState(input.uiPreferences?.defaultPanelState);
|
|
106
138
|
return {
|
|
107
139
|
...defaults,
|
|
108
140
|
...input,
|
|
@@ -132,6 +164,14 @@ function mergeWithDefaults(input) {
|
|
|
132
164
|
: defaults.commandPresets,
|
|
133
165
|
structuredChatPersona: normalizeStructuredChatPersona(input.structuredChatPersona),
|
|
134
166
|
language: typeof input.language === "string" ? input.language.trim() : defaults.language,
|
|
167
|
+
uiPreferences: {
|
|
168
|
+
...defaults.uiPreferences,
|
|
169
|
+
...(input.uiPreferences || {}),
|
|
170
|
+
defaultPanelState: {
|
|
171
|
+
...defaults.uiPreferences?.defaultPanelState,
|
|
172
|
+
...(normalizedPanelState || {}),
|
|
173
|
+
}
|
|
174
|
+
},
|
|
135
175
|
};
|
|
136
176
|
}
|
|
137
177
|
export function isExecutionMode(value) {
|
package/dist/git-worktree.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionSnapshot } from "./types.js";
|
|
1
|
+
import { SessionSnapshot, WorktreeMergeCheckResult, WorktreeMergeResult } from "./types.js";
|
|
2
2
|
interface WorktreeSetupOptions {
|
|
3
3
|
cwd: string;
|
|
4
4
|
sessionId: string;
|
|
@@ -8,5 +8,21 @@ interface WorktreeSetupResult {
|
|
|
8
8
|
worktreeEnabled: boolean;
|
|
9
9
|
worktree: NonNullable<SessionSnapshot["worktree"]>;
|
|
10
10
|
}
|
|
11
|
+
interface WorktreeOperationOptions {
|
|
12
|
+
worktree: NonNullable<SessionSnapshot["worktree"]>;
|
|
13
|
+
}
|
|
14
|
+
interface WorktreeMergeOptions extends WorktreeOperationOptions {
|
|
15
|
+
targetBranch?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class WorktreeMergeError extends Error {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly result?: Partial<WorktreeMergeResult> | undefined;
|
|
20
|
+
constructor(code: string, message: string, result?: Partial<WorktreeMergeResult> | undefined);
|
|
21
|
+
}
|
|
22
|
+
export declare function getDefaultBaseBranch(repoRoot: string): string;
|
|
23
|
+
export declare function getWorktreeMergeErrorCode(error: unknown): string | undefined;
|
|
24
|
+
export declare function checkSessionWorktreeMergeability(options: WorktreeMergeOptions): WorktreeMergeCheckResult;
|
|
25
|
+
export declare function cleanupSessionWorktree(options: WorktreeOperationOptions): boolean;
|
|
26
|
+
export declare function mergeSessionWorktree(options: WorktreeMergeOptions): WorktreeMergeResult;
|
|
11
27
|
export declare function prepareSessionWorktree(options: WorktreeSetupOptions): WorktreeSetupResult;
|
|
12
28
|
export {};
|
package/dist/git-worktree.js
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
const WORKTREE_MERGE_ERROR_CODES = {
|
|
5
|
+
MISSING: "WORKTREE_MISSING",
|
|
6
|
+
DIRTY: "WORKTREE_DIRTY",
|
|
7
|
+
TARGET_MISSING: "TARGET_BRANCH_MISSING",
|
|
8
|
+
NOTHING_TO_MERGE: "NOTHING_TO_MERGE",
|
|
9
|
+
CONFLICT: "WORKTREE_MERGE_CONFLICT",
|
|
10
|
+
CLEANUP_FAILED: "WORKTREE_CLEANUP_FAILED",
|
|
11
|
+
};
|
|
12
|
+
export class WorktreeMergeError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
result;
|
|
15
|
+
constructor(code, message, result) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.result = result;
|
|
19
|
+
this.name = "WorktreeMergeError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
4
22
|
function runGit(args, cwd) {
|
|
5
23
|
return execFileSync("git", args, {
|
|
6
24
|
cwd,
|
|
@@ -19,6 +37,232 @@ function getCurrentBranch(repoRoot) {
|
|
|
19
37
|
const branch = runGit(["branch", "--show-current"], repoRoot);
|
|
20
38
|
return branch || "master";
|
|
21
39
|
}
|
|
40
|
+
function isGitCommandError(error) {
|
|
41
|
+
return error instanceof Error;
|
|
42
|
+
}
|
|
43
|
+
function getGitCommandMessage(error) {
|
|
44
|
+
if (isGitCommandError(error)) {
|
|
45
|
+
return error.stderr?.trim() || error.stdout?.trim() || error.message;
|
|
46
|
+
}
|
|
47
|
+
return String(error);
|
|
48
|
+
}
|
|
49
|
+
function refExists(repoRoot, ref) {
|
|
50
|
+
try {
|
|
51
|
+
runGit(["rev-parse", "--verify", ref], repoRoot);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function getRepoRootFromWorktree(worktreePath) {
|
|
59
|
+
const repoRoot = runGit(["rev-parse", "--show-toplevel"], worktreePath);
|
|
60
|
+
if (!repoRoot || !existsSync(repoRoot)) {
|
|
61
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 仓库根目录不存在。", {
|
|
62
|
+
cleanupDone: false,
|
|
63
|
+
conflict: false,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return repoRoot;
|
|
67
|
+
}
|
|
68
|
+
function ensureWorktreePath(worktree) {
|
|
69
|
+
const worktreePath = path.resolve(worktree.path);
|
|
70
|
+
if (!existsSync(worktreePath)) {
|
|
71
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 目录不存在,可能已被手动删除。", {
|
|
72
|
+
cleanupDone: false,
|
|
73
|
+
conflict: false,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return worktreePath;
|
|
77
|
+
}
|
|
78
|
+
function getMainRepoRoot(repoRoot) {
|
|
79
|
+
const commonDir = runGit(["rev-parse", "--git-common-dir"], repoRoot);
|
|
80
|
+
if (commonDir) {
|
|
81
|
+
const maybeRoot = path.resolve(repoRoot, commonDir, "..");
|
|
82
|
+
if (existsSync(maybeRoot)) {
|
|
83
|
+
return maybeRoot;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return repoRoot;
|
|
87
|
+
}
|
|
88
|
+
export function getDefaultBaseBranch(repoRoot) {
|
|
89
|
+
try {
|
|
90
|
+
const symbolicRef = runGit(["symbolic-ref", "refs/remotes/origin/HEAD"], repoRoot);
|
|
91
|
+
const match = symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
92
|
+
if (match && match[1]) {
|
|
93
|
+
return match[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// ignore and fallback below
|
|
98
|
+
}
|
|
99
|
+
const candidates = ["master", "main", getCurrentBranch(repoRoot)];
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
if (candidate && refExists(repoRoot, candidate)) {
|
|
102
|
+
return candidate;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return "master";
|
|
106
|
+
}
|
|
107
|
+
function getMainRepoContext(options) {
|
|
108
|
+
const worktreePath = ensureWorktreePath(options.worktree);
|
|
109
|
+
const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath);
|
|
110
|
+
const repoRoot = getMainRepoRoot(worktreeRepoRoot);
|
|
111
|
+
const sourceBranch = options.worktree.branch;
|
|
112
|
+
if (!refExists(repoRoot, sourceBranch)) {
|
|
113
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.MISSING, "Worktree 分支不存在,可能已被手动删除。", {
|
|
114
|
+
cleanupDone: false,
|
|
115
|
+
conflict: false,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const targetBranch = options.targetBranch?.trim() || getDefaultBaseBranch(repoRoot);
|
|
119
|
+
if (!refExists(repoRoot, targetBranch)) {
|
|
120
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.TARGET_MISSING, `目标分支不存在:${targetBranch}`, {
|
|
121
|
+
cleanupDone: false,
|
|
122
|
+
conflict: false,
|
|
123
|
+
targetBranch,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return { repoRoot, worktreePath, sourceBranch, targetBranch };
|
|
127
|
+
}
|
|
128
|
+
function hasUncommittedChanges(worktreePath) {
|
|
129
|
+
const worktreeRepoRoot = getRepoRootFromWorktree(worktreePath);
|
|
130
|
+
return runGit(["status", "--porcelain"], worktreeRepoRoot).length > 0;
|
|
131
|
+
}
|
|
132
|
+
function getAheadCount(repoRoot, targetBranch, sourceBranch) {
|
|
133
|
+
const count = runGit(["rev-list", "--count", `${targetBranch}..${sourceBranch}`], repoRoot);
|
|
134
|
+
return Number.parseInt(count || "0", 10) || 0;
|
|
135
|
+
}
|
|
136
|
+
function checkConflicts(repoRoot, targetBranch, sourceBranch) {
|
|
137
|
+
try {
|
|
138
|
+
runGit(["merge-tree", targetBranch, sourceBranch], repoRoot);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function ensureMergeableContext(options) {
|
|
146
|
+
const context = getMainRepoContext(options);
|
|
147
|
+
if (hasUncommittedChanges(context.worktreePath)) {
|
|
148
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.DIRTY, "Worktree 中仍有未提交改动,请先提交后再合并。", {
|
|
149
|
+
sourceBranch: context.sourceBranch,
|
|
150
|
+
targetBranch: context.targetBranch,
|
|
151
|
+
repoRoot: context.repoRoot,
|
|
152
|
+
cleanupDone: false,
|
|
153
|
+
conflict: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch);
|
|
157
|
+
if (aheadCount <= 0) {
|
|
158
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.NOTHING_TO_MERGE, "当前 worktree 没有可合并到主分支的新提交。", {
|
|
159
|
+
sourceBranch: context.sourceBranch,
|
|
160
|
+
targetBranch: context.targetBranch,
|
|
161
|
+
repoRoot: context.repoRoot,
|
|
162
|
+
cleanupDone: false,
|
|
163
|
+
conflict: false,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return { ...context, aheadCount };
|
|
167
|
+
}
|
|
168
|
+
function buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts) {
|
|
169
|
+
const ok = !hasDirtyChanges && aheadCount > 0 && !hasConflicts;
|
|
170
|
+
return {
|
|
171
|
+
ok,
|
|
172
|
+
sourceBranch: context.sourceBranch,
|
|
173
|
+
targetBranch: context.targetBranch,
|
|
174
|
+
worktreePath: context.worktreePath,
|
|
175
|
+
repoRoot: context.repoRoot,
|
|
176
|
+
hasUncommittedChanges: hasDirtyChanges,
|
|
177
|
+
aheadCount,
|
|
178
|
+
hasConflicts,
|
|
179
|
+
recommendedAction: hasConflicts ? "resolve-conflict" : aheadCount > 0 ? "merge" : "noop",
|
|
180
|
+
reason: hasDirtyChanges
|
|
181
|
+
? "Worktree 中仍有未提交改动。"
|
|
182
|
+
: aheadCount <= 0
|
|
183
|
+
? "当前 worktree 没有可合并的新提交。"
|
|
184
|
+
: hasConflicts
|
|
185
|
+
? "检测到潜在冲突,请先处理。"
|
|
186
|
+
: undefined,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function getHeadCommit(repoRoot) {
|
|
190
|
+
return runGit(["rev-parse", "HEAD"], repoRoot);
|
|
191
|
+
}
|
|
192
|
+
function cleanupMergedWorktree(context) {
|
|
193
|
+
runGit(["worktree", "remove", context.worktreePath], context.repoRoot);
|
|
194
|
+
runGit(["branch", "-d", context.sourceBranch], context.repoRoot);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
export function getWorktreeMergeErrorCode(error) {
|
|
198
|
+
return error instanceof WorktreeMergeError ? error.code : undefined;
|
|
199
|
+
}
|
|
200
|
+
export function checkSessionWorktreeMergeability(options) {
|
|
201
|
+
const context = getMainRepoContext(options);
|
|
202
|
+
const hasDirtyChanges = hasUncommittedChanges(context.worktreePath);
|
|
203
|
+
const aheadCount = getAheadCount(context.repoRoot, context.targetBranch, context.sourceBranch);
|
|
204
|
+
const hasConflicts = !hasDirtyChanges && aheadCount > 0
|
|
205
|
+
? checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch)
|
|
206
|
+
: false;
|
|
207
|
+
return buildCheckResult(context, aheadCount, hasDirtyChanges, hasConflicts);
|
|
208
|
+
}
|
|
209
|
+
export function cleanupSessionWorktree(options) {
|
|
210
|
+
const context = getMainRepoContext({ worktree: options.worktree });
|
|
211
|
+
return cleanupMergedWorktree(context);
|
|
212
|
+
}
|
|
213
|
+
export function mergeSessionWorktree(options) {
|
|
214
|
+
const context = ensureMergeableContext(options);
|
|
215
|
+
const hasConflicts = checkConflicts(context.repoRoot, context.targetBranch, context.sourceBranch);
|
|
216
|
+
if (hasConflicts) {
|
|
217
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, "合并检测到冲突,请先手动处理。", {
|
|
218
|
+
sourceBranch: context.sourceBranch,
|
|
219
|
+
targetBranch: context.targetBranch,
|
|
220
|
+
repoRoot: context.repoRoot,
|
|
221
|
+
cleanupDone: false,
|
|
222
|
+
conflict: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
runGit(["checkout", context.targetBranch], context.repoRoot);
|
|
227
|
+
runGit(["merge", "--no-ff", "--no-edit", context.sourceBranch], context.repoRoot);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const message = getGitCommandMessage(error);
|
|
231
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CONFLICT, message || "合并失败,可能存在冲突。", {
|
|
232
|
+
sourceBranch: context.sourceBranch,
|
|
233
|
+
targetBranch: context.targetBranch,
|
|
234
|
+
repoRoot: context.repoRoot,
|
|
235
|
+
cleanupDone: false,
|
|
236
|
+
conflict: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const mergedAt = new Date().toISOString();
|
|
240
|
+
const mergeCommit = getHeadCommit(context.repoRoot);
|
|
241
|
+
try {
|
|
242
|
+
cleanupMergedWorktree(context);
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
sourceBranch: context.sourceBranch,
|
|
246
|
+
targetBranch: context.targetBranch,
|
|
247
|
+
repoRoot: context.repoRoot,
|
|
248
|
+
mergeCommit,
|
|
249
|
+
mergedAt,
|
|
250
|
+
cleanupDone: true,
|
|
251
|
+
conflict: false,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
throw new WorktreeMergeError(WORKTREE_MERGE_ERROR_CODES.CLEANUP_FAILED, getGitCommandMessage(error) || "已合并,但清理 worktree 失败。", {
|
|
256
|
+
sourceBranch: context.sourceBranch,
|
|
257
|
+
targetBranch: context.targetBranch,
|
|
258
|
+
repoRoot: context.repoRoot,
|
|
259
|
+
mergeCommit,
|
|
260
|
+
mergedAt,
|
|
261
|
+
cleanupDone: false,
|
|
262
|
+
conflict: false,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
}
|
|
22
266
|
export function prepareSessionWorktree(options) {
|
|
23
267
|
const resolvedCwd = path.resolve(options.cwd);
|
|
24
268
|
const repoRoot = runGit(["rev-parse", "--show-toplevel"], resolvedCwd);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import express from "express";
|
|
2
2
|
import { parseMessages } from "./message-parser.js";
|
|
3
3
|
import { SessionInputError } from "./process-manager.js";
|
|
4
|
+
import { checkSessionWorktreeMergeability, cleanupSessionWorktree, getWorktreeMergeErrorCode, mergeSessionWorktree, WorktreeMergeError } from "./git-worktree.js";
|
|
4
5
|
export function getErrorMessage(error, fallback) {
|
|
5
6
|
return error instanceof Error ? error.message : fallback;
|
|
6
7
|
}
|
|
@@ -70,6 +71,66 @@ function listAllSessions(processes, structured) {
|
|
|
70
71
|
return [...structured.list(), ...processes.list()]
|
|
71
72
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
72
73
|
}
|
|
74
|
+
function requireWorktreeSession(snapshot) {
|
|
75
|
+
if (!snapshot) {
|
|
76
|
+
throw new Error("未找到该会话。");
|
|
77
|
+
}
|
|
78
|
+
if (!snapshot.worktreeEnabled || !snapshot.worktree?.branch || !snapshot.worktree?.path) {
|
|
79
|
+
throw new Error("该会话未启用 worktree 模式。 ");
|
|
80
|
+
}
|
|
81
|
+
return snapshot;
|
|
82
|
+
}
|
|
83
|
+
function buildWorktreeMergeInfo(current, status, info) {
|
|
84
|
+
return {
|
|
85
|
+
...(current.worktreeMergeInfo ?? null),
|
|
86
|
+
...(info ?? null),
|
|
87
|
+
lastError: info?.lastError,
|
|
88
|
+
conflict: info?.conflict,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function saveWorktreeMergeState(storage, current, status, info) {
|
|
92
|
+
const mergedInfo = buildWorktreeMergeInfo(current, status, info);
|
|
93
|
+
const updated = {
|
|
94
|
+
...current,
|
|
95
|
+
worktreeMergeStatus: status,
|
|
96
|
+
worktreeMergeInfo: mergedInfo,
|
|
97
|
+
};
|
|
98
|
+
storage.saveSessionMetadata(updated);
|
|
99
|
+
return updated;
|
|
100
|
+
}
|
|
101
|
+
function getWorktreeMergeResponseStatus(error) {
|
|
102
|
+
const code = getWorktreeMergeErrorCode(error);
|
|
103
|
+
if (!code) {
|
|
104
|
+
return 400;
|
|
105
|
+
}
|
|
106
|
+
if (code === "WORKTREE_MERGE_CONFLICT") {
|
|
107
|
+
return 409;
|
|
108
|
+
}
|
|
109
|
+
return 400;
|
|
110
|
+
}
|
|
111
|
+
function getWorktreeMergePayload(error, fallback) {
|
|
112
|
+
if (error instanceof WorktreeMergeError) {
|
|
113
|
+
return {
|
|
114
|
+
error: error.message,
|
|
115
|
+
errorCode: error.code,
|
|
116
|
+
result: error.result ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
error: getErrorMessage(error, fallback),
|
|
121
|
+
errorCode: getWorktreeMergeErrorCode(error) ?? null,
|
|
122
|
+
result: null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function getLatestSessionSnapshot(processes, structured, storage, id) {
|
|
126
|
+
return getSessionById(processes, structured, id) ?? storage.getSession(id);
|
|
127
|
+
}
|
|
128
|
+
function canMergeSession(snapshot) {
|
|
129
|
+
return Boolean(snapshot.worktreeEnabled && snapshot.worktree?.branch && snapshot.worktree?.path);
|
|
130
|
+
}
|
|
131
|
+
function isMergeActionAllowed(snapshot) {
|
|
132
|
+
return snapshot.status !== "running";
|
|
133
|
+
}
|
|
73
134
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode) {
|
|
74
135
|
app.get("/api/sessions", (_req, res) => {
|
|
75
136
|
const all = listAllSessions(processes, structured);
|
|
@@ -117,6 +178,98 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
117
178
|
res.status(400).json({ error: getErrorMessage(error, "无法发送结构化消息。") });
|
|
118
179
|
}
|
|
119
180
|
});
|
|
181
|
+
app.post("/api/sessions/:id/worktree/merge/check", (req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
184
|
+
if (!isMergeActionAllowed(current)) {
|
|
185
|
+
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const checking = saveWorktreeMergeState(storage, current, "checking", {
|
|
189
|
+
...(current.worktreeMergeInfo ?? null),
|
|
190
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
191
|
+
lastError: undefined,
|
|
192
|
+
conflict: false,
|
|
193
|
+
});
|
|
194
|
+
const result = checkSessionWorktreeMergeability({
|
|
195
|
+
worktree: checking.worktree,
|
|
196
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
197
|
+
});
|
|
198
|
+
const nextStatus = result.ok ? "ready" : "failed";
|
|
199
|
+
const updated = saveWorktreeMergeState(storage, checking, nextStatus, {
|
|
200
|
+
targetBranch: result.targetBranch,
|
|
201
|
+
conflict: result.hasConflicts,
|
|
202
|
+
lastError: result.ok ? undefined : result.reason,
|
|
203
|
+
});
|
|
204
|
+
res.json({ session: updated, result });
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法检查 worktree 合并状态。"));
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
app.post("/api/sessions/:id/worktree/merge", express.json(), (req, res) => {
|
|
211
|
+
try {
|
|
212
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
213
|
+
if (!isMergeActionAllowed(current)) {
|
|
214
|
+
res.status(409).json({ error: "会话仍在运行,请结束后再合并。", errorCode: "SESSION_STILL_RUNNING" });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const merging = saveWorktreeMergeState(storage, current, "merging", {
|
|
218
|
+
...(current.worktreeMergeInfo ?? null),
|
|
219
|
+
lastError: undefined,
|
|
220
|
+
conflict: false,
|
|
221
|
+
});
|
|
222
|
+
const result = mergeSessionWorktree({
|
|
223
|
+
worktree: merging.worktree,
|
|
224
|
+
targetBranch: current.worktreeMergeInfo?.targetBranch,
|
|
225
|
+
});
|
|
226
|
+
const updated = saveWorktreeMergeState(storage, merging, "merged", {
|
|
227
|
+
targetBranch: result.targetBranch,
|
|
228
|
+
mergedAt: result.mergedAt,
|
|
229
|
+
mergeCommit: result.mergeCommit,
|
|
230
|
+
cleanupDone: result.cleanupDone,
|
|
231
|
+
lastError: undefined,
|
|
232
|
+
conflict: false,
|
|
233
|
+
});
|
|
234
|
+
res.json({ session: updated, result });
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const current = getLatestSessionSnapshot(processes, structured, storage, req.params.id);
|
|
238
|
+
if (current && canMergeSession(current)) {
|
|
239
|
+
const payload = getWorktreeMergePayload(error, "无法合并 worktree。");
|
|
240
|
+
saveWorktreeMergeState(storage, current, "failed", {
|
|
241
|
+
...(current.worktreeMergeInfo ?? null),
|
|
242
|
+
targetBranch: payload.result?.targetBranch ?? current.worktreeMergeInfo?.targetBranch,
|
|
243
|
+
mergedAt: payload.result?.mergedAt,
|
|
244
|
+
mergeCommit: payload.result?.mergeCommit,
|
|
245
|
+
cleanupDone: payload.result?.cleanupDone,
|
|
246
|
+
lastError: payload.error,
|
|
247
|
+
conflict: payload.result?.conflict === true,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法合并 worktree。"));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
app.post("/api/sessions/:id/worktree/cleanup", (req, res) => {
|
|
254
|
+
try {
|
|
255
|
+
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|
|
256
|
+
if (current.worktreeMergeStatus !== "merged" || current.worktreeMergeInfo?.cleanupDone !== false) {
|
|
257
|
+
res.status(400).json({ error: "当前 worktree 无需补偿清理。", errorCode: "WORKTREE_CLEANUP_NOT_NEEDED" });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
cleanupSessionWorktree({ worktree: current.worktree });
|
|
261
|
+
const updated = saveWorktreeMergeState(storage, current, "merged", {
|
|
262
|
+
...(current.worktreeMergeInfo ?? null),
|
|
263
|
+
cleanupDone: true,
|
|
264
|
+
lastError: undefined,
|
|
265
|
+
conflict: false,
|
|
266
|
+
});
|
|
267
|
+
res.json({ session: updated, ok: true });
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
res.status(getWorktreeMergeResponseStatus(error)).json(getWorktreeMergePayload(error, "无法清理 worktree。"));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
120
273
|
app.post("/api/sessions/batch-delete", express.json(), (req, res) => {
|
|
121
274
|
const sessionIds = Array.isArray(req.body?.sessionIds)
|
|
122
275
|
? req.body.sessionIds.filter((value) => typeof value === "string" && value.trim().length > 0)
|
package/dist/server.js
CHANGED
|
@@ -519,6 +519,40 @@ export async function startServer(config, configPath) {
|
|
|
519
519
|
changed = true;
|
|
520
520
|
}
|
|
521
521
|
}
|
|
522
|
+
if (body.uiPreferences && typeof body.uiPreferences === "object") {
|
|
523
|
+
const defaultPanelStateInput = body.uiPreferences.defaultPanelState;
|
|
524
|
+
if (defaultPanelStateInput && typeof defaultPanelStateInput === "object") {
|
|
525
|
+
const nextDefaultPanelState = {
|
|
526
|
+
...config.uiPreferences?.defaultPanelState,
|
|
527
|
+
};
|
|
528
|
+
let panelChanged = false;
|
|
529
|
+
const panelFields = [
|
|
530
|
+
"sessionsDrawerOpen",
|
|
531
|
+
"filePanelOpen",
|
|
532
|
+
"shortcutsExpanded",
|
|
533
|
+
"claudeHistoryExpanded",
|
|
534
|
+
"chatMessageExpanded",
|
|
535
|
+
"structuredThinkingExpanded",
|
|
536
|
+
"structuredToolGroupExpanded",
|
|
537
|
+
"structuredInlineToolExpanded",
|
|
538
|
+
"structuredTerminalExpanded",
|
|
539
|
+
"structuredToolCardExpanded"
|
|
540
|
+
];
|
|
541
|
+
for (const field of panelFields) {
|
|
542
|
+
if (field in defaultPanelStateInput && typeof defaultPanelStateInput[field] === "boolean") {
|
|
543
|
+
nextDefaultPanelState[field] = defaultPanelStateInput[field];
|
|
544
|
+
panelChanged = true;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (panelChanged) {
|
|
548
|
+
config.uiPreferences = {
|
|
549
|
+
...config.uiPreferences,
|
|
550
|
+
defaultPanelState: nextDefaultPanelState,
|
|
551
|
+
};
|
|
552
|
+
changed = true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
522
556
|
if (!changed) {
|
|
523
557
|
res.status(400).json({ error: "没有可更新的配置字段。" });
|
|
524
558
|
return;
|
package/dist/storage.d.ts
CHANGED
|
@@ -37,5 +37,6 @@ export declare class WandStorage {
|
|
|
37
37
|
getLatestSessionByClaudeSessionId(claudeSessionId: string): SessionSnapshot | null;
|
|
38
38
|
loadSessions(): SessionSnapshot[];
|
|
39
39
|
private mapSessionRow;
|
|
40
|
+
updateSessionWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
|
|
40
41
|
deleteSession(id: string): void;
|
|
41
42
|
}
|