@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.
@@ -60,6 +60,335 @@
60
60
  var configPath = "${escapeHtml(configPath)}";
61
61
  var CHAT_EXPAND_STATE_STORAGE_KEY = "wand-chat-expand-state-v1";
62
62
  var CHAT_AUTO_FOLLOW_STORAGE_KEY = "wand-chat-auto-follow";
63
+ var DEFAULT_PANEL_STATE = {
64
+ sessionsDrawerOpen: false,
65
+ filePanelOpen: false,
66
+ shortcutsExpanded: false,
67
+ claudeHistoryExpanded: true,
68
+ chatMessageExpanded: true,
69
+ structuredThinkingExpanded: true,
70
+ structuredToolGroupExpanded: false,
71
+ structuredInlineToolExpanded: false,
72
+ structuredTerminalExpanded: false,
73
+ structuredToolCardExpanded: false,
74
+ };
75
+
76
+ function getConfiguredPanelDefaults(configOverride) {
77
+ var currentConfig = configOverride;
78
+ if (!currentConfig || typeof currentConfig !== "object") {
79
+ return {
80
+ sessionsDrawerOpen: DEFAULT_PANEL_STATE.sessionsDrawerOpen,
81
+ filePanelOpen: DEFAULT_PANEL_STATE.filePanelOpen,
82
+ shortcutsExpanded: DEFAULT_PANEL_STATE.shortcutsExpanded,
83
+ claudeHistoryExpanded: DEFAULT_PANEL_STATE.claudeHistoryExpanded,
84
+ chatMessageExpanded: DEFAULT_PANEL_STATE.chatMessageExpanded,
85
+ structuredThinkingExpanded: DEFAULT_PANEL_STATE.structuredThinkingExpanded,
86
+ structuredToolGroupExpanded: DEFAULT_PANEL_STATE.structuredToolGroupExpanded,
87
+ structuredInlineToolExpanded: DEFAULT_PANEL_STATE.structuredInlineToolExpanded,
88
+ structuredTerminalExpanded: DEFAULT_PANEL_STATE.structuredTerminalExpanded,
89
+ structuredToolCardExpanded: DEFAULT_PANEL_STATE.structuredToolCardExpanded,
90
+ };
91
+ }
92
+ var preferences = currentConfig.uiPreferences;
93
+ var configured = preferences && typeof preferences === "object" ? preferences.defaultPanelState : null;
94
+ return {
95
+ sessionsDrawerOpen: configured && typeof configured.sessionsDrawerOpen === "boolean" ? configured.sessionsDrawerOpen : DEFAULT_PANEL_STATE.sessionsDrawerOpen,
96
+ filePanelOpen: configured && typeof configured.filePanelOpen === "boolean" ? configured.filePanelOpen : DEFAULT_PANEL_STATE.filePanelOpen,
97
+ shortcutsExpanded: configured && typeof configured.shortcutsExpanded === "boolean" ? configured.shortcutsExpanded : DEFAULT_PANEL_STATE.shortcutsExpanded,
98
+ claudeHistoryExpanded: configured && typeof configured.claudeHistoryExpanded === "boolean" ? configured.claudeHistoryExpanded : DEFAULT_PANEL_STATE.claudeHistoryExpanded,
99
+ chatMessageExpanded: configured && typeof configured.chatMessageExpanded === "boolean" ? configured.chatMessageExpanded : DEFAULT_PANEL_STATE.chatMessageExpanded,
100
+ structuredThinkingExpanded: configured && typeof configured.structuredThinkingExpanded === "boolean" ? configured.structuredThinkingExpanded : DEFAULT_PANEL_STATE.structuredThinkingExpanded,
101
+ structuredToolGroupExpanded: configured && typeof configured.structuredToolGroupExpanded === "boolean" ? configured.structuredToolGroupExpanded : DEFAULT_PANEL_STATE.structuredToolGroupExpanded,
102
+ structuredInlineToolExpanded: configured && typeof configured.structuredInlineToolExpanded === "boolean" ? configured.structuredInlineToolExpanded : DEFAULT_PANEL_STATE.structuredInlineToolExpanded,
103
+ structuredTerminalExpanded: configured && typeof configured.structuredTerminalExpanded === "boolean" ? configured.structuredTerminalExpanded : DEFAULT_PANEL_STATE.structuredTerminalExpanded,
104
+ structuredToolCardExpanded: configured && typeof configured.structuredToolCardExpanded === "boolean" ? configured.structuredToolCardExpanded : DEFAULT_PANEL_STATE.structuredToolCardExpanded,
105
+ };
106
+ }
107
+
108
+ function getStoredBoolean(key) {
109
+ try {
110
+ var saved = localStorage.getItem(key);
111
+ if (saved === "true") return true;
112
+ if (saved === "false") return false;
113
+ return null;
114
+ } catch (e) {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function getInitialPanelBoolean(key, fieldName) {
120
+ var stored = getStoredBoolean(key);
121
+ if (stored !== null) return stored;
122
+ return DEFAULT_PANEL_STATE[fieldName];
123
+ }
124
+
125
+ function persistPanelBoolean(key, value) {
126
+ try {
127
+ localStorage.setItem(key, String(!!value));
128
+ } catch (e) {}
129
+ }
130
+
131
+ function applyConfiguredPanelDefaults() {
132
+ var defaults = getConfiguredPanelDefaults((typeof state !== "undefined" && state) ? state.config : null);
133
+ if (getStoredBoolean("wand-sessions-drawer-open") === null) {
134
+ state.sessionsDrawerOpen = defaults.sessionsDrawerOpen;
135
+ }
136
+ if (getStoredBoolean("wand-file-panel-open") === null) {
137
+ state.filePanelOpen = defaults.filePanelOpen;
138
+ }
139
+ if (getStoredBoolean("wand-shortcuts-expanded") === null) {
140
+ state.shortcutsExpanded = defaults.shortcutsExpanded;
141
+ }
142
+ if (getStoredBoolean("wand-claude-history-expanded") === null) {
143
+ state.claudeHistoryExpanded = defaults.claudeHistoryExpanded;
144
+ }
145
+ }
146
+
147
+ function getPanelStateSettingsFormValues() {
148
+ return {
149
+ sessionsDrawerOpen: !!((document.getElementById("cfg-panel-sessions-drawer") || {}).checked),
150
+ filePanelOpen: !!((document.getElementById("cfg-panel-file") || {}).checked),
151
+ shortcutsExpanded: !!((document.getElementById("cfg-panel-shortcuts") || {}).checked),
152
+ claudeHistoryExpanded: !!((document.getElementById("cfg-panel-history") || {}).checked),
153
+ chatMessageExpanded: !!((document.getElementById("cfg-panel-chat-message") || {}).checked),
154
+ structuredThinkingExpanded: !!((document.getElementById("cfg-panel-structured-thinking") || {}).checked),
155
+ structuredToolGroupExpanded: !!((document.getElementById("cfg-panel-structured-tool-group") || {}).checked),
156
+ structuredInlineToolExpanded: !!((document.getElementById("cfg-panel-structured-inline-tool") || {}).checked),
157
+ structuredTerminalExpanded: !!((document.getElementById("cfg-panel-structured-terminal") || {}).checked),
158
+ structuredToolCardExpanded: !!((document.getElementById("cfg-panel-structured-tool-card") || {}).checked),
159
+ };
160
+ }
161
+
162
+ function syncPanelStateSettingsForm(panelDefaults) {
163
+ var defaults = panelDefaults || getConfiguredPanelDefaults();
164
+ var sessionsDrawerEl = document.getElementById("cfg-panel-sessions-drawer");
165
+ var filePanelEl = document.getElementById("cfg-panel-file");
166
+ var shortcutsEl = document.getElementById("cfg-panel-shortcuts");
167
+ var historyEl = document.getElementById("cfg-panel-history");
168
+ var chatMessageEl = document.getElementById("cfg-panel-chat-message");
169
+ var structuredThinkingEl = document.getElementById("cfg-panel-structured-thinking");
170
+ var structuredToolGroupEl = document.getElementById("cfg-panel-structured-tool-group");
171
+ var structuredInlineToolEl = document.getElementById("cfg-panel-structured-inline-tool");
172
+ var structuredTerminalEl = document.getElementById("cfg-panel-structured-terminal");
173
+ var structuredToolCardEl = document.getElementById("cfg-panel-structured-tool-card");
174
+ if (sessionsDrawerEl) sessionsDrawerEl.checked = !!defaults.sessionsDrawerOpen;
175
+ if (filePanelEl) filePanelEl.checked = !!defaults.filePanelOpen;
176
+ if (shortcutsEl) shortcutsEl.checked = !!defaults.shortcutsExpanded;
177
+ if (historyEl) historyEl.checked = !!defaults.claudeHistoryExpanded;
178
+ if (chatMessageEl) chatMessageEl.checked = !!defaults.chatMessageExpanded;
179
+ if (structuredThinkingEl) structuredThinkingEl.checked = !!defaults.structuredThinkingExpanded;
180
+ if (structuredToolGroupEl) structuredToolGroupEl.checked = !!defaults.structuredToolGroupExpanded;
181
+ if (structuredInlineToolEl) structuredInlineToolEl.checked = !!defaults.structuredInlineToolExpanded;
182
+ if (structuredTerminalEl) structuredTerminalEl.checked = !!defaults.structuredTerminalExpanded;
183
+ if (structuredToolCardEl) structuredToolCardEl.checked = !!defaults.structuredToolCardExpanded;
184
+ }
185
+
186
+ function applySettingsConfig(config) {
187
+ state.config = config || null;
188
+ applyConfiguredPanelDefaults();
189
+ updatePanelDefaultControls();
190
+ }
191
+
192
+ function renderSettingsNav() {
193
+ return '<div class="settings-nav">' +
194
+ '<button class="settings-tab active" data-tab="about">关于</button>' +
195
+ '<button class="settings-tab" data-tab="general">基本配置</button>' +
196
+ '<button class="settings-tab" data-tab="security">安全</button>' +
197
+ '<button class="settings-tab" data-tab="presets">命令预设</button>' +
198
+ '</div>';
199
+ }
200
+
201
+ function renderAppearanceSettingsCard() {
202
+ return '<div class="settings-card settings-card-accent">' +
203
+ '<div class="settings-card-header">' +
204
+ '<div>' +
205
+ '<h3 class="settings-section-title">界面偏好</h3>' +
206
+ '<p class="settings-hint">这些选项决定新页面或未保存本地偏好的默认展开状态。</p>' +
207
+ '</div>' +
208
+ '</div>' +
209
+ '<div class="settings-toggle-list">' +
210
+ '<label class="settings-toggle-item" for="cfg-panel-sessions-drawer">' +
211
+ '<div><span class="settings-toggle-title">默认展开会话侧栏</span><span class="settings-toggle-desc">进入页面时左侧会话列表默认展开。</span></div>' +
212
+ '<input id="cfg-panel-sessions-drawer" type="checkbox" class="field-checkbox" />' +
213
+ '</label>' +
214
+ '<label class="settings-toggle-item" for="cfg-panel-file">' +
215
+ '<div><span class="settings-toggle-title">默认展开文件面板</span><span class="settings-toggle-desc">右侧文件浏览器在初始状态下打开。</span></div>' +
216
+ '<input id="cfg-panel-file" type="checkbox" class="field-checkbox" />' +
217
+ '</label>' +
218
+ '<label class="settings-toggle-item" for="cfg-panel-shortcuts">' +
219
+ '<div><span class="settings-toggle-title">默认展开快捷键栏</span><span class="settings-toggle-desc">移动端快捷键面板首次显示时展开完整行。</span></div>' +
220
+ '<input id="cfg-panel-shortcuts" type="checkbox" class="field-checkbox" />' +
221
+ '</label>' +
222
+ '<label class="settings-toggle-item" for="cfg-panel-history">' +
223
+ '<div><span class="settings-toggle-title">默认展开 Claude 历史</span><span class="settings-toggle-desc">侧栏里的 Claude 历史分组默认展开。</span></div>' +
224
+ '<input id="cfg-panel-history" type="checkbox" class="field-checkbox" />' +
225
+ '</label>' +
226
+ '<label class="settings-toggle-item" for="cfg-panel-chat-message">' +
227
+ '<div><span class="settings-toggle-title">默认展开聊天详情</span><span class="settings-toggle-desc">当某条消息没有本地展开记录时,采用这里的默认值。</span></div>' +
228
+ '<input id="cfg-panel-chat-message" type="checkbox" class="field-checkbox" />' +
229
+ '</label>' +
230
+ '<label class="settings-toggle-item" for="cfg-panel-structured-thinking">' +
231
+ '<div><span class="settings-toggle-title">默认展开思考卡片</span><span class="settings-toggle-desc">结构化模式中的 thinking 块默认展开。</span></div>' +
232
+ '<input id="cfg-panel-structured-thinking" type="checkbox" class="field-checkbox" />' +
233
+ '</label>' +
234
+ '<label class="settings-toggle-item" for="cfg-panel-structured-tool-group">' +
235
+ '<div><span class="settings-toggle-title">默认展开工具组</span><span class="settings-toggle-desc">连续工具调用合并后的工具组默认展开。</span></div>' +
236
+ '<input id="cfg-panel-structured-tool-group" type="checkbox" class="field-checkbox" />' +
237
+ '</label>' +
238
+ '<label class="settings-toggle-item" for="cfg-panel-structured-inline-tool">' +
239
+ '<div><span class="settings-toggle-title">默认展开内联工具</span><span class="settings-toggle-desc">Read、Grep、Glob 等内联工具结果默认展开。</span></div>' +
240
+ '<input id="cfg-panel-structured-inline-tool" type="checkbox" class="field-checkbox" />' +
241
+ '</label>' +
242
+ '<label class="settings-toggle-item" for="cfg-panel-structured-terminal">' +
243
+ '<div><span class="settings-toggle-title">默认展开终端卡片</span><span class="settings-toggle-desc">Bash 等终端输出卡片默认展开。</span></div>' +
244
+ '<input id="cfg-panel-structured-terminal" type="checkbox" class="field-checkbox" />' +
245
+ '</label>' +
246
+ '<label class="settings-toggle-item" for="cfg-panel-structured-tool-card">' +
247
+ '<div><span class="settings-toggle-title">默认展开通用工具卡</span><span class="settings-toggle-desc">工具调用、计划类卡片等通用卡片默认展开。</span></div>' +
248
+ '<input id="cfg-panel-structured-tool-card" type="checkbox" class="field-checkbox" />' +
249
+ '</label>' +
250
+ '</div>' +
251
+ '</div>';
252
+ }
253
+
254
+ function buildSettingsGeneralPanel() {
255
+ return '<div class="settings-panel" id="settings-tab-general">' +
256
+ '<div class="settings-card settings-card-accent">' +
257
+ '<div class="settings-card-header">' +
258
+ '<div>' +
259
+ '<h3 class="settings-section-title">基础运行配置</h3>' +
260
+ '<p class="settings-hint">影响服务器监听、默认模式和 CLI 行为。</p>' +
261
+ '</div>' +
262
+ '</div>' +
263
+ '<div class="field-row">' +
264
+ '<div class="field">' +
265
+ '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
266
+ '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
267
+ '</div>' +
268
+ '<div class="field">' +
269
+ '<label class="field-label" for="cfg-port">端口 (port)</label>' +
270
+ '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
271
+ '</div>' +
272
+ '</div>' +
273
+ '<div class="field field-inline">' +
274
+ '<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
275
+ '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
276
+ '</div>' +
277
+ '<div class="field-row">' +
278
+ '<div class="field">' +
279
+ '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
280
+ '<select id="cfg-mode" class="field-input">' +
281
+ '<option value="default">default</option>' +
282
+ '<option value="assist">assist</option>' +
283
+ '<option value="agent">agent</option>' +
284
+ '<option value="agent-max">agent-max</option>' +
285
+ '<option value="auto-edit">auto-edit</option>' +
286
+ '<option value="full-access">full-access</option>' +
287
+ '<option value="native">native</option>' +
288
+ '<option value="managed">managed</option>' +
289
+ '</select>' +
290
+ '</div>' +
291
+ '<div class="field">' +
292
+ '<label class="field-label" for="cfg-language">回复语言</label>' +
293
+ '<select id="cfg-language" class="field-input">' +
294
+ '<option value="">自动(不指定)</option>' +
295
+ '<option value="中文">中文</option>' +
296
+ '<option value="English">English</option>' +
297
+ '<option value="日本語">日本語</option>' +
298
+ '<option value="한국어">한국어</option>' +
299
+ '<option value="Español">Español</option>' +
300
+ '<option value="Français">Français</option>' +
301
+ '<option value="Deutsch">Deutsch</option>' +
302
+ '<option value="Русский">Русский</option>' +
303
+ '</select>' +
304
+ '</div>' +
305
+ '</div>' +
306
+ '<p class="field-hint settings-inline-hint">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
307
+ '<div class="field">' +
308
+ '<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
309
+ '<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
310
+ '</div>' +
311
+ '<div class="field">' +
312
+ '<label class="field-label" for="cfg-shell">Shell</label>' +
313
+ '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
314
+ '</div>' +
315
+ '</div>' +
316
+ renderAppearanceSettingsCard() +
317
+ '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
318
+ '<p id="config-message" class="hint hidden"></p>' +
319
+ '</div>';
320
+ }
321
+
322
+ function persistSettingsBackedUiState() {
323
+ persistPanelBoolean("wand-sessions-drawer-open", state.sessionsDrawerOpen);
324
+ persistPanelBoolean("wand-file-panel-open", state.filePanelOpen);
325
+ persistPanelBoolean("wand-shortcuts-expanded", state.shortcutsExpanded);
326
+ persistPanelBoolean("wand-claude-history-expanded", state.claudeHistoryExpanded);
327
+ }
328
+
329
+ function resetSettingsBackedUiStateToDefaults() {
330
+ var defaults = getConfiguredPanelDefaults();
331
+ state.sessionsDrawerOpen = defaults.sessionsDrawerOpen;
332
+ state.filePanelOpen = defaults.filePanelOpen;
333
+ state.shortcutsExpanded = defaults.shortcutsExpanded;
334
+ state.claudeHistoryExpanded = defaults.claudeHistoryExpanded;
335
+ persistSettingsBackedUiState();
336
+ }
337
+
338
+ function handleSettingsConfigSaved(nextConfig) {
339
+ applySettingsConfig(nextConfig || state.config);
340
+ syncPanelStateSettingsForm();
341
+ resetSettingsBackedUiStateToDefaults();
342
+ updateLayoutState();
343
+ updateSessionsList();
344
+ updateCurrentSession();
345
+ }
346
+
347
+ function updateSettingsActiveNav() {
348
+ var activeTab = document.querySelector(".settings-tab.active");
349
+ var nav = document.querySelector(".settings-nav");
350
+ if (!nav) return;
351
+ if (activeTab) {
352
+ nav.setAttribute("data-active-tab", activeTab.getAttribute("data-tab") || "");
353
+ }
354
+ }
355
+
356
+ function updateCollapsedShortcutsUi() {
357
+ var wrap = document.querySelector(".inline-shortcuts-wrap");
358
+ var row = document.querySelector(".inline-shortcuts-expanded-row");
359
+ var toggle = document.querySelector(".shortcuts-toggle");
360
+ if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
361
+ if (row) row.classList.toggle("visible", state.shortcutsExpanded);
362
+ if (toggle) {
363
+ toggle.classList.toggle("active", state.shortcutsExpanded);
364
+ toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
365
+ }
366
+ }
367
+
368
+ function updatePanelDefaultControls() {
369
+ syncPanelStateSettingsForm();
370
+ }
371
+
372
+ function persistDrawerState() {
373
+ persistPanelBoolean("wand-sessions-drawer-open", state.sessionsDrawerOpen);
374
+ }
375
+
376
+ function persistHistoryPanelState() {
377
+ persistPanelBoolean("wand-claude-history-expanded", state.claudeHistoryExpanded);
378
+ }
379
+
380
+ function persistShortcutsExpandedState() {
381
+ persistPanelBoolean("wand-shortcuts-expanded", state.shortcutsExpanded);
382
+ }
383
+
384
+ function persistFilePanelState() {
385
+ persistPanelBoolean("wand-file-panel-open", state.filePanelOpen);
386
+ }
387
+
388
+ function refreshUiAfterPanelStateChange() {
389
+ updateLayoutState();
390
+ updateSessionsList();
391
+ }
63
392
 
64
393
  var state = {
65
394
  selectedId: (function() {
@@ -108,7 +437,7 @@
108
437
  loginPending: false,
109
438
  loginChecked: false,
110
439
  bootstrapping: true,
111
- sessionsDrawerOpen: false,
440
+ sessionsDrawerOpen: getInitialPanelBoolean("wand-sessions-drawer-open", "sessionsDrawerOpen"),
112
441
  modalOpen: false,
113
442
  presetValue: "",
114
443
  cwdValue: "",
@@ -117,6 +446,11 @@
117
446
  sessionCreateKind: "structured",
118
447
  sessionCreateWorktree: false,
119
448
  sessionTool: "claude",
449
+ activeWorktreeMergeSessionId: null,
450
+ worktreeMergeCheckResult: null,
451
+ worktreeMergeLoading: false,
452
+ worktreeMergeSubmitting: false,
453
+ worktreeMergeError: "",
120
454
  preferredCommand: "claude",
121
455
  structuredRunner: "claude-cli-print",
122
456
  lastResize: { cols: 0, rows: 0 },
@@ -136,13 +470,7 @@
136
470
  })(),
137
471
  terminalBaseFontSize: 13,
138
472
  keyboardPopupOpen: false,
139
- filePanelOpen: (function() {
140
- try {
141
- return localStorage.getItem("wand-file-panel-open") === "true";
142
- } catch (e) {
143
- return false;
144
- }
145
- })(),
473
+ filePanelOpen: getInitialPanelBoolean("wand-file-panel-open", "filePanelOpen"),
146
474
  chatAutoFollow: (function() {
147
475
  try {
148
476
  var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
@@ -166,14 +494,14 @@
166
494
  currentTask: null, // Current task title from Claude
167
495
  terminalInteractive: false,
168
496
  miniKeyboardVisible: false,
169
- shortcutsExpanded: false,
497
+ shortcutsExpanded: getInitialPanelBoolean("wand-shortcuts-expanded", "shortcutsExpanded"),
170
498
  modifiers: { ctrl: false, alt: false, shift: false },
171
499
  fileSearchQuery: "",
172
500
  fileExplorerLoading: false,
173
501
  allFiles: [],
174
502
  claudeHistory: [],
175
503
  claudeHistoryLoaded: false,
176
- claudeHistoryExpanded: true,
504
+ claudeHistoryExpanded: getInitialPanelBoolean("wand-claude-history-expanded", "claudeHistoryExpanded"),
177
505
  claudeHistoryExpandedDirs: {},
178
506
  sessionsManageMode: false,
179
507
  selectedSessionIds: {},
@@ -535,6 +863,56 @@
535
863
  }
536
864
  }
537
865
 
866
+ function getDefaultChatMessageExpanded() {
867
+ return getConfiguredPanelDefaults().chatMessageExpanded;
868
+ }
869
+
870
+ function getDefaultStructuredCardExpanded(cardType, fallbackValue) {
871
+ var defaults = getConfiguredPanelDefaults();
872
+ if (cardType === "thinking") {
873
+ return defaults.structuredThinkingExpanded;
874
+ }
875
+ if (cardType === "tool-group") {
876
+ return defaults.structuredToolGroupExpanded;
877
+ }
878
+ if (cardType === "inline-tool") {
879
+ return defaults.structuredInlineToolExpanded;
880
+ }
881
+ if (cardType === "terminal") {
882
+ return defaults.structuredTerminalExpanded;
883
+ }
884
+ if (cardType === "tool-card") {
885
+ return defaults.structuredToolCardExpanded;
886
+ }
887
+ if (typeof fallbackValue === "boolean") {
888
+ return fallbackValue;
889
+ }
890
+ return defaults.chatMessageExpanded;
891
+ }
892
+
893
+ function hasPersistedExpandState(itemKey) {
894
+ if (!itemKey || !state.selectedId) return false;
895
+ var sessionState = getCurrentChatExpandState();
896
+ return typeof sessionState[itemKey] === "boolean";
897
+ }
898
+
899
+ function getExpandState(itemKey, cardType, fallbackValue) {
900
+ if (!itemKey || !state.selectedId) {
901
+ if (typeof fallbackValue === "boolean") return fallbackValue;
902
+ return getDefaultStructuredCardExpanded(cardType, fallbackValue);
903
+ }
904
+ var sessionState = getCurrentChatExpandState();
905
+ if (typeof sessionState[itemKey] === "boolean") return sessionState[itemKey];
906
+ return getDefaultStructuredCardExpanded(cardType, fallbackValue);
907
+ }
908
+
909
+ function getPersistedExpandState(itemKey) {
910
+ if (!itemKey || !state.selectedId) return null;
911
+ var sessionState = getCurrentChatExpandState();
912
+ if (typeof sessionState[itemKey] === "boolean") return sessionState[itemKey];
913
+ return null;
914
+ }
915
+
538
916
  function saveChatExpandStateMap(map) {
539
917
  try {
540
918
  if (!map || Object.keys(map).length === 0) {
@@ -555,12 +933,6 @@
555
933
  return sessionState && typeof sessionState === "object" ? sessionState : {};
556
934
  }
557
935
 
558
- function getPersistedExpandState(itemKey) {
559
- if (!itemKey || !state.selectedId) return null;
560
- var sessionState = getCurrentChatExpandState();
561
- return typeof sessionState[itemKey] === "boolean" ? sessionState[itemKey] : null;
562
- }
563
-
564
936
  function setPersistedExpandState(itemKey, expanded) {
565
937
  if (!itemKey || !state.selectedId) return;
566
938
  var map = loadChatExpandStateMap();
@@ -677,9 +1049,8 @@
677
1049
  container.querySelectorAll("[data-expand-key]").forEach(function(el) {
678
1050
  var itemKey = getElementExpandKey(el);
679
1051
  var kind = el.dataset.expandKind || "";
680
- var persisted = getPersistedExpandState(itemKey);
681
- if (persisted === null || !kind) return;
682
- applyExpandedState(el, kind, persisted);
1052
+ if (!kind || !hasPersistedExpandState(itemKey)) return;
1053
+ applyExpandedState(el, kind, getPersistedExpandState(itemKey));
683
1054
  });
684
1055
  }
685
1056
 
@@ -822,7 +1193,7 @@
822
1193
  })
823
1194
  .then(function(config) {
824
1195
  if (!config) return;
825
- state.config = config;
1196
+ applySettingsConfig(config);
826
1197
  state.loginChecked = true;
827
1198
  requestAnimationFrame(function() {
828
1199
  try {
@@ -1042,12 +1413,9 @@
1042
1413
  var preferredTool = getComposerTool();
1043
1414
  var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
1044
1415
 
1416
+ var showTerminalHeaderControls = !!selectedSession && state.currentView === "terminal";
1417
+ var showChatHeaderControls = !!selectedSession && state.currentView !== "terminal";
1045
1418
  return '<div class="app-container">' +
1046
- '<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="Toggle sidebar">' +
1047
- '<span class="hamburger-icon">' +
1048
- '<span></span><span></span><span></span>' +
1049
- '</span>' +
1050
- '</button>' +
1051
1419
  '<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
1052
1420
  '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
1053
1421
  '<aside id="sessions-drawer" class="sidebar' + drawerClass + '">' +
@@ -1089,11 +1457,37 @@
1089
1457
  '</div>' +
1090
1458
  '</aside>' +
1091
1459
  '<main class="main-content">' +
1092
- '<span class="current-task hidden" id="current-task"></span>' +
1093
- '<div class="view-toggle-bar' + (state.selectedId ? '' : ' hidden') + '" id="view-toggle-bar">' +
1094
- '<button id="view-terminal-btn" class="topbar-btn' + (state.currentView === "terminal" ? ' active' : '') + '" type="button" title="查看原始终端输出">终端</button>' +
1095
- '<button id="view-chat-btn" class="topbar-btn' + (state.currentView !== "terminal" ? ' active' : '') + '" type="button" title="查看聊天解析视图">聊天</button>' +
1460
+ '<div class="main-content-header">' +
1461
+ '<div class="main-content-header-left">' +
1462
+ '<button id="sessions-toggle-button" class="main-header-btn menu-toggle-btn' + (state.sessionsDrawerOpen ? ' active' : '') + '" type="button" aria-label="打开菜单" title="菜单">' +
1463
+ '<span class="hamburger-icon">' +
1464
+ '<span></span><span></span><span></span>' +
1465
+ '</span>' +
1466
+ '</button>' +
1467
+ '<span class="current-task hidden" id="current-task"></span>' +
1468
+ '</div>' +
1469
+ '<div class="main-content-header-center">' +
1470
+ '<div class="view-toggle-bar' + (state.selectedId ? '' : ' hidden') + '" id="view-toggle-bar">' +
1471
+ '<button id="view-terminal-btn" class="topbar-btn' + (state.currentView === "terminal" ? ' active' : '') + '" type="button" title="查看原始终端输出">终端</button>' +
1472
+ '<button id="view-chat-btn" class="topbar-btn' + (state.currentView !== "terminal" ? ' active' : '') + '" type="button" title="查看聊天解析视图">聊天</button>' +
1473
+ '</div>' +
1474
+ '</div>' +
1475
+ '<div class="main-content-header-right">' +
1476
+ '<div class="main-header-controls' + (showTerminalHeaderControls ? '' : ' hidden') + '" id="terminal-header-controls">' +
1477
+ '<button id="terminal-scale-down-top" class="main-header-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
1478
+ '<span class="main-header-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
1479
+ '<button id="terminal-scale-up-top" class="main-header-btn terminal-scale-btn" type="button" title="放大">+</button>' +
1480
+ '<button id="page-refresh-btn" class="main-header-btn" type="button" title="刷新页面">↻</button>' +
1481
+ '<button id="terminal-jump-bottom" class="main-header-btn jump-latest-btn' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
1482
+ '</div>' +
1483
+ '<div class="main-header-controls' + (showChatHeaderControls ? '' : ' hidden') + '" id="chat-header-controls">' +
1484
+ '<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '" title="' + (state.chatAutoFollow ? '追踪底部:开启' : '追踪底部:已暂停') + '">' + (state.chatAutoFollow ? '追底' : '暂停') + '</button>' +
1485
+ '<button id="chat-jump-bottom" class="main-header-btn jump-latest-btn' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
1486
+ '</div>' +
1487
+ '<button id="topbar-new-session-button" class="main-header-btn main-header-new-session" type="button" title="新对话">+ 新对话</button>' +
1488
+ '</div>' +
1096
1489
  '</div>' +
1490
+ '<div class="main-content-body">' +
1097
1491
  // File panel backdrop (mobile)
1098
1492
  '<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
1099
1493
  // File side panel
@@ -1114,22 +1508,8 @@
1114
1508
  '<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</div>' +
1115
1509
  '</div>' +
1116
1510
  '</div>' +
1117
- '<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
1118
- '<div class="terminal-scale-overlay" aria-label="终端缩放控件">' +
1119
- '<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
1120
- '<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
1121
- '<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
1122
- '<span class="terminal-scale-overlay-divider"></span>' +
1123
- '<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
1124
- '</div>' +
1125
- '<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
1126
- '</div>' +
1127
- '<div id="chat-output" class="chat-container hidden">' +
1128
- '<div class="chat-overlay-controls">' +
1129
- '<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '" title="' + (state.chatAutoFollow ? '追踪底部:开启' : '追踪底部:已暂停') + '">' + (state.chatAutoFollow ? '追底' : '暂停') + '</button>' +
1130
- '</div>' +
1131
- '<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
1132
- '</div>' +
1511
+ '<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active"></div>' +
1512
+ '<div id="chat-output" class="chat-container hidden"></div>' +
1133
1513
  '<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
1134
1514
  '<div class="blank-chat-inner">' +
1135
1515
  '<div class="blank-chat-logo">W</div>' +
@@ -1239,7 +1619,29 @@
1239
1619
  '</section>' +
1240
1620
  '</main>' +
1241
1621
  '</div>' +
1242
- '</div>' + renderSessionModal() + renderSettingsModal();
1622
+ '</div>' + renderSessionModal() + renderWorktreeMergeModal() + renderSettingsModal();
1623
+ }
1624
+
1625
+ function renderWorktreeMergeModal() {
1626
+ return '<section id="worktree-merge-modal" class="modal-backdrop hidden">' +
1627
+ '<div class="modal worktree-merge-modal">' +
1628
+ '<div class="modal-header">' +
1629
+ '<div>' +
1630
+ '<h2 class="modal-title">合并 Worktree</h2>' +
1631
+ '<p class="modal-subtitle">检查当前任务分支并快捷合并到主分支。</p>' +
1632
+ '</div>' +
1633
+ '<button id="close-worktree-merge-button" class="btn btn-ghost btn-icon">&times;</button>' +
1634
+ '</div>' +
1635
+ '<div class="modal-body">' +
1636
+ '<div id="worktree-merge-content" class="worktree-merge-content"></div>' +
1637
+ '<p id="worktree-merge-error" class="error-message hidden"></p>' +
1638
+ '<div class="worktree-merge-actions">' +
1639
+ '<button id="worktree-merge-cancel-button" class="btn btn-secondary">取消</button>' +
1640
+ '<button id="worktree-merge-confirm-button" class="btn btn-primary">确认合并并清理</button>' +
1641
+ '</div>' +
1642
+ '</div>' +
1643
+ '</div>' +
1644
+ '</section>';
1243
1645
  }
1244
1646
 
1245
1647
  function renderSettingsModal() {
@@ -1249,16 +1651,9 @@
1249
1651
  '<h2 class="modal-title">设置</h2>' +
1250
1652
  '<button id="close-settings-button" class="btn btn-ghost btn-icon">×</button>' +
1251
1653
  '</div>' +
1252
- '<div class="modal-body">' +
1253
- // Tabs
1254
- '<div class="settings-tabs">' +
1255
- '<button class="settings-tab active" data-tab="about">关于</button>' +
1256
- '<button class="settings-tab" data-tab="general">基本配置</button>' +
1257
- '<button class="settings-tab" data-tab="security">安全</button>' +
1258
- '<button class="settings-tab" data-tab="presets">命令预设</button>' +
1259
- '</div>' +
1260
-
1261
- // About tab
1654
+ '<div class="modal-body settings-layout">' +
1655
+ renderSettingsNav() +
1656
+ '<div class="settings-content">' +
1262
1657
  '<div class="settings-panel active" id="settings-tab-about">' +
1263
1658
  '<div class="settings-about-info">' +
1264
1659
  '<div class="settings-about-row"><span class="settings-label">包名</span><span class="settings-value" id="settings-pkg-name">-</span></div>' +
@@ -1291,63 +1686,7 @@
1291
1686
  '</div>' +
1292
1687
  '</div>' +
1293
1688
 
1294
- // General config tab
1295
- '<div class="settings-panel" id="settings-tab-general">' +
1296
- '<div class="field-row">' +
1297
- '<div class="field">' +
1298
- '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
1299
- '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
1300
- '</div>' +
1301
- '<div class="field">' +
1302
- '<label class="field-label" for="cfg-port">端口 (port)</label>' +
1303
- '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
1304
- '</div>' +
1305
- '</div>' +
1306
- '<div class="field field-inline">' +
1307
- '<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
1308
- '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
1309
- '</div>' +
1310
- '<div class="field-row">' +
1311
- '<div class="field">' +
1312
- '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
1313
- '<select id="cfg-mode" class="field-input">' +
1314
- '<option value="default">default</option>' +
1315
- '<option value="assist">assist</option>' +
1316
- '<option value="agent">agent</option>' +
1317
- '<option value="agent-max">agent-max</option>' +
1318
- '<option value="auto-edit">auto-edit</option>' +
1319
- '<option value="full-access">full-access</option>' +
1320
- '<option value="native">native</option>' +
1321
- '<option value="managed">managed</option>' +
1322
- '</select>' +
1323
- '</div>' +
1324
- '<div class="field">' +
1325
- '<label class="field-label" for="cfg-language">回复语言</label>' +
1326
- '<select id="cfg-language" class="field-input">' +
1327
- '<option value="">自动(不指定)</option>' +
1328
- '<option value="中文">中文</option>' +
1329
- '<option value="English">English</option>' +
1330
- '<option value="日本語">日本語</option>' +
1331
- '<option value="한국어">한국어</option>' +
1332
- '<option value="Español">Español</option>' +
1333
- '<option value="Français">Français</option>' +
1334
- '<option value="Deutsch">Deutsch</option>' +
1335
- '<option value="Русский">Русский</option>' +
1336
- '</select>' +
1337
- '</div>' +
1338
- '</div>' +
1339
- '<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
1340
- '<div class="field">' +
1341
- '<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
1342
- '<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
1343
- '</div>' +
1344
- '<div class="field">' +
1345
- '<label class="field-label" for="cfg-shell">Shell</label>' +
1346
- '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
1347
- '</div>' +
1348
- '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
1349
- '<p id="config-message" class="hint hidden"></p>' +
1350
- '</div>' +
1689
+ buildSettingsGeneralPanel() +
1351
1690
 
1352
1691
  // Security tab
1353
1692
  '<div class="settings-panel" id="settings-tab-security">' +
@@ -1385,6 +1724,7 @@
1385
1724
  '<div class="settings-panel" id="settings-tab-presets">' +
1386
1725
  '<div id="presets-list" class="presets-list"></div>' +
1387
1726
  '</div>' +
1727
+ '</div>' +
1388
1728
  '</div>' +
1389
1729
  '</div>' +
1390
1730
  '</section>';
@@ -1747,9 +2087,7 @@
1747
2087
 
1748
2088
  function setFilePanelOpen(nextOpen) {
1749
2089
  state.filePanelOpen = nextOpen;
1750
- try {
1751
- localStorage.setItem("wand-file-panel-open", String(state.filePanelOpen));
1752
- } catch (e) {}
2090
+ persistFilePanelState();
1753
2091
  if (state.filePanelOpen && isMobileLayout()) {
1754
2092
  state.sessionsDrawerOpen = false;
1755
2093
  }
@@ -2296,9 +2634,18 @@
2296
2634
  recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
2297
2635
  }
2298
2636
 
2637
+ var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
2638
+ var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
2639
+ var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
2640
+ var mergeTitle = needsCleanup ? "重试清理 worktree" : "合并到主分支";
2641
+ var mergeButton = canOpenMerge && session.worktreeMergeStatus !== "merged"
2642
+ ? '<button class="session-action-btn merge-btn" data-action="worktree-merge" data-session-id="' + session.id + '" type="button" aria-label="' + escapeHtml(mergeTitle) + '" title="' + escapeHtml(mergeTitle) + '"' + (mergeDisabled ? ' disabled' : '') + '><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/><path d="M5 7l-2 2 2 2"/><path d="M19 15l2 2-2 2"/></svg></button>'
2643
+ : needsCleanup
2644
+ ? '<button class="session-action-btn merge-btn" data-action="worktree-cleanup" data-session-id="' + session.id + '" type="button" aria-label="重试清理 worktree" title="重试清理 worktree"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>'
2645
+ : "";
2299
2646
  var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
2300
2647
  var modeBadge = renderSessionKindBadge(session);
2301
- var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
2648
+ var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
2302
2649
 
2303
2650
  return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
2304
2651
  '<div class="session-item-content">' +
@@ -2322,12 +2669,35 @@
2322
2669
  '</div>';
2323
2670
  }
2324
2671
 
2672
+ function getWorktreeMergeStatusLabel(session) {
2673
+ if (!session || !session.worktreeMergeStatus) return "";
2674
+ var labels = {
2675
+ ready: "可合并",
2676
+ checking: "检查中",
2677
+ merging: "合并中",
2678
+ merged: session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false ? "已合并待清理" : "已合并",
2679
+ failed: "合并失败"
2680
+ };
2681
+ return labels[session.worktreeMergeStatus] || "";
2682
+ }
2683
+
2684
+ function renderWorktreeMergeBadge(session) {
2685
+ var label = getWorktreeMergeStatusLabel(session);
2686
+ if (!label) return "";
2687
+ return '<span class="session-kind-badge worktree-merge ' + escapeHtml(session.worktreeMergeStatus || "") + '">' + escapeHtml(label) + '</span>';
2688
+ }
2689
+
2325
2690
  function renderWorktreeBadge(session) {
2326
2691
  if (!session || !session.worktreeEnabled) return "";
2327
- var title = session.worktree && session.worktree.branch
2328
- ? ' title="' + escapeHtml('Worktree: ' + session.worktree.branch) + '"'
2329
- : '';
2330
- return '<span class="session-kind-badge worktree"' + title + '>Worktree</span>';
2692
+ var titleParts = [];
2693
+ if (session.worktree && session.worktree.branch) {
2694
+ titleParts.push('Worktree: ' + session.worktree.branch);
2695
+ }
2696
+ if (session.worktree && session.worktree.path) {
2697
+ titleParts.push('Path: ' + session.worktree.path);
2698
+ }
2699
+ var title = titleParts.length > 0 ? ' title="' + escapeHtml(titleParts.join('\n')) + '"' : '';
2700
+ return '<span class="session-kind-badge worktree"' + title + '>Worktree</span>' + renderWorktreeMergeBadge(session);
2331
2701
  }
2332
2702
 
2333
2703
  function renderSessionKindBadge(session) {
@@ -2723,15 +3093,7 @@
2723
3093
  });
2724
3094
  var savePassBtn = document.getElementById("save-password-button");
2725
3095
  if (savePassBtn) savePassBtn.addEventListener("click", savePassword);
2726
- // Settings tab clicks
2727
- var settingsTabs = document.querySelectorAll(".settings-tab");
2728
- for (var ti = 0; ti < settingsTabs.length; ti++) {
2729
- settingsTabs[ti].addEventListener("click", function(e) {
2730
- switchSettingsTab(e.target.getAttribute("data-tab"));
2731
- });
2732
- }
2733
- var saveConfigBtn = document.getElementById("save-config-button");
2734
- if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
3096
+ bindSettingsModalEvents();
2735
3097
  var uploadCertBtn = document.getElementById("upload-cert-button");
2736
3098
  if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
2737
3099
  var checkUpdateBtn = document.getElementById("check-update-button");
@@ -2754,6 +3116,12 @@
2754
3116
  if (drawerNewSessBtn) drawerNewSessBtn.addEventListener("click", openSessionModal);
2755
3117
  var closeModalBtn = document.getElementById("close-modal-button");
2756
3118
  if (closeModalBtn) closeModalBtn.addEventListener("click", closeSessionModal);
3119
+ var closeWorktreeMergeBtn = document.getElementById("close-worktree-merge-button");
3120
+ if (closeWorktreeMergeBtn) closeWorktreeMergeBtn.addEventListener("click", closeWorktreeMergeModal);
3121
+ var worktreeMergeCancelBtn = document.getElementById("worktree-merge-cancel-button");
3122
+ if (worktreeMergeCancelBtn) worktreeMergeCancelBtn.addEventListener("click", closeWorktreeMergeModal);
3123
+ var worktreeMergeConfirmBtn = document.getElementById("worktree-merge-confirm-button");
3124
+ if (worktreeMergeConfirmBtn) worktreeMergeConfirmBtn.addEventListener("click", confirmWorktreeMerge);
2757
3125
  var runBtn = document.getElementById("run-button");
2758
3126
  if (runBtn) runBtn.addEventListener("click", runCommand);
2759
3127
  var approvePermissionBtn = document.getElementById("approve-permission-btn");
@@ -2778,6 +3146,7 @@
2778
3146
  var sessionModal = document.getElementById("session-modal");
2779
3147
  if (sessionModal) sessionModal.addEventListener("click", function(e) {
2780
3148
  if (e.target.id === "session-modal") closeSessionModal();
3149
+ if (e.target.id === "worktree-merge-modal") closeWorktreeMergeModal();
2781
3150
  });
2782
3151
 
2783
3152
  var inputBox = document.getElementById("input-box");
@@ -2821,15 +3190,8 @@
2821
3190
  if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
2822
3191
  e.stopPropagation();
2823
3192
  state.shortcutsExpanded = !state.shortcutsExpanded;
2824
- var wrap = document.querySelector(".inline-shortcuts-wrap");
2825
- var toggle = document.querySelector(".shortcuts-toggle");
2826
- var row = document.querySelector(".inline-shortcuts-expanded-row");
2827
- if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
2828
- if (row) row.classList.toggle("visible", state.shortcutsExpanded);
2829
- if (toggle) {
2830
- toggle.classList.toggle("active", state.shortcutsExpanded);
2831
- toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
2832
- }
3193
+ persistShortcutsExpandedState();
3194
+ updateCollapsedShortcutsUi();
2833
3195
  });
2834
3196
  // Close shortcuts strip on outside click
2835
3197
  document.addEventListener("click", function(e) {
@@ -2839,13 +3201,8 @@
2839
3201
  var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
2840
3202
  if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
2841
3203
  state.shortcutsExpanded = false;
2842
- wrap.classList.remove("expanded");
2843
- if (expandedRow) expandedRow.classList.remove("visible");
2844
- var toggle = document.querySelector(".shortcuts-toggle");
2845
- if (toggle) {
2846
- toggle.classList.remove("active");
2847
- toggle.textContent = "\u2039";
2848
- }
3204
+ persistShortcutsExpandedState();
3205
+ updateCollapsedShortcutsUi();
2849
3206
  }
2850
3207
  });
2851
3208
 
@@ -3324,6 +3681,7 @@
3324
3681
  event.preventDefault();
3325
3682
  event.stopPropagation();
3326
3683
  state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
3684
+ persistHistoryPanelState();
3327
3685
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
3328
3686
  loadClaudeHistory();
3329
3687
  }
@@ -3371,6 +3729,10 @@
3371
3729
  handleResumeAction(actionButton);
3372
3730
  } else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
3373
3731
  handleResumeHistoryAction(actionButton);
3732
+ } else if (actionButton.dataset.action === "worktree-merge" && actionButton.dataset.sessionId) {
3733
+ openWorktreeMergeModal(actionButton.dataset.sessionId);
3734
+ } else if (actionButton.dataset.action === "worktree-cleanup" && actionButton.dataset.sessionId) {
3735
+ retryWorktreeCleanup(actionButton.dataset.sessionId);
3374
3736
  }
3375
3737
  return;
3376
3738
  }
