@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 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) {
@@ -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 {};
@@ -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
  }