@@ -3991,7 +4353,7 @@
3991
4353
  })
3992
4354
  .then(function(res) { return res.json(); })
3993
4355
  .then(function(config) {
3994
- state.config = config;
4356
+ applySettingsConfig(config);
3995
4357
  var statusDot = document.getElementById("status-dot");
3996
4358
  var statusText = document.getElementById("status-text");
3997
4359
  if (statusDot) statusDot.classList.add("active");
@@ -4029,9 +4391,16 @@
4029
4391
  state.sessions = [];
4030
4392
  state.claudeHistory = [];
4031
4393
  state.claudeHistoryLoaded = false;
4032
- state.claudeHistoryExpanded = true;
4394
+ state.claudeHistoryExpanded = getConfiguredPanelDefaults().claudeHistoryExpanded;
4395
+ persistHistoryPanelState();
4033
4396
  state.claudeHistoryExpandedDirs = {};
4034
- state.sessionsDrawerOpen = false;
4397
+ state.sessionsDrawerOpen = getConfiguredPanelDefaults().sessionsDrawerOpen;
4398
+ persistDrawerState();
4399
+ state.filePanelOpen = getConfiguredPanelDefaults().filePanelOpen;
4400
+ persistFilePanelState();
4401
+ state.shortcutsExpanded = getConfiguredPanelDefaults().shortcutsExpanded;
4402
+ persistShortcutsExpandedState();
4403
+ updateCollapsedShortcutsUi();
4035
4404
  render();
4036
4405
  }
4037
4406
 
@@ -4697,11 +5066,10 @@
4697
5066
 
4698
5067
  function toggleSessionsDrawer() {
4699
5068
  state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
5069
+ persistDrawerState();
4700
5070
  if (state.sessionsDrawerOpen && isMobileLayout()) {
4701
5071
  state.filePanelOpen = false;
4702
- try {
4703
- localStorage.setItem("wand-file-panel-open", "false");
4704
- } catch (e) {}
5072
+ persistFilePanelState();
4705
5073
  }
4706
5074
  updateLayoutState();
4707
5075
  }
@@ -4710,6 +5078,7 @@
4710
5078
  if (!state.sessionsDrawerOpen) return;
4711
5079
  closeSwipedItem();
4712
5080
  state.sessionsDrawerOpen = false;
5081
+ persistDrawerState();
4713
5082
  updateLayoutState();
4714
5083
  }
4715
5084
 
@@ -4795,9 +5164,166 @@
4795
5164
  document.addEventListener("keydown", focusTrapHandler);
4796
5165
  }
4797
5166
 
5167
+ function getActiveWorktreeMergeSession() {
5168
+ if (!state.activeWorktreeMergeSessionId) return null;
5169
+ return state.sessions.find(function(session) { return session.id === state.activeWorktreeMergeSessionId; }) || null;
5170
+ }
5171
+
5172
+ function renderWorktreeMergeContent() {
5173
+ var container = document.getElementById("worktree-merge-content");
5174
+ var confirmBtn = document.getElementById("worktree-merge-confirm-button");
5175
+ var errorEl = document.getElementById("worktree-merge-error");
5176
+ var session = getActiveWorktreeMergeSession();
5177
+ var result = state.worktreeMergeCheckResult;
5178
+ if (!container || !confirmBtn) return;
5179
+ if (!session || !session.worktree) {
5180
+ container.innerHTML = '<p class="field-hint">未找到可合并的 worktree 会话。</p>';
5181
+ confirmBtn.disabled = true;
5182
+ return;
5183
+ }
5184
+ if (errorEl) {
5185
+ if (state.worktreeMergeError) {
5186
+ showError(errorEl, state.worktreeMergeError);
5187
+ } else {
5188
+ hideError(errorEl);
5189
+ }
5190
+ }
5191
+ var rows = [
5192
+ '<div class="worktree-merge-row"><span>来源分支</span><strong>' + escapeHtml(session.worktree.branch || "-") + '</strong></div>',
5193
+ '<div class="worktree-merge-row"><span>工作目录</span><strong>' + escapeHtml(session.worktree.path || "-") + '</strong></div>'
5194
+ ];
5195
+ if (result) {
5196
+ rows.push('<div class="worktree-merge-row"><span>目标分支</span><strong>' + escapeHtml(result.targetBranch || "-") + '</strong></div>');
5197
+ rows.push('<div class="worktree-merge-row"><span>待合并提交</span><strong>' + escapeHtml(String(result.aheadCount || 0)) + '</strong></div>');
5198
+ rows.push('<div class="worktree-merge-row"><span>未提交改动</span><strong>' + escapeHtml(result.hasUncommittedChanges ? "有" : "无") + '</strong></div>');
5199
+ rows.push('<div class="worktree-merge-row"><span>冲突风险</span><strong>' + escapeHtml(result.hasConflicts ? "有" : "无") + '</strong></div>');
5200
+ if (result.reason) {
5201
+ rows.push('<p class="field-hint">' + escapeHtml(result.reason) + '</p>');
5202
+ }
5203
+ } else if (state.worktreeMergeLoading) {
5204
+ rows.push('<p class="field-hint">正在检查 worktree 合并状态…</p>');
5205
+ }
5206
+ container.innerHTML = rows.join("");
5207
+ confirmBtn.disabled = state.worktreeMergeLoading || state.worktreeMergeSubmitting || !result || result.ok !== true;
5208
+ confirmBtn.textContent = state.worktreeMergeSubmitting ? "合并中..." : "确认合并并清理";
5209
+ }
5210
+
5211
+ function openWorktreeMergeModal(sessionId) {
5212
+ state.activeWorktreeMergeSessionId = sessionId;
5213
+ state.worktreeMergeCheckResult = null;
5214
+ state.worktreeMergeLoading = true;
5215
+ state.worktreeMergeSubmitting = false;
5216
+ state.worktreeMergeError = "";
5217
+ closeSessionModal();
5218
+ closeSettingsModal();
5219
+ var modal = document.getElementById("worktree-merge-modal");
5220
+ if (modal) {
5221
+ modal.classList.remove("hidden");
5222
+ lastFocusedElement = document.activeElement;
5223
+ setupFocusTrap(modal);
5224
+ }
5225
+ renderWorktreeMergeContent();
5226
+ fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/merge/check", {
5227
+ method: "POST",
5228
+ credentials: "same-origin"
5229
+ })
5230
+ .then(function(res) { return res.json(); })
5231
+ .then(function(data) {
5232
+ if (data && data.error) {
5233
+ throw new Error(data.error);
5234
+ }
5235
+ if (data && data.session) {
5236
+ updateSessionSnapshot(data.session);
5237
+ }
5238
+ state.worktreeMergeCheckResult = data.result || null;
5239
+ state.worktreeMergeError = "";
5240
+ })
5241
+ .catch(function(error) {
5242
+ state.worktreeMergeError = (error && error.message) || "无法检查 worktree 合并状态。";
5243
+ })
5244
+ .finally(function() {
5245
+ state.worktreeMergeLoading = false;
5246
+ renderWorktreeMergeContent();
5247
+ });
5248
+ }
5249
+
5250
+ function closeWorktreeMergeModal() {
5251
+ var modal = document.getElementById("worktree-merge-modal");
5252
+ state.activeWorktreeMergeSessionId = null;
5253
+ state.worktreeMergeCheckResult = null;
5254
+ state.worktreeMergeLoading = false;
5255
+ state.worktreeMergeSubmitting = false;
5256
+ state.worktreeMergeError = "";
5257
+ if (modal) {
5258
+ modal.classList.add("hidden");
5259
+ }
5260
+ if (focusTrapHandler) {
5261
+ document.removeEventListener("keydown", focusTrapHandler);
5262
+ focusTrapHandler = null;
5263
+ }
5264
+ if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
5265
+ lastFocusedElement.focus();
5266
+ }
5267
+ }
5268
+
5269
+ function confirmWorktreeMerge() {
5270
+ if (!state.activeWorktreeMergeSessionId || state.worktreeMergeSubmitting) return;
5271
+ state.worktreeMergeSubmitting = true;
5272
+ state.worktreeMergeError = "";
5273
+ renderWorktreeMergeContent();
5274
+ fetch("/api/sessions/" + encodeURIComponent(state.activeWorktreeMergeSessionId) + "/worktree/merge", {
5275
+ method: "POST",
5276
+ credentials: "same-origin",
5277
+ headers: { "Content-Type": "application/json" },
5278
+ body: JSON.stringify({})
5279
+ })
5280
+ .then(function(res) { return res.json(); })
5281
+ .then(function(data) {
5282
+ if (data && data.error) {
5283
+ throw new Error(data.error);
5284
+ }
5285
+ if (data && data.session) {
5286
+ updateSessionSnapshot(data.session);
5287
+ }
5288
+ showToast("已合并到 " + escapeHtml((data.result && data.result.targetBranch) || "主分支") + ((data.result && data.result.cleanupDone === false) ? ",但工作树待清理。" : "。"), "info");
5289
+ closeWorktreeMergeModal();
5290
+ return refreshAll();
5291
+ })
5292
+ .catch(function(error) {
5293
+ state.worktreeMergeError = (error && error.message) || "无法合并 worktree。";
5294
+ renderWorktreeMergeContent();
5295
+ })
5296
+ .finally(function() {
5297
+ state.worktreeMergeSubmitting = false;
5298
+ renderWorktreeMergeContent();
5299
+ });
5300
+ }
5301
+
5302
+ function retryWorktreeCleanup(sessionId) {
5303
+ fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/cleanup", {
5304
+ method: "POST",
5305
+ credentials: "same-origin"
5306
+ })
5307
+ .then(function(res) { return res.json(); })
5308
+ .then(function(data) {
5309
+ if (data && data.error) {
5310
+ throw new Error(data.error);
5311
+ }
5312
+ if (data && data.session) {
5313
+ updateSessionSnapshot(data.session);
5314
+ }
5315
+ showToast("已完成 worktree 清理。", "info");
5316
+ return refreshAll();
5317
+ })
5318
+ .catch(function(error) {
5319
+ showToast((error && error.message) || "无法清理 worktree。", "error");
5320
+ });
5321
+ }
5322
+
4798
5323
  function openSettingsModal() {
4799
5324
  // Close session modal first if open (mutual exclusion)
4800
5325
  closeSessionModal();
5326
+ refreshSettingsModalUi();
4801
5327
  var modal = document.getElementById("settings-modal");
4802
5328
  if (modal) {
4803
5329
  modal.classList.remove("hidden");
@@ -4808,9 +5334,8 @@
4808
5334
  if (confirmEl) confirmEl.value = "";
4809
5335
  hideSettingsMessages();
4810
5336
  setupFocusTrap(modal);
4811
- // Activate first tab
5337
+ bindSettingsModalEvents();
4812
5338
  switchSettingsTab("about");
4813
- // Load settings data
4814
5339
  loadSettingsData();
4815
5340
  }
4816
5341
  }
@@ -4901,62 +5426,120 @@
4901
5426
  panels[j].classList.remove("active");
4902
5427
  }
4903
5428
  }
5429
+ updateSettingsActiveNav();
5430
+ }
5431
+
5432
+ function refreshSettingsGeneralPanel() {
5433
+ var existingPanel = document.getElementById("settings-tab-general");
5434
+ if (!existingPanel) return;
5435
+ var wrapper = document.createElement("div");
5436
+ wrapper.innerHTML = buildSettingsGeneralPanel();
5437
+ var nextPanel = wrapper.firstChild;
5438
+ if (!nextPanel) return;
5439
+ existingPanel.replaceWith(nextPanel);
5440
+ var saveConfigBtn = document.getElementById("save-config-button");
5441
+ if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
5442
+ syncPanelStateSettingsForm();
5443
+ }
5444
+
5445
+ function bindSettingsTabEvents() {
5446
+ var settingsTabs = document.querySelectorAll(".settings-tab");
5447
+ for (var ti = 0; ti < settingsTabs.length; ti++) {
5448
+ settingsTabs[ti].addEventListener("click", function(e) {
5449
+ switchSettingsTab(e.target.getAttribute("data-tab"));
5450
+ });
5451
+ }
5452
+ }
5453
+
5454
+ function bindSettingsConfigActions() {
5455
+ var saveConfigBtn = document.getElementById("save-config-button");
5456
+ if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
5457
+ }
5458
+
5459
+ function bindSettingsModalEvents() {
5460
+ bindSettingsTabEvents();
5461
+ bindSettingsConfigActions();
5462
+ }
5463
+
5464
+ function refreshSettingsModalUi() {
5465
+ refreshSettingsGeneralPanel();
5466
+ updateSettingsActiveNav();
5467
+ }
5468
+
5469
+ function normalizePanelStateSettings(cfg) {
5470
+ var panelState = cfg && cfg.uiPreferences && cfg.uiPreferences.defaultPanelState;
5471
+ var defaults = getConfiguredPanelDefaults(cfg);
5472
+ return {
5473
+ sessionsDrawerOpen: panelState && typeof panelState.sessionsDrawerOpen === "boolean" ? panelState.sessionsDrawerOpen : defaults.sessionsDrawerOpen,
5474
+ filePanelOpen: panelState && typeof panelState.filePanelOpen === "boolean" ? panelState.filePanelOpen : defaults.filePanelOpen,
5475
+ shortcutsExpanded: panelState && typeof panelState.shortcutsExpanded === "boolean" ? panelState.shortcutsExpanded : defaults.shortcutsExpanded,
5476
+ claudeHistoryExpanded: panelState && typeof panelState.claudeHistoryExpanded === "boolean" ? panelState.claudeHistoryExpanded : defaults.claudeHistoryExpanded,
5477
+ chatMessageExpanded: panelState && typeof panelState.chatMessageExpanded === "boolean" ? panelState.chatMessageExpanded : defaults.chatMessageExpanded,
5478
+ structuredThinkingExpanded: panelState && typeof panelState.structuredThinkingExpanded === "boolean" ? panelState.structuredThinkingExpanded : defaults.structuredThinkingExpanded,
5479
+ structuredToolGroupExpanded: panelState && typeof panelState.structuredToolGroupExpanded === "boolean" ? panelState.structuredToolGroupExpanded : defaults.structuredToolGroupExpanded,
5480
+ structuredInlineToolExpanded: panelState && typeof panelState.structuredInlineToolExpanded === "boolean" ? panelState.structuredInlineToolExpanded : defaults.structuredInlineToolExpanded,
5481
+ structuredTerminalExpanded: panelState && typeof panelState.structuredTerminalExpanded === "boolean" ? panelState.structuredTerminalExpanded : defaults.structuredTerminalExpanded,
5482
+ structuredToolCardExpanded: panelState && typeof panelState.structuredToolCardExpanded === "boolean" ? panelState.structuredToolCardExpanded : defaults.structuredToolCardExpanded,
5483
+ };
5484
+ }
5485
+
5486
+ function applyLoadedSettingsData(data) {
5487
+ var cfg = data.config || {};
5488
+ applySettingsConfig(cfg);
5489
+
5490
+ var nameEl = document.getElementById("settings-pkg-name");
5491
+ var verEl = document.getElementById("settings-version");
5492
+ var nodeEl = document.getElementById("settings-node-req");
5493
+ var repoEl = document.getElementById("settings-repo-url");
5494
+ if (nameEl) nameEl.textContent = data.packageName || "-";
5495
+ if (verEl) verEl.textContent = data.version || "-";
5496
+ if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
5497
+ if (repoEl && data.repoUrl) {
5498
+ repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
5499
+ }
5500
+
5501
+ var hostEl = document.getElementById("cfg-host");
5502
+ var portEl = document.getElementById("cfg-port");
5503
+ var httpsEl = document.getElementById("cfg-https");
5504
+ var modeEl = document.getElementById("cfg-mode");
5505
+ var cwdEl = document.getElementById("cfg-cwd");
5506
+ var shellEl = document.getElementById("cfg-shell");
5507
+ if (hostEl) hostEl.value = cfg.host || "";
5508
+ if (portEl) portEl.value = cfg.port || "";
5509
+ if (httpsEl) httpsEl.checked = cfg.https === true;
5510
+ if (modeEl) modeEl.value = cfg.defaultMode || "default";
5511
+ if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
5512
+ if (shellEl) shellEl.value = cfg.shell || "";
5513
+ var langEl = document.getElementById("cfg-language");
5514
+ if (langEl) langEl.value = cfg.language || "";
5515
+ syncPanelStateSettingsForm(normalizePanelStateSettings(cfg));
5516
+
5517
+ var certStatus = document.getElementById("cert-status");
5518
+ if (certStatus) {
5519
+ certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
5520
+ certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
5521
+ }
5522
+
5523
+ var presetsList = document.getElementById("presets-list");
5524
+ if (presetsList && cfg.commandPresets) {
5525
+ var html = "";
5526
+ for (var i = 0; i < cfg.commandPresets.length; i++) {
5527
+ var p = cfg.commandPresets[i];
5528
+ html += '<div class="preset-item">' +
5529
+ '<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
5530
+ '<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
5531
+ '</div>';
5532
+ }
5533
+ if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>没有命令预设</span><span class="hint">在 config.json 的 commandPresets 中配置</span></div>';
5534
+ presetsList.innerHTML = html;
5535
+ }
4904
5536
  }
4905
5537
 
4906
5538
  function loadSettingsData() {
4907
5539
  fetch("/api/settings", { credentials: "same-origin" })
4908
5540
  .then(function(res) { return res.json(); })
4909
5541
  .then(function(data) {
4910
- // About
4911
- var nameEl = document.getElementById("settings-pkg-name");
4912
- var verEl = document.getElementById("settings-version");
4913
- var nodeEl = document.getElementById("settings-node-req");
4914
- var repoEl = document.getElementById("settings-repo-url");
4915
- if (nameEl) nameEl.textContent = data.packageName || "-";
4916
- if (verEl) verEl.textContent = data.version || "-";
4917
- if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
4918
- if (repoEl && data.repoUrl) {
4919
- repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
4920
- }
4921
-
4922
- // Config fields
4923
- var cfg = data.config || {};
4924
- var hostEl = document.getElementById("cfg-host");
4925
- var portEl = document.getElementById("cfg-port");
4926
- var httpsEl = document.getElementById("cfg-https");
4927
- var modeEl = document.getElementById("cfg-mode");
4928
- var cwdEl = document.getElementById("cfg-cwd");
4929
- var shellEl = document.getElementById("cfg-shell");
4930
- if (hostEl) hostEl.value = cfg.host || "";
4931
- if (portEl) portEl.value = cfg.port || "";
4932
- if (httpsEl) httpsEl.checked = cfg.https === true;
4933
- if (modeEl) modeEl.value = cfg.defaultMode || "default";
4934
- if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
4935
- if (shellEl) shellEl.value = cfg.shell || "";
4936
- var langEl = document.getElementById("cfg-language");
4937
- if (langEl) langEl.value = cfg.language || "";
4938
-
4939
- // Cert status
4940
- var certStatus = document.getElementById("cert-status");
4941
- if (certStatus) {
4942
- certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
4943
- certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
4944
- }
4945
-
4946
- // Presets
4947
- var presetsList = document.getElementById("presets-list");
4948
- if (presetsList && cfg.commandPresets) {
4949
- var html = "";
4950
- for (var i = 0; i < cfg.commandPresets.length; i++) {
4951
- var p = cfg.commandPresets[i];
4952
- html += '<div class="preset-item">' +
4953
- '<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
4954
- '<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
4955
- '</div>';
4956
- }
4957
- if (!html) html = '<div class="empty-state-compact"><span class="empty-icon">\u2699</span><span>\u6ca1\u6709\u547d\u4ee4\u9884\u8bbe</span><span class="hint">\u5728 config.json \u7684 commandPresets \u4e2d\u914d\u7f6e</span></div>';
4958
- presetsList.innerHTML = html;
4959
- }
5542
+ applyLoadedSettingsData(data);
4960
5543
  })
4961
5544
  .catch(function() {});
4962
5545
  }
@@ -4973,6 +5556,9 @@
4973
5556
  defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
4974
5557
  shell: (document.getElementById("cfg-shell") || {}).value,
4975
5558
  language: (document.getElementById("cfg-language") || {}).value || "",
5559
+ uiPreferences: {
5560
+ defaultPanelState: getPanelStateSettingsFormValues(),
5561
+ }
4976
5562
  };
4977
5563
 
4978
5564
  fetch("/api/settings/config", {
@@ -4990,6 +5576,7 @@
4990
5576
  } else {
4991
5577
  msgEl.textContent = "配置已保存,部分更改需要重启后生效。";
4992
5578
  msgEl.style.color = "var(--success)";
5579
+ handleSettingsConfigSaved(data.config);
4993
5580
  }
4994
5581
  msgEl.classList.remove("hidden");
4995
5582
  }
@@ -5003,6 +5590,7 @@
5003
5590
  });
5004
5591
  }
5005
5592
 
5593
+
5006
5594
  function uploadCertificates() {
5007
5595
  var keyFile = document.getElementById("cert-key-file");
5008
5596
  var certFile = document.getElementById("cert-cert-file");
@@ -9281,12 +9869,12 @@
9281
9869
  if (cards.length > 0) {
9282
9870
  var firstCard = cards[0];
9283
9871
  var firstCardKey = getElementExpandKey(firstCard);
9284
- if (getPersistedExpandState(firstCardKey) === null) {
9872
+ if (!hasPersistedExpandState(firstCardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
9285
9873
  firstCard.classList.remove("collapsed");
9286
9874
  }
9287
9875
  for (var ci = 1; ci < cards.length; ci++) {
9288
9876
  var cardKey = getElementExpandKey(cards[ci]);
9289
- if (getPersistedExpandState(cardKey) === null) {
9877
+ if (!hasPersistedExpandState(cardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
9290
9878
  cards[ci].classList.add("collapsed");
9291
9879
  }
9292
9880
  }
@@ -9304,7 +9892,7 @@
9304
9892
  var allCards = container.querySelectorAll(".tool-use-card");
9305
9893
  allCards.forEach(function(c) {
9306
9894
  var cardKey = getElementExpandKey(c);
9307
- if (getPersistedExpandState(cardKey) !== null) return;
9895
+ if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
9308
9896
  // Keep expanded if this card is inside a newly added message
9309
9897
  if (newEls) {
9310
9898
  for (var i = 0; i < newEls.length; i++) {
@@ -9428,7 +10016,7 @@
9428
10016
  var newestCard = null;
9429
10017
  allCards.forEach(function(c) {
9430
10018
  var cardKey = getElementExpandKey(c);
9431
- if (getPersistedExpandState(cardKey) !== null) return;
10019
+ if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
9432
10020
  if (newestMsgEl && newestMsgEl.contains(c)) {
9433
10021
  if (!newestCard) newestCard = c;
9434
10022
  else c.classList.add("collapsed");
@@ -10518,7 +11106,7 @@
10518
11106
  // Thinking card (deep thought) — from PTY parsing
10519
11107
  if (msg.role === "thinking") {
10520
11108
  var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
10521
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11109
+ var thinkingExpanded = getExpandState(thinkingKey, "thinking");
10522
11110
  return '<div class="chat-message thinking">' +
10523
11111
  '<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
10524
11112
  '<span class="thinking-inline-icon">⦿</span>' +
@@ -10655,8 +11243,7 @@
10655
11243
  }
10656
11244
  var summaryText = parts.join(" · ");
10657
11245
  var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
10658
- var persistedExpanded = getPersistedExpandState(groupKey);
10659
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11246
+ var shouldExpand = getExpandState(groupKey, "tool-group");
10660
11247
 
10661
11248
  // Render each item's inline-tool card
10662
11249
  var innerHtml = "";
@@ -10767,7 +11354,7 @@
10767
11354
  '</div>';
10768
11355
  }
10769
11356
  var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
10770
- var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11357
+ var thinkingExpanded = getExpandState(thinkingKey, "thinking");
10771
11358
  return '<div class="thinking-inline ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
10772
11359
  '<span class="thinking-inline-icon">⦿</span>' +
10773
11360
  '<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
@@ -10793,7 +11380,6 @@
10793
11380
  function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo, messageKey, index) {
10794
11381
  var toolId = block.id || "tool-" + toolName;
10795
11382
  var expandKey = buildExpandKey("inline-tool", [messageKey, toolId || index, index]);
10796
- var persistedExpanded = getPersistedExpandState(expandKey);
10797
11383
  var inputData = block.input || {};
10798
11384
  var resultContent = extractToolResultText(toolResult && toolResult.content);
10799
11385
 
@@ -10864,7 +11450,7 @@
10864
11450
  var fullResult = resultContent;
10865
11451
 
10866
11452
  var expandedHtml = "";
10867
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11453
+ var shouldExpand = getExpandState(expandKey, "inline-tool");
10868
11454
  if (hasResult) {
10869
11455
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
10870
11456
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -10904,7 +11490,6 @@
10904
11490
  var resultContent = extractToolResultText(toolResult && toolResult.content);
10905
11491
  var toolId = block.id || "tool-" + toolName;
10906
11492
  var expandKey = buildExpandKey("terminal", [messageKey, toolId || index, index]);
10907
- var persistedExpanded = getPersistedExpandState(expandKey);
10908
11493
 
10909
11494
  var isError = toolResult && toolResult.is_error;
10910
11495
  var exitCode = inputData.exitCode;
@@ -10942,7 +11527,7 @@
10942
11527
 
10943
11528
  // Show command preview in header (truncate long commands)
10944
11529
  var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
10945
- var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11530
+ var shouldExpand = getExpandState(expandKey, "terminal");
10946
11531
 
10947
11532
  return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
10948
11533
  '<div class="term-header" onclick="__terminalExpand(this)">' +
@@ -11140,8 +11725,7 @@
11140
11725
  }
11141
11726
 
11142
11727
  var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
11143
- var persistedExpanded = getPersistedExpandState(expandKey);
11144
- var shouldExpand = persistedExpanded === null ? statusClass === "loading" : persistedExpanded;
11728
+ var shouldExpand = getExpandState(expandKey, "tool-card", statusClass === "loading");
11145
11729
  var collapsedClass = shouldExpand ? "" : " collapsed";
11146
11730
  var toggleHtml = '<span class="tool-use-toggle">▼</span>';
11147
11731
  return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +