@co0ontty/wand 1.9.0 → 1.10.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,335 +60,6 @@
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
- }
392
63
 
393
64
  var state = {
394
65
  selectedId: (function() {
@@ -437,7 +108,7 @@
437
108
  loginPending: false,
438
109
  loginChecked: false,
439
110
  bootstrapping: true,
440
- sessionsDrawerOpen: getInitialPanelBoolean("wand-sessions-drawer-open", "sessionsDrawerOpen"),
111
+ sessionsDrawerOpen: false,
441
112
  modalOpen: false,
442
113
  presetValue: "",
443
114
  cwdValue: "",
@@ -459,6 +130,13 @@
459
130
  showInstallPrompt: false,
460
131
  ws: null,
461
132
  wsConnected: false,
133
+ _updateBubbleShown: false,
134
+ notifSound: (function() {
135
+ try { var v = localStorage.getItem("wand-notif-sound"); return v === null ? true : v === "true"; } catch (e) { return true; }
136
+ })(),
137
+ notifBubble: (function() {
138
+ try { var v = localStorage.getItem("wand-notif-bubble"); return v === null ? true : v === "true"; } catch (e) { return true; }
139
+ })(),
462
140
  currentView: "terminal",
463
141
  terminalScale: (function() {
464
142
  try {
@@ -470,7 +148,13 @@
470
148
  })(),
471
149
  terminalBaseFontSize: 13,
472
150
  keyboardPopupOpen: false,
473
- filePanelOpen: getInitialPanelBoolean("wand-file-panel-open", "filePanelOpen"),
151
+ filePanelOpen: (function() {
152
+ try {
153
+ return localStorage.getItem("wand-file-panel-open") === "true";
154
+ } catch (e) {
155
+ return false;
156
+ }
157
+ })(),
474
158
  chatAutoFollow: (function() {
475
159
  try {
476
160
  var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
@@ -494,18 +178,20 @@
494
178
  currentTask: null, // Current task title from Claude
495
179
  terminalInteractive: false,
496
180
  miniKeyboardVisible: false,
497
- shortcutsExpanded: getInitialPanelBoolean("wand-shortcuts-expanded", "shortcutsExpanded"),
181
+ shortcutsExpanded: false,
498
182
  modifiers: { ctrl: false, alt: false, shift: false },
499
183
  fileSearchQuery: "",
500
184
  fileExplorerLoading: false,
501
185
  allFiles: [],
502
186
  claudeHistory: [],
503
187
  claudeHistoryLoaded: false,
504
- claudeHistoryExpanded: getInitialPanelBoolean("wand-claude-history-expanded", "claudeHistoryExpanded"),
188
+ claudeHistoryExpanded: true,
505
189
  claudeHistoryExpandedDirs: {},
506
190
  sessionsManageMode: false,
507
191
  selectedSessionIds: {},
508
192
  selectedClaudeHistoryIds: {},
193
+ askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
194
+ queueEpoch: 0, // Monotonic counter for queue state freshness
509
195
  // Load last used working directory from localStorage
510
196
  workingDir: (function() {
511
197
  try {
@@ -650,6 +336,8 @@
650
336
  if (button) {
651
337
  button.classList.toggle("visible", shouldShow);
652
338
  }
339
+ var chatContainer = document.getElementById("chat-output");
340
+ if (chatContainer) chatContainer.classList.toggle("has-jump-btn", shouldShow);
653
341
  }
654
342
 
655
343
  function scrollChatToBottom(smooth) {
@@ -863,56 +551,6 @@
863
551
  }
864
552
  }
865
553
 
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
-
916
554
  function saveChatExpandStateMap(map) {
917
555
  try {
918
556
  if (!map || Object.keys(map).length === 0) {
@@ -933,6 +571,12 @@
933
571
  return sessionState && typeof sessionState === "object" ? sessionState : {};
934
572
  }
935
573
 
574
+ function getPersistedExpandState(itemKey) {
575
+ if (!itemKey || !state.selectedId) return null;
576
+ var sessionState = getCurrentChatExpandState();
577
+ return typeof sessionState[itemKey] === "boolean" ? sessionState[itemKey] : null;
578
+ }
579
+
936
580
  function setPersistedExpandState(itemKey, expanded) {
937
581
  if (!itemKey || !state.selectedId) return;
938
582
  var map = loadChatExpandStateMap();
@@ -1049,8 +693,9 @@
1049
693
  container.querySelectorAll("[data-expand-key]").forEach(function(el) {
1050
694
  var itemKey = getElementExpandKey(el);
1051
695
  var kind = el.dataset.expandKind || "";
1052
- if (!kind || !hasPersistedExpandState(itemKey)) return;
1053
- applyExpandedState(el, kind, getPersistedExpandState(itemKey));
696
+ var persisted = getPersistedExpandState(itemKey);
697
+ if (persisted === null || !kind) return;
698
+ applyExpandedState(el, kind, persisted);
1054
699
  });
1055
700
  }
1056
701
 
@@ -1059,6 +704,7 @@
1059
704
  state.lastRenderedMsgCount = 0;
1060
705
  state.lastRenderedEmpty = null;
1061
706
  state.renderPending = false;
707
+ state.askUserSelections = {};
1062
708
  if (state.chatScrollElement && state.chatScrollHandler) {
1063
709
  state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
1064
710
  }
@@ -1193,7 +839,7 @@
1193
839
  })
1194
840
  .then(function(config) {
1195
841
  if (!config) return;
1196
- applySettingsConfig(config);
842
+ state.config = config;
1197
843
  state.loginChecked = true;
1198
844
  requestAnimationFrame(function() {
1199
845
  try {
@@ -1207,18 +853,7 @@
1207
853
  refreshAll();
1208
854
  requestNotificationPermission();
1209
855
  if (config.updateAvailable && config.latestVersion) {
1210
- showNotificationBubble({
1211
- title: "\u53d1\u73b0\u65b0\u7248\u672c",
1212
- body: "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion,
1213
- type: "info",
1214
- icon: "\u2191",
1215
- duration: 10000,
1216
- actionLabel: "\u53bb\u66f4\u65b0",
1217
- action: function() {
1218
- var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
1219
- if (settingsBtn) settingsBtn.click();
1220
- }
1221
- });
856
+ showUpdateBubble(config.currentVersion || "-", config.latestVersion);
1222
857
  sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
1223
858
  }
1224
859
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
@@ -1413,8 +1048,6 @@
1413
1048
  var preferredTool = getComposerTool();
1414
1049
  var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
1415
1050
 
1416
- var showTerminalHeaderControls = !!selectedSession && state.currentView === "terminal";
1417
- var showChatHeaderControls = !!selectedSession && state.currentView !== "terminal";
1418
1051
  return '<div class="app-container">' +
1419
1052
  '<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
1420
1053
  '<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
@@ -1457,37 +1090,14 @@
1457
1090
  '</div>' +
1458
1091
  '</aside>' +
1459
1092
  '<main class="main-content">' +
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>' +
1093
+ '<div class="main-header-row">' +
1094
+ '<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="切换会话侧栏" type="button">' +
1095
+ '<span class="hamburger-icon">' +
1096
+ '<span></span><span></span><span></span>' +
1097
+ '</span>' +
1098
+ '</button>' +
1099
+ '<span class="current-task hidden" id="current-task"></span>' +
1489
1100
  '</div>' +
1490
- '<div class="main-content-body">' +
1491
1101
  // File panel backdrop (mobile)
1492
1102
  '<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
1493
1103
  // File side panel
@@ -1508,8 +1118,22 @@
1508
1118
  '<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</div>' +
1509
1119
  '</div>' +
1510
1120
  '</div>' +
1511
- '<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active"></div>' +
1512
- '<div id="chat-output" class="chat-container hidden"></div>' +
1121
+ '<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
1122
+ '<div class="terminal-scale-overlay" aria-label="终端缩放控件">' +
1123
+ '<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
1124
+ '<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
1125
+ '<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
1126
+ '<span class="terminal-scale-overlay-divider"></span>' +
1127
+ '<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
1128
+ '</div>' +
1129
+ '<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
1130
+ '</div>' +
1131
+ '<div id="chat-output" class="chat-container hidden">' +
1132
+ '<div class="chat-overlay-controls">' +
1133
+ '<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>' +
1134
+ '</div>' +
1135
+ '<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
1136
+ '</div>' +
1513
1137
  '<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
1514
1138
  '<div class="blank-chat-inner">' +
1515
1139
  '<div class="blank-chat-logo">W</div>' +
@@ -1651,9 +1275,17 @@
1651
1275
  '<h2 class="modal-title">设置</h2>' +
1652
1276
  '<button id="close-settings-button" class="btn btn-ghost btn-icon">×</button>' +
1653
1277
  '</div>' +
1654
- '<div class="modal-body settings-layout">' +
1655
- renderSettingsNav() +
1656
- '<div class="settings-content">' +
1278
+ '<div class="modal-body">' +
1279
+ // Tabs
1280
+ '<div class="settings-tabs">' +
1281
+ '<button class="settings-tab active" data-tab="about">\u5173\u4e8e</button>' +
1282
+ '<button class="settings-tab" data-tab="general">\u57fa\u672c\u914d\u7f6e</button>' +
1283
+ '<button class="settings-tab" data-tab="notifications">\u901a\u77e5</button>' +
1284
+ '<button class="settings-tab" data-tab="security">\u5b89\u5168</button>' +
1285
+ '<button class="settings-tab" data-tab="presets">\u547d\u4ee4\u9884\u8bbe</button>' +
1286
+ '</div>' +
1287
+
1288
+ // About tab
1657
1289
  '<div class="settings-panel active" id="settings-tab-about">' +
1658
1290
  '<div class="settings-about-info">' +
1659
1291
  '<div class="settings-about-row"><span class="settings-label">包名</span><span class="settings-value" id="settings-pkg-name">-</span></div>' +
@@ -1667,26 +1299,99 @@
1667
1299
  '<span class="settings-value" id="settings-latest-version">-</span>' +
1668
1300
  '</div>' +
1669
1301
  '<div class="settings-update-actions">' +
1670
- '<button id="check-update-button" class="btn btn-ghost btn-sm">检查更新</button>' +
1671
- '<button id="do-update-button" class="btn btn-primary btn-sm hidden">更新到最新版</button>' +
1302
+ '<button id="check-update-button" class="btn btn-ghost btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
1303
+ '<button id="do-update-button" class="btn btn-primary btn-sm hidden">\u66f4\u65b0\u5230\u6700\u65b0\u7248</button>' +
1304
+ '<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
1672
1305
  '</div>' +
1673
1306
  '<p id="update-message" class="hint hidden"></p>' +
1674
1307
  '</div>' +
1675
- '<div class="settings-notification-section">' +
1676
- '<div class="settings-section-title">\u901a\u77e5\u72b6\u6001</div>' +
1308
+ '</div>' +
1309
+
1310
+ // Notifications tab
1311
+ '<div class="settings-panel" id="settings-tab-notifications">' +
1312
+ '<div class="settings-section-title">\u901a\u77e5\u504f\u597d</div>' +
1313
+ '<div class="field field-inline">' +
1314
+ '<input id="cfg-notif-sound" type="checkbox" class="field-checkbox" />' +
1315
+ '<label class="field-label" for="cfg-notif-sound">\u64ad\u653e\u63d0\u793a\u97f3</label>' +
1316
+ '</div>' +
1317
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">\u91cd\u8981\u901a\u77e5\uff08\u7248\u672c\u66f4\u65b0\u3001\u6743\u9650\u7b49\u5f85\u7b49\uff09\u65f6\u64ad\u653e\u67d4\u548c\u7684\u63d0\u793a\u97f3</p>' +
1318
+ '<div class="field field-inline">' +
1319
+ '<input id="cfg-notif-bubble" type="checkbox" class="field-checkbox" />' +
1320
+ '<label class="field-label" for="cfg-notif-bubble">\u5e94\u7528\u5185\u901a\u77e5\u6c14\u6ce1</label>' +
1321
+ '</div>' +
1322
+ '<p class="hint" style="margin-top:0;margin-bottom:10px">\u5728\u9875\u9762\u9876\u90e8\u5f39\u51fa\u6d6e\u52a8\u901a\u77e5\u6c14\u6ce1</p>' +
1323
+ '<div class="settings-notification-section" style="margin-top:6px">' +
1324
+ '<div class="settings-section-title">\u6d4f\u89c8\u5668\u901a\u77e5</div>' +
1677
1325
  '<div class="settings-about-row">' +
1678
- '<span class="settings-label">\u6d4f\u89c8\u5668\u901a\u77e5</span>' +
1326
+ '<span class="settings-label">\u6388\u6743\u72b6\u6001</span>' +
1679
1327
  '<span class="settings-value" id="notification-permission-status">-</span>' +
1680
1328
  '</div>' +
1681
1329
  '<div class="settings-update-actions">' +
1682
1330
  '<button id="notification-request-btn" class="btn btn-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
1331
+ '<button id="notification-reset-btn" class="btn btn-ghost btn-sm hidden">\u91cd\u65b0\u6388\u6743</button>' +
1683
1332
  '<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
1684
1333
  '</div>' +
1685
1334
  '<p id="notification-test-message" class="hint hidden"></p>' +
1686
1335
  '</div>' +
1687
1336
  '</div>' +
1688
1337
 
1689
- buildSettingsGeneralPanel() +
1338
+ // General config tab
1339
+ '<div class="settings-panel" id="settings-tab-general">' +
1340
+ '<div class="field-row">' +
1341
+ '<div class="field">' +
1342
+ '<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
1343
+ '<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
1344
+ '</div>' +
1345
+ '<div class="field">' +
1346
+ '<label class="field-label" for="cfg-port">端口 (port)</label>' +
1347
+ '<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
1348
+ '</div>' +
1349
+ '</div>' +
1350
+ '<div class="field field-inline">' +
1351
+ '<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
1352
+ '<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
1353
+ '</div>' +
1354
+ '<div class="field-row">' +
1355
+ '<div class="field">' +
1356
+ '<label class="field-label" for="cfg-mode">默认执行模式</label>' +
1357
+ '<select id="cfg-mode" class="field-input">' +
1358
+ '<option value="default">default</option>' +
1359
+ '<option value="assist">assist</option>' +
1360
+ '<option value="agent">agent</option>' +
1361
+ '<option value="agent-max">agent-max</option>' +
1362
+ '<option value="auto-edit">auto-edit</option>' +
1363
+ '<option value="full-access">full-access</option>' +
1364
+ '<option value="native">native</option>' +
1365
+ '<option value="managed">managed</option>' +
1366
+ '</select>' +
1367
+ '</div>' +
1368
+ '<div class="field">' +
1369
+ '<label class="field-label" for="cfg-language">回复语言</label>' +
1370
+ '<select id="cfg-language" class="field-input">' +
1371
+ '<option value="">自动(不指定)</option>' +
1372
+ '<option value="中文">中文</option>' +
1373
+ '<option value="English">English</option>' +
1374
+ '<option value="日本語">日本語</option>' +
1375
+ '<option value="한국어">한국어</option>' +
1376
+ '<option value="Español">Español</option>' +
1377
+ '<option value="Français">Français</option>' +
1378
+ '<option value="Deutsch">Deutsch</option>' +
1379
+ '<option value="Русский">Русский</option>' +
1380
+ '</select>' +
1381
+ '</div>' +
1382
+ '</div>' +
1383
+ '<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
1384
+ '<div class="field">' +
1385
+ '<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
1386
+ '<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
1387
+ '</div>' +
1388
+ '<div class="field">' +
1389
+ '<label class="field-label" for="cfg-shell">Shell</label>' +
1390
+ '<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
1391
+ '</div>' +
1392
+ '<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
1393
+ '<p id="config-message" class="hint hidden"></p>' +
1394
+ '</div>' +
1690
1395
 
1691
1396
  // Security tab
1692
1397
  '<div class="settings-panel" id="settings-tab-security">' +
@@ -1724,7 +1429,6 @@
1724
1429
  '<div class="settings-panel" id="settings-tab-presets">' +
1725
1430
  '<div id="presets-list" class="presets-list"></div>' +
1726
1431
  '</div>' +
1727
- '</div>' +
1728
1432
  '</div>' +
1729
1433
  '</div>' +
1730
1434
  '</section>';
@@ -2087,7 +1791,9 @@
2087
1791
 
2088
1792
  function setFilePanelOpen(nextOpen) {
2089
1793
  state.filePanelOpen = nextOpen;
2090
- persistFilePanelState();
1794
+ try {
1795
+ localStorage.setItem("wand-file-panel-open", String(state.filePanelOpen));
1796
+ } catch (e) {}
2091
1797
  if (state.filePanelOpen && isMobileLayout()) {
2092
1798
  state.sessionsDrawerOpen = false;
2093
1799
  }
@@ -2901,33 +2607,85 @@
2901
2607
  }
2902
2608
  }
2903
2609
  }
2904
- // Global handler for ask-user option buttons called via onclick
2905
- window.__askOption = function(btnEl) {
2906
- var optionLabel = btnEl.dataset.optionLabel;
2907
- if (optionLabel && state.selectedId) {
2908
- btnEl.classList.add("selected");
2909
- // Only disable options within the same question group, not globally
2910
- var questionGroup = btnEl.closest(".ask-user-question-group");
2911
- if (questionGroup) {
2912
- questionGroup.querySelectorAll(".ask-user-option").forEach(function(opt) {
2913
- opt.classList.add("selected");
2914
- opt.style.pointerEvents = "none";
2915
- });
2916
- var sentDiv = document.createElement("div");
2917
- sentDiv.className = "ask-user-answer-sent";
2918
- sentDiv.innerHTML = "\u2713 \u5df2\u53d1\u9001: " + escapeHtml(optionLabel);
2919
- questionGroup.appendChild(sentDiv);
2920
- }
2921
- fetch("/api/sessions/" + state.selectedId + "/input", {
2922
- method: "POST",
2923
- headers: { "Content-Type": "application/json" },
2924
- credentials: "same-origin",
2925
- body: JSON.stringify({ input: optionLabel + "\n", view: state.currentView })
2926
- }).catch(function(err) {
2927
- console.error("[wand] Error sending answer:", err);
2610
+ // ── AskUserQuestion handlers: select render submit ──
2611
+ window.__askSelect = function(toolUseId, qIdx, optIdx, isMulti) {
2612
+ var sel = state.askUserSelections[toolUseId];
2613
+ if (!sel) {
2614
+ sel = { submitted: false };
2615
+ state.askUserSelections[toolUseId] = sel;
2616
+ }
2617
+ if (sel.submitted) return;
2618
+ var current = sel[qIdx] || [];
2619
+ if (isMulti) {
2620
+ var pos = current.indexOf(optIdx);
2621
+ if (pos === -1) { current.push(optIdx); } else { current.splice(pos, 1); }
2622
+ } else {
2623
+ current = current[0] === optIdx ? [] : [optIdx];
2624
+ }
2625
+ sel[qIdx] = current;
2626
+ window.__askRender(toolUseId);
2627
+ };
2628
+
2629
+ window.__askRender = function(toolUseId) {
2630
+ var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
2631
+ if (!card) return;
2632
+ var sel = state.askUserSelections[toolUseId] || {};
2633
+ // Update option selected states
2634
+ card.querySelectorAll(".ask-user-option").forEach(function(btn) {
2635
+ var qIdx = parseInt(btn.dataset.questionIndex, 10);
2636
+ var oIdx = parseInt(btn.dataset.optionIndex, 10);
2637
+ var chosen = (sel[qIdx] || []).indexOf(oIdx) !== -1;
2638
+ btn.classList.toggle("selected", chosen);
2639
+ });
2640
+ // Update submit button: enabled only when every question has at least one selection
2641
+ var submitBtn = card.querySelector(".ask-user-submit");
2642
+ if (submitBtn) {
2643
+ var groups = card.querySelectorAll(".ask-user-question-group");
2644
+ var allAnswered = true;
2645
+ groups.forEach(function(g, i) {
2646
+ if (!sel[i] || sel[i].length === 0) allAnswered = false;
2928
2647
  });
2648
+ submitBtn.disabled = !allAnswered || !!sel.submitted;
2649
+ if (sel.submitted) {
2650
+ submitBtn.textContent = "已提交...";
2651
+ submitBtn.classList.add("ask-user-submitted");
2652
+ }
2929
2653
  }
2930
2654
  };
2655
+
2656
+ window.__askSubmit = function(toolUseId) {
2657
+ var sel = state.askUserSelections[toolUseId];
2658
+ if (!sel || sel.submitted || !state.selectedId) return;
2659
+ var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
2660
+ if (!card) return;
2661
+ var groups = card.querySelectorAll(".ask-user-question-group");
2662
+ var lines = [];
2663
+ var allAnswered = true;
2664
+ groups.forEach(function(group, qIdx) {
2665
+ var selected = sel[qIdx] || [];
2666
+ if (selected.length === 0) { allAnswered = false; return; }
2667
+ var labels = [];
2668
+ selected.forEach(function(optIdx) {
2669
+ var btn = group.querySelector('[data-option-index="' + optIdx + '"]');
2670
+ if (btn) labels.push(btn.dataset.optionLabel);
2671
+ });
2672
+ lines.push(labels.join(", "));
2673
+ });
2674
+ if (!allAnswered) return;
2675
+ sel.submitted = true;
2676
+ window.__askRender(toolUseId);
2677
+ var answerText = lines.join("\n");
2678
+ fetch("/api/sessions/" + state.selectedId + "/input", {
2679
+ method: "POST",
2680
+ headers: { "Content-Type": "application/json" },
2681
+ credentials: "same-origin",
2682
+ body: JSON.stringify({ input: answerText + "\n", view: state.currentView })
2683
+ }).catch(function(err) {
2684
+ console.error("[wand] Error sending answer:", err);
2685
+ sel.submitted = false;
2686
+ window.__askRender(toolUseId);
2687
+ });
2688
+ };
2931
2689
  function attachEventListeners() {
2932
2690
 
2933
2691
  var loginButton = document.getElementById("login-button");
@@ -3093,20 +2851,51 @@
3093
2851
  });
3094
2852
  var savePassBtn = document.getElementById("save-password-button");
3095
2853
  if (savePassBtn) savePassBtn.addEventListener("click", savePassword);
3096
- bindSettingsModalEvents();
2854
+ // Settings tab clicks
2855
+ var settingsTabs = document.querySelectorAll(".settings-tab");
2856
+ for (var ti = 0; ti < settingsTabs.length; ti++) {
2857
+ settingsTabs[ti].addEventListener("click", function(e) {
2858
+ switchSettingsTab(e.target.getAttribute("data-tab"));
2859
+ });
2860
+ }
2861
+ var saveConfigBtn = document.getElementById("save-config-button");
2862
+ if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
3097
2863
  var uploadCertBtn = document.getElementById("upload-cert-button");
3098
2864
  if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
3099
2865
  var checkUpdateBtn = document.getElementById("check-update-button");
3100
2866
  if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
3101
2867
  var doUpdateBtn = document.getElementById("do-update-button");
3102
2868
  if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
3103
- // Notification test section
2869
+ var doRestartBtn = document.getElementById("do-restart-button");
2870
+ if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
2871
+ // Notification preferences
2872
+ var notifSoundEl = document.getElementById("cfg-notif-sound");
2873
+ if (notifSoundEl) {
2874
+ notifSoundEl.checked = state.notifSound;
2875
+ notifSoundEl.addEventListener("change", function() {
2876
+ state.notifSound = notifSoundEl.checked;
2877
+ try { localStorage.setItem("wand-notif-sound", String(state.notifSound)); } catch (e) {}
2878
+ // Preview sound when toggling on
2879
+ if (state.notifSound) _doPlaySound();
2880
+ });
2881
+ }
2882
+ var notifBubbleEl = document.getElementById("cfg-notif-bubble");
2883
+ if (notifBubbleEl) {
2884
+ notifBubbleEl.checked = state.notifBubble;
2885
+ notifBubbleEl.addEventListener("change", function() {
2886
+ state.notifBubble = notifBubbleEl.checked;
2887
+ try { localStorage.setItem("wand-notif-bubble", String(state.notifBubble)); } catch (e) {}
2888
+ });
2889
+ }
2890
+ // Browser notification section
3104
2891
  var notifRequestBtn = document.getElementById("notification-request-btn");
3105
2892
  if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
3106
2893
  if (typeof Notification !== "undefined") {
3107
2894
  Notification.requestPermission().then(function() { updateNotificationStatus(); });
3108
2895
  }
3109
2896
  });
2897
+ var notifResetBtn = document.getElementById("notification-reset-btn");
2898
+ if (notifResetBtn) notifResetBtn.addEventListener("click", resetNotificationPermission);
3110
2899
  var notifTestBtn = document.getElementById("notification-test-btn");
3111
2900
  if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
3112
2901
  updateNotificationStatus();
@@ -3169,11 +2958,6 @@
3169
2958
  inputBox.addEventListener("blur", handleInputBoxBlur);
3170
2959
  }
3171
2960
 
3172
- // View toggle handlers
3173
- var viewTermBtn = document.getElementById("view-terminal-btn");
3174
- if (viewTermBtn) viewTermBtn.addEventListener("click", function() { setView("terminal"); });
3175
- var viewChatBtn = document.getElementById("view-chat-btn");
3176
- if (viewChatBtn) viewChatBtn.addEventListener("click", function() { setView("chat"); });
3177
2961
  // Terminal interactive toggle (both topbar and terminal-header)
3178
2962
  var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
3179
2963
  terminalInteractiveToggles.forEach(function(id) {
@@ -3190,8 +2974,15 @@
3190
2974
  if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
3191
2975
  e.stopPropagation();
3192
2976
  state.shortcutsExpanded = !state.shortcutsExpanded;
3193
- persistShortcutsExpandedState();
3194
- updateCollapsedShortcutsUi();
2977
+ var wrap = document.querySelector(".inline-shortcuts-wrap");
2978
+ var toggle = document.querySelector(".shortcuts-toggle");
2979
+ var row = document.querySelector(".inline-shortcuts-expanded-row");
2980
+ if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
2981
+ if (row) row.classList.toggle("visible", state.shortcutsExpanded);
2982
+ if (toggle) {
2983
+ toggle.classList.toggle("active", state.shortcutsExpanded);
2984
+ toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
2985
+ }
3195
2986
  });
3196
2987
  // Close shortcuts strip on outside click
3197
2988
  document.addEventListener("click", function(e) {
@@ -3201,8 +2992,13 @@
3201
2992
  var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
3202
2993
  if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
3203
2994
  state.shortcutsExpanded = false;
3204
- persistShortcutsExpandedState();
3205
- updateCollapsedShortcutsUi();
2995
+ wrap.classList.remove("expanded");
2996
+ if (expandedRow) expandedRow.classList.remove("visible");
2997
+ var toggle = document.querySelector(".shortcuts-toggle");
2998
+ if (toggle) {
2999
+ toggle.classList.remove("active");
3000
+ toggle.textContent = "\u2039";
3001
+ }
3206
3002
  }
3207
3003
  });
3208
3004
 
@@ -3681,7 +3477,6 @@
3681
3477
  event.preventDefault();
3682
3478
  event.stopPropagation();
3683
3479
  state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
3684
- persistHistoryPanelState();
3685
3480
  if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
3686
3481
  loadClaudeHistory();
3687
3482
  }
@@ -3851,6 +3646,8 @@
3851
3646
  if (button) {
3852
3647
  button.classList.toggle("visible", shouldShow);
3853
3648
  }
3649
+ var termContainer = document.getElementById("output");
3650
+ if (termContainer) termContainer.classList.toggle("has-jump-btn", shouldShow);
3854
3651
  }
3855
3652
 
3856
3653
  function isTerminalNearBottom() {
@@ -4353,7 +4150,7 @@
4353
4150
  })
4354
4151
  .then(function(res) { return res.json(); })
4355
4152
  .then(function(config) {
4356
- applySettingsConfig(config);
4153
+ state.config = config;
4357
4154
  var statusDot = document.getElementById("status-dot");
4358
4155
  var statusText = document.getElementById("status-text");
4359
4156
  if (statusDot) statusDot.classList.add("active");
@@ -4391,16 +4188,9 @@
4391
4188
  state.sessions = [];
4392
4189
  state.claudeHistory = [];
4393
4190
  state.claudeHistoryLoaded = false;
4394
- state.claudeHistoryExpanded = getConfiguredPanelDefaults().claudeHistoryExpanded;
4395
- persistHistoryPanelState();
4191
+ state.claudeHistoryExpanded = true;
4396
4192
  state.claudeHistoryExpandedDirs = {};
4397
- state.sessionsDrawerOpen = getConfiguredPanelDefaults().sessionsDrawerOpen;
4398
- persistDrawerState();
4399
- state.filePanelOpen = getConfiguredPanelDefaults().filePanelOpen;
4400
- persistFilePanelState();
4401
- state.shortcutsExpanded = getConfiguredPanelDefaults().shortcutsExpanded;
4402
- persistShortcutsExpandedState();
4403
- updateCollapsedShortcutsUi();
4193
+ state.sessionsDrawerOpen = false;
4404
4194
  render();
4405
4195
  }
4406
4196
 
@@ -4588,9 +4378,6 @@
4588
4378
 
4589
4379
  function applyCurrentView() {
4590
4380
  var hasSession = !!state.selectedId;
4591
- var terminalBtn = document.getElementById("view-terminal-btn");
4592
- var chatBtn = document.getElementById("view-chat-btn");
4593
- var toggleBar = document.getElementById("view-toggle-bar");
4594
4381
  var terminalContainer = document.getElementById("output");
4595
4382
  var chatContainer = document.getElementById("chat-output");
4596
4383
  var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
@@ -4603,17 +4390,6 @@
4603
4390
  state.currentView = "terminal";
4604
4391
  }
4605
4392
 
4606
- if (toggleBar) {
4607
- toggleBar.classList.toggle("hidden", !hasSession);
4608
- }
4609
- if (terminalBtn) {
4610
- terminalBtn.classList.toggle("hidden", structured || !hasSession);
4611
- terminalBtn.classList.toggle("active", showTerminal);
4612
- }
4613
- if (chatBtn) {
4614
- chatBtn.classList.toggle("hidden", !hasSession);
4615
- chatBtn.classList.toggle("active", showChat);
4616
- }
4617
4393
  if (terminalContainer) {
4618
4394
  terminalContainer.classList.toggle("active", showTerminal);
4619
4395
  terminalContainer.classList.toggle("hidden", !showTerminal);
@@ -5066,10 +4842,11 @@
5066
4842
 
5067
4843
  function toggleSessionsDrawer() {
5068
4844
  state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
5069
- persistDrawerState();
5070
4845
  if (state.sessionsDrawerOpen && isMobileLayout()) {
5071
4846
  state.filePanelOpen = false;
5072
- persistFilePanelState();
4847
+ try {
4848
+ localStorage.setItem("wand-file-panel-open", "false");
4849
+ } catch (e) {}
5073
4850
  }
5074
4851
  updateLayoutState();
5075
4852
  }
@@ -5078,7 +4855,6 @@
5078
4855
  if (!state.sessionsDrawerOpen) return;
5079
4856
  closeSwipedItem();
5080
4857
  state.sessionsDrawerOpen = false;
5081
- persistDrawerState();
5082
4858
  updateLayoutState();
5083
4859
  }
5084
4860
 
@@ -5323,7 +5099,6 @@
5323
5099
  function openSettingsModal() {
5324
5100
  // Close session modal first if open (mutual exclusion)
5325
5101
  closeSessionModal();
5326
- refreshSettingsModalUi();
5327
5102
  var modal = document.getElementById("settings-modal");
5328
5103
  if (modal) {
5329
5104
  modal.classList.remove("hidden");
@@ -5334,9 +5109,16 @@
5334
5109
  if (confirmEl) confirmEl.value = "";
5335
5110
  hideSettingsMessages();
5336
5111
  setupFocusTrap(modal);
5337
- bindSettingsModalEvents();
5112
+ // Activate first tab
5338
5113
  switchSettingsTab("about");
5114
+ // Load settings data
5339
5115
  loadSettingsData();
5116
+ // Sync notification preferences
5117
+ var soundEl = document.getElementById("cfg-notif-sound");
5118
+ var bubbleEl = document.getElementById("cfg-notif-bubble");
5119
+ if (soundEl) soundEl.checked = state.notifSound;
5120
+ if (bubbleEl) bubbleEl.checked = state.notifBubble;
5121
+ updateNotificationStatus();
5340
5122
  }
5341
5123
  }
5342
5124
 
@@ -5426,120 +5208,72 @@
5426
5208
  panels[j].classList.remove("active");
5427
5209
  }
5428
5210
  }
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
- }
5536
5211
  }
5537
5212
 
5538
5213
  function loadSettingsData() {
5539
5214
  fetch("/api/settings", { credentials: "same-origin" })
5540
5215
  .then(function(res) { return res.json(); })
5541
5216
  .then(function(data) {
5542
- applyLoadedSettingsData(data);
5217
+ // About
5218
+ var nameEl = document.getElementById("settings-pkg-name");
5219
+ var verEl = document.getElementById("settings-version");
5220
+ var nodeEl = document.getElementById("settings-node-req");
5221
+ var repoEl = document.getElementById("settings-repo-url");
5222
+ if (nameEl) nameEl.textContent = data.packageName || "-";
5223
+ if (verEl) verEl.textContent = data.version || "-";
5224
+ if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
5225
+ if (repoEl && data.repoUrl) {
5226
+ repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
5227
+ }
5228
+
5229
+ // Prefill update info if available
5230
+ var latestEl = document.getElementById("settings-latest-version");
5231
+ var updateBtn = document.getElementById("do-update-button");
5232
+ if (data.latestVersion && latestEl) {
5233
+ latestEl.textContent = data.latestVersion;
5234
+ if (data.updateAvailable && updateBtn) {
5235
+ updateBtn.classList.remove("hidden");
5236
+ }
5237
+ }
5238
+
5239
+ // Config fields
5240
+ var cfg = data.config || {};
5241
+ var hostEl = document.getElementById("cfg-host");
5242
+ var portEl = document.getElementById("cfg-port");
5243
+ var httpsEl = document.getElementById("cfg-https");
5244
+ var modeEl = document.getElementById("cfg-mode");
5245
+ var cwdEl = document.getElementById("cfg-cwd");
5246
+ var shellEl = document.getElementById("cfg-shell");
5247
+ if (hostEl) hostEl.value = cfg.host || "";
5248
+ if (portEl) portEl.value = cfg.port || "";
5249
+ if (httpsEl) httpsEl.checked = cfg.https === true;
5250
+ if (modeEl) modeEl.value = cfg.defaultMode || "default";
5251
+ if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
5252
+ if (shellEl) shellEl.value = cfg.shell || "";
5253
+ var langEl = document.getElementById("cfg-language");
5254
+ if (langEl) langEl.value = cfg.language || "";
5255
+
5256
+ // Cert status
5257
+ var certStatus = document.getElementById("cert-status");
5258
+ if (certStatus) {
5259
+ certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
5260
+ certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
5261
+ }
5262
+
5263
+ // Presets
5264
+ var presetsList = document.getElementById("presets-list");
5265
+ if (presetsList && cfg.commandPresets) {
5266
+ var html = "";
5267
+ for (var i = 0; i < cfg.commandPresets.length; i++) {
5268
+ var p = cfg.commandPresets[i];
5269
+ html += '<div class="preset-item">' +
5270
+ '<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
5271
+ '<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
5272
+ '</div>';
5273
+ }
5274
+ 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>';
5275
+ presetsList.innerHTML = html;
5276
+ }
5543
5277
  })
5544
5278
  .catch(function() {});
5545
5279
  }
@@ -5556,9 +5290,6 @@
5556
5290
  defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
5557
5291
  shell: (document.getElementById("cfg-shell") || {}).value,
5558
5292
  language: (document.getElementById("cfg-language") || {}).value || "",
5559
- uiPreferences: {
5560
- defaultPanelState: getPanelStateSettingsFormValues(),
5561
- }
5562
5293
  };
5563
5294
 
5564
5295
  fetch("/api/settings/config", {
@@ -5576,7 +5307,6 @@
5576
5307
  } else {
5577
5308
  msgEl.textContent = "配置已保存,部分更改需要重启后生效。";
5578
5309
  msgEl.style.color = "var(--success)";
5579
- handleSettingsConfigSaved(data.config);
5580
5310
  }
5581
5311
  msgEl.classList.remove("hidden");
5582
5312
  }
@@ -5590,7 +5320,6 @@
5590
5320
  });
5591
5321
  }
5592
5322
 
5593
-
5594
5323
  function uploadCertificates() {
5595
5324
  var keyFile = document.getElementById("cert-key-file");
5596
5325
  var certFile = document.getElementById("cert-cert-file");
@@ -5699,7 +5428,7 @@
5699
5428
  .then(function(res) { return res.json(); })
5700
5429
  .then(function(data) {
5701
5430
  if (msgEl) {
5702
- msgEl.textContent = data.message || data.error || "更新完成。";
5431
+ msgEl.textContent = data.message || data.error || "\u66f4\u65b0\u5b8c\u6210\u3002";
5703
5432
  msgEl.style.color = data.error ? "var(--error)" : "var(--success)";
5704
5433
  msgEl.classList.remove("hidden");
5705
5434
  }
@@ -5707,11 +5436,14 @@
5707
5436
  updateBtn.disabled = false;
5708
5437
  } else {
5709
5438
  updateBtn.classList.add("hidden");
5439
+ // Show restart button
5440
+ var restartBtn = document.getElementById("do-restart-button");
5441
+ if (restartBtn) restartBtn.classList.remove("hidden");
5710
5442
  }
5711
5443
  })
5712
5444
  .catch(function() {
5713
5445
  if (msgEl) {
5714
- msgEl.textContent = "更新失败。";
5446
+ msgEl.textContent = "\u66f4\u65b0\u5931\u8d25\u3002";
5715
5447
  msgEl.style.color = "var(--error)";
5716
5448
  msgEl.classList.remove("hidden");
5717
5449
  }
@@ -5719,11 +5451,18 @@
5719
5451
  });
5720
5452
  }
5721
5453
 
5454
+ function performSettingsRestart() {
5455
+ var restartBtn = document.getElementById("do-restart-button");
5456
+ var msgEl = document.getElementById("update-message");
5457
+ performRestart(restartBtn, msgEl);
5458
+ }
5459
+
5722
5460
  // ── Notification Settings Helpers ──
5723
5461
 
5724
5462
  function updateNotificationStatus() {
5725
5463
  var statusEl = document.getElementById("notification-permission-status");
5726
5464
  var requestBtn = document.getElementById("notification-request-btn");
5465
+ var resetBtn = document.getElementById("notification-reset-btn");
5727
5466
  var testMsgEl = document.getElementById("notification-test-message");
5728
5467
  if (!statusEl) return;
5729
5468
 
@@ -5731,6 +5470,7 @@
5731
5470
  statusEl.textContent = "\u4e0d\u652f\u6301";
5732
5471
  statusEl.style.color = "var(--fg-muted)";
5733
5472
  if (requestBtn) requestBtn.classList.add("hidden");
5473
+ if (resetBtn) resetBtn.classList.add("hidden");
5734
5474
  return;
5735
5475
  }
5736
5476
 
@@ -5739,45 +5479,86 @@
5739
5479
  statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
5740
5480
  statusEl.style.color = "var(--success)";
5741
5481
  if (requestBtn) requestBtn.classList.add("hidden");
5482
+ if (resetBtn) resetBtn.classList.add("hidden");
5742
5483
  } else if (perm === "denied") {
5743
5484
  statusEl.textContent = "\u5df2\u62d2\u7edd";
5744
5485
  statusEl.style.color = "var(--danger)";
5745
5486
  if (requestBtn) requestBtn.classList.add("hidden");
5746
- if (testMsgEl) {
5747
- testMsgEl.textContent = "\u6d4f\u89c8\u5668\u5df2\u62d2\u7edd\u901a\u77e5\u6743\u9650\uff0c\u8bf7\u5728\u6d4f\u89c8\u5668\u8bbe\u7f6e\u4e2d\u624b\u52a8\u5f00\u542f";
5748
- testMsgEl.style.color = "var(--fg-muted)";
5749
- testMsgEl.classList.remove("hidden");
5750
- }
5487
+ if (resetBtn) resetBtn.classList.remove("hidden");
5751
5488
  } else {
5752
5489
  statusEl.textContent = "\u672a\u6388\u6743";
5753
5490
  statusEl.style.color = "var(--warning)";
5754
5491
  if (requestBtn) requestBtn.classList.remove("hidden");
5492
+ if (resetBtn) resetBtn.classList.remove("hidden");
5755
5493
  }
5756
5494
  }
5757
5495
 
5496
+ function resetNotificationPermission() {
5497
+ var testMsgEl = document.getElementById("notification-test-message");
5498
+ if (typeof Notification === "undefined") return;
5499
+
5500
+ // Always call requestPermission — this triggers the browser's native
5501
+ // permission dialog when allowed. In "default" state it always works.
5502
+ // In "denied" state, some browsers (newer Chrome) re-prompt, others don't.
5503
+ Notification.requestPermission().then(function(result) {
5504
+ updateNotificationStatus();
5505
+ if (result === "granted") {
5506
+ if (testMsgEl) {
5507
+ testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
5508
+ testMsgEl.style.color = "var(--success)";
5509
+ testMsgEl.classList.remove("hidden");
5510
+ }
5511
+ } else if (result === "denied") {
5512
+ // Browser blocked re-prompting — show inline guide with site-settings shortcut
5513
+ if (testMsgEl) {
5514
+ var origin = location.origin;
5515
+ testMsgEl.innerHTML =
5516
+ "\u6d4f\u89c8\u5668\u5df2\u62e6\u622a\u6388\u6743\u5f39\u7a97\uff0c\u8bf7\u624b\u52a8\u91cd\u7f6e\uff1a<br>" +
5517
+ '<span style="display:inline-flex;align-items:center;gap:4px;margin:4px 0">' +
5518
+ "\u2460 \u70b9\u51fb\u5730\u5740\u680f\u5de6\u4fa7\u7684 " +
5519
+ '<span style="display:inline-flex;align-items:center;justify-content:center;' +
5520
+ "width:16px;height:16px;border-radius:50%;border:1px solid var(--border);" +
5521
+ 'font-size:11px;vertical-align:middle">i</span>' +
5522
+ " \u6216\u9501\u56fe\u6807" +
5523
+ "</span><br>" +
5524
+ "\u2461 \u627e\u5230\u300c\u901a\u77e5\u300d\u2192 \u6539\u4e3a\u300c\u5141\u8bb8\u300d<br>" +
5525
+ "\u2462 \u5237\u65b0\u9875\u9762\u5373\u53ef";
5526
+ testMsgEl.style.color = "var(--fg-muted)";
5527
+ testMsgEl.classList.remove("hidden");
5528
+ }
5529
+ }
5530
+ });
5531
+ }
5532
+
5758
5533
  function testNotification() {
5759
5534
  var testMsgEl = document.getElementById("notification-test-message");
5535
+ var results = [];
5760
5536
 
5761
- // Always show in-app bubble
5537
+ // 1. Test sound playback
5538
+ var soundOk = tryPlayNotificationSound();
5539
+ results.push(soundOk ? "\u2713 \u63d0\u793a\u97f3" : "\u2717 \u63d0\u793a\u97f3\uff08\u65e0\u6cd5\u64ad\u653e\uff09");
5540
+
5541
+ // 2. Test in-app bubble
5542
+ var bubbleEnabled = state.notifBubble;
5762
5543
  showNotificationBubble({
5763
5544
  title: "\u6d4b\u8bd5\u901a\u77e5",
5764
- body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\uff0c\u5e94\u7528\u5185\u6c14\u6ce1\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
5545
+ body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\u3002",
5765
5546
  type: "info",
5766
5547
  icon: "\u266a",
5767
5548
  duration: 5000,
5549
+ playSound: false, // sound already played above
5768
5550
  });
5551
+ results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
5769
5552
 
5770
- // Test browser notification
5553
+ // 3. Test browser notification
5771
5554
  if (typeof Notification === "undefined") {
5772
- if (testMsgEl) {
5773
- testMsgEl.textContent = "\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u901a\u77e5 API\uff0c\u4ec5\u53ef\u4f7f\u7528\u5e94\u7528\u5185\u6c14\u6ce1\u901a\u77e5\u3002";
5774
- testMsgEl.style.color = "var(--fg-muted)";
5775
- testMsgEl.classList.remove("hidden");
5776
- }
5555
+ results.push("\u2013 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
5556
+ showTestResults(testMsgEl, results);
5777
5557
  return;
5778
5558
  }
5779
5559
 
5780
- if (Notification.permission === "granted") {
5560
+ var perm = Notification.permission;
5561
+ if (perm === "granted") {
5781
5562
  try {
5782
5563
  var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
5783
5564
  body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
@@ -5785,35 +5566,37 @@
5785
5566
  tag: "wand-test",
5786
5567
  });
5787
5568
  setTimeout(function() { n.close(); }, 5000);
5788
- if (testMsgEl) {
5789
- testMsgEl.textContent = "\u2713 \u6d4f\u89c8\u5668\u901a\u77e5 + \u5e94\u7528\u5185\u6c14\u6ce1\u5747\u5df2\u53d1\u9001";
5790
- testMsgEl.style.color = "var(--success)";
5791
- testMsgEl.classList.remove("hidden");
5792
- }
5569
+ results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5");
5793
5570
  } catch (_e) {
5794
- if (testMsgEl) {
5795
- testMsgEl.textContent = "\u6d4f\u89c8\u5668\u901a\u77e5\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS";
5796
- testMsgEl.style.color = "var(--warning)";
5797
- testMsgEl.classList.remove("hidden");
5798
- }
5571
+ results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
5799
5572
  }
5573
+ showTestResults(testMsgEl, results);
5574
+ } else if (perm === "denied") {
5575
+ results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
5576
+ showTestResults(testMsgEl, results);
5800
5577
  } else {
5801
- // permission is "default" or "denied" always try requesting
5578
+ // "default" — try requesting
5802
5579
  Notification.requestPermission().then(function(result) {
5803
5580
  updateNotificationStatus();
5804
5581
  if (result === "granted") {
5805
- testNotification();
5806
- } else if (result === "denied") {
5807
- if (testMsgEl) {
5808
- testMsgEl.textContent = "\u6d4f\u89c8\u5668\u5df2\u62d2\u7edd\u901a\u77e5\u6743\u9650\uff0c\u8bf7\u70b9\u51fb\u5730\u5740\u680f\u5de6\u4fa7\u9501\u56fe\u6807\u6216\u5728\u6d4f\u89c8\u5668\u8bbe\u7f6e\u4e2d\u624b\u52a8\u5f00\u542f";
5809
- testMsgEl.style.color = "var(--fg-muted)";
5810
- testMsgEl.classList.remove("hidden");
5811
- }
5582
+ results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
5583
+ } else {
5584
+ results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
5812
5585
  }
5586
+ showTestResults(testMsgEl, results);
5813
5587
  });
5814
5588
  }
5815
5589
  }
5816
5590
 
5591
+ function showTestResults(el, results) {
5592
+ if (!el) return;
5593
+ el.innerHTML = results.map(function(r) { return escapeHtml(r); }).join("<br>");
5594
+ // color based on whether all passed
5595
+ var allOk = results.every(function(r) { return r.indexOf("\u2713") === 0 || r.indexOf("\u2013") === 0; });
5596
+ el.style.color = allOk ? "var(--success)" : "var(--warning)";
5597
+ el.classList.remove("hidden");
5598
+ }
5599
+
5817
5600
  function quickStartSession() {
5818
5601
  var command = getPreferredTool();
5819
5602
  var defaultCwd = getEffectiveCwd();
@@ -6813,13 +6596,17 @@
6813
6596
  var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
6814
6597
  userMsgs.push(userTurn);
6815
6598
  var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
6599
+ // Write optimistic user turn into session.messages so WS updates
6600
+ // that arrive before the HTTP response don't erase it.
6816
6601
  updateSessionSnapshot({
6817
6602
  id: session.id,
6818
6603
  status: "running",
6604
+ messages: userMsgs,
6819
6605
  structuredState: optimisticStructuredState,
6820
6606
  });
6821
6607
  state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
6822
6608
  status: "running",
6609
+ messages: userMsgs,
6823
6610
  structuredState: optimisticStructuredState,
6824
6611
  }), userMsgs);
6825
6612
  updateInputHint("思考中…");
@@ -6832,6 +6619,11 @@
6832
6619
  }
6833
6620
  setDraftValue("");
6834
6621
 
6622
+ // Capture queue epoch before the POST so we can detect whether
6623
+ // a newer WS update has already refreshed the queue by the time
6624
+ // the HTTP response arrives.
6625
+ var epochBeforePost = state.queueEpoch;
6626
+
6835
6627
  return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
6836
6628
  method: "POST",
6837
6629
  headers: { "Content-Type": "application/json" },
@@ -6844,13 +6636,21 @@
6844
6636
  throw new Error(snapshot.error);
6845
6637
  }
6846
6638
  if (snapshot && snapshot.id) {
6639
+ // If a WS update has already bumped the queue epoch, the HTTP
6640
+ // response's queuedMessages is stale — drop it to avoid
6641
+ // re-introducing already-dequeued items.
6642
+ if (state.queueEpoch > epochBeforePost && snapshot.queuedMessages) {
6643
+ delete snapshot.queuedMessages;
6644
+ }
6847
6645
  updateSessionSnapshot(snapshot);
6848
6646
  var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
6849
6647
  state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
6850
6648
  renderChat(true);
6851
6649
  if (isQueueing) {
6852
6650
  var queuedCount = getStructuredQueuedInputs(refreshedSession).length;
6853
- showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
6651
+ if (queuedCount > 0) {
6652
+ showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
6653
+ }
6854
6654
  } else {
6855
6655
  updateInputHint("Enter 发送 · Shift+Enter 换行");
6856
6656
  }
@@ -6902,7 +6702,25 @@
6902
6702
  }
6903
6703
  var queued = getStructuredQueuedInputs(session);
6904
6704
  if (queued && queued.length > 0) {
6705
+ // Collect recent user message texts to deduplicate against queued items.
6706
+ // A queued message that already appears as a real user turn should not
6707
+ // be rendered a second time with the "排队中" badge.
6708
+ var existingUserTexts = {};
6709
+ for (var ei = base.length - 1; ei >= 0 && Object.keys(existingUserTexts).length < queued.length + 5; ei--) {
6710
+ var em = base[ei];
6711
+ if (em && em.role === "user" && Array.isArray(em.content)) {
6712
+ for (var ej = 0; ej < em.content.length; ej++) {
6713
+ if (em.content[ej] && em.content[ej].type === "text" && em.content[ej].text) {
6714
+ existingUserTexts[em.content[ej].text] = (existingUserTexts[em.content[ej].text] || 0) + 1;
6715
+ }
6716
+ }
6717
+ }
6718
+ }
6905
6719
  for (var qi = 0; qi < queued.length; qi++) {
6720
+ if (existingUserTexts[queued[qi]]) {
6721
+ existingUserTexts[queued[qi]]--;
6722
+ continue; // Skip — this queued text is already shown as a real message
6723
+ }
6906
6724
  base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
6907
6725
  }
6908
6726
  }
@@ -8017,24 +7835,7 @@
8017
7835
 
8018
7836
  function handleInputBoxBlur() {
8019
7837
  resetInputPanelViewportSpacing();
8020
- // Restore app container height when keyboard closes.
8021
- // Use a short delay because on iOS the visualViewport may not
8022
- // have updated yet at the moment blur fires.
8023
7838
  setTimeout(function() {
8024
- var appContainer = document.querySelector('.app-container');
8025
- if (appContainer) {
8026
- // Only clear if keyboard is actually closed now
8027
- var vv = window.visualViewport;
8028
- if (vv) {
8029
- var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
8030
- if (offsetBottom <= 50) {
8031
- appContainer.style.height = '';
8032
- }
8033
- } else {
8034
- appContainer.style.height = '';
8035
- }
8036
- }
8037
- // Scroll the window back to top to fix any residual offset
8038
7839
  window.scrollTo(0, 0);
8039
7840
  }, 100);
8040
7841
  }
@@ -8757,20 +8558,6 @@
8757
8558
  var isKeyboardOpen = offsetBottom > 50;
8758
8559
  var heightChanged = Math.abs(vv.height - lastHeight) > 8;
8759
8560
 
8760
- // Dynamically resize the app container to match visible viewport.
8761
- // This is needed because 100dvh does NOT shrink when the keyboard
8762
- // appears in PWA standalone mode, and on some browsers the layout
8763
- // viewport doesn't update on keyboard dismiss without this.
8764
- var appContainer = document.querySelector('.app-container');
8765
- if (appContainer) {
8766
- if (isKeyboardOpen) {
8767
- appContainer.style.height = vv.height + 'px';
8768
- } else if (keyboardOpen) {
8769
- // Keyboard just closed — clear forced height
8770
- appContainer.style.height = '';
8771
- }
8772
- }
8773
-
8774
8561
  if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
8775
8562
  syncInputBoxScroll(inputBox);
8776
8563
  }
@@ -9095,8 +8882,9 @@
9095
8882
  if (msg.data.messages) {
9096
8883
  snapshot.messages = msg.data.messages;
9097
8884
  }
9098
- if (msg.data.queuedMessages) {
9099
- snapshot.queuedMessages = msg.data.queuedMessages;
8885
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
8886
+ snapshot.queuedMessages = msg.data.queuedMessages || [];
8887
+ state.queueEpoch++;
9100
8888
  }
9101
8889
  if (msg.data.structuredState) {
9102
8890
  snapshot.structuredState = msg.data.structuredState;
@@ -9284,6 +9072,10 @@
9284
9072
  });
9285
9073
  }
9286
9074
  }
9075
+ if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
9076
+ statusUpdate.queuedMessages = msg.data.queuedMessages || [];
9077
+ state.queueEpoch++;
9078
+ }
9287
9079
  if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
9288
9080
  statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
9289
9081
  }
@@ -9337,12 +9129,13 @@
9337
9129
  }
9338
9130
  // Re-render chat when structured session inFlight state changes
9339
9131
  if (statusUpdate.structuredState) {
9340
- scheduleChatRender();
9341
- // Flush queued structured messages when inFlight clears
9132
+ // Flush queued structured messages synchronously before render
9133
+ // so the chat view uses up-to-date queue state.
9342
9134
  if (!statusUpdate.structuredState.inFlight) {
9343
9135
  updateInputHint("Enter 发送 · Shift+Enter 换行");
9344
- setTimeout(flushStructuredInputQueue, 50);
9136
+ flushStructuredInputQueue();
9345
9137
  }
9138
+ scheduleChatRender();
9346
9139
  }
9347
9140
  }
9348
9141
  }
@@ -9350,23 +9143,14 @@
9350
9143
  case 'notification':
9351
9144
  if (msg.data) {
9352
9145
  if (msg.data.kind === "update") {
9353
- showNotificationBubble({
9354
- title: "\u53d1\u73b0\u65b0\u7248\u672c",
9355
- body: "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
9356
- type: "info",
9357
- icon: "\u2191",
9358
- duration: 0,
9359
- actionLabel: "\u53bb\u66f4\u65b0",
9360
- action: function() {
9361
- var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
9362
- if (settingsBtn) settingsBtn.click();
9363
- }
9364
- });
9146
+ showUpdateBubble(msg.data.current || "-", msg.data.latest || "-");
9365
9147
  sendBrowserNotification(
9366
9148
  "Wand \u53d1\u73b0\u65b0\u7248\u672c",
9367
9149
  "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
9368
9150
  { tag: "wand-update" }
9369
9151
  );
9152
+ } else if (msg.data.kind === "restart") {
9153
+ showRestartOverlay();
9370
9154
  }
9371
9155
  }
9372
9156
  break;
@@ -9869,12 +9653,12 @@
9869
9653
  if (cards.length > 0) {
9870
9654
  var firstCard = cards[0];
9871
9655
  var firstCardKey = getElementExpandKey(firstCard);
9872
- if (!hasPersistedExpandState(firstCardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
9656
+ if (getPersistedExpandState(firstCardKey) === null) {
9873
9657
  firstCard.classList.remove("collapsed");
9874
9658
  }
9875
9659
  for (var ci = 1; ci < cards.length; ci++) {
9876
9660
  var cardKey = getElementExpandKey(cards[ci]);
9877
- if (!hasPersistedExpandState(cardKey) && !getConfiguredPanelDefaults().structuredToolCardExpanded) {
9661
+ if (getPersistedExpandState(cardKey) === null) {
9878
9662
  cards[ci].classList.add("collapsed");
9879
9663
  }
9880
9664
  }
@@ -9892,7 +9676,7 @@
9892
9676
  var allCards = container.querySelectorAll(".tool-use-card");
9893
9677
  allCards.forEach(function(c) {
9894
9678
  var cardKey = getElementExpandKey(c);
9895
- if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
9679
+ if (getPersistedExpandState(cardKey) !== null) return;
9896
9680
  // Keep expanded if this card is inside a newly added message
9897
9681
  if (newEls) {
9898
9682
  for (var i = 0; i < newEls.length; i++) {
@@ -10016,7 +9800,7 @@
10016
9800
  var newestCard = null;
10017
9801
  allCards.forEach(function(c) {
10018
9802
  var cardKey = getElementExpandKey(c);
10019
- if (hasPersistedExpandState(cardKey) || getConfiguredPanelDefaults().structuredToolCardExpanded) return;
9803
+ if (getPersistedExpandState(cardKey) !== null) return;
10020
9804
  if (newestMsgEl && newestMsgEl.contains(c)) {
10021
9805
  if (!newestCard) newestCard = c;
10022
9806
  else c.classList.add("collapsed");
@@ -10043,7 +9827,7 @@
10043
9827
  updateChatJumpToBottomButton();
10044
9828
  return;
10045
9829
  }
10046
- var chatMsgs = container && container.classList && container.classList.contains("chat-messages")
9830
+ var chatMsgs = (container && container.classList && container.classList.contains("chat-messages"))
10047
9831
  ? container
10048
9832
  : getChatScrollElement();
10049
9833
  if (!chatMsgs || !chatMsgs.isConnected) return;
@@ -11106,7 +10890,7 @@
11106
10890
  // Thinking card (deep thought) — from PTY parsing
11107
10891
  if (msg.role === "thinking") {
11108
10892
  var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
11109
- var thinkingExpanded = getExpandState(thinkingKey, "thinking");
10893
+ var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11110
10894
  return '<div class="chat-message thinking">' +
11111
10895
  '<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
11112
10896
  '<span class="thinking-inline-icon">⦿</span>' +
@@ -11243,7 +11027,8 @@
11243
11027
  }
11244
11028
  var summaryText = parts.join(" · ");
11245
11029
  var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
11246
- var shouldExpand = getExpandState(groupKey, "tool-group");
11030
+ var persistedExpanded = getPersistedExpandState(groupKey);
11031
+ var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11247
11032
 
11248
11033
  // Render each item's inline-tool card
11249
11034
  var innerHtml = "";
@@ -11354,7 +11139,7 @@
11354
11139
  '</div>';
11355
11140
  }
11356
11141
  var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
11357
- var thinkingExpanded = getExpandState(thinkingKey, "thinking");
11142
+ var thinkingExpanded = getPersistedExpandState(thinkingKey) === true;
11358
11143
  return '<div class="thinking-inline ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="' + escapeHtml(thinkingText) + '" onclick="__thinkingToggle(this)">' +
11359
11144
  '<span class="thinking-inline-icon">⦿</span>' +
11360
11145
  '<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
@@ -11380,6 +11165,7 @@
11380
11165
  function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo, messageKey, index) {
11381
11166
  var toolId = block.id || "tool-" + toolName;
11382
11167
  var expandKey = buildExpandKey("inline-tool", [messageKey, toolId || index, index]);
11168
+ var persistedExpanded = getPersistedExpandState(expandKey);
11383
11169
  var inputData = block.input || {};
11384
11170
  var resultContent = extractToolResultText(toolResult && toolResult.content);
11385
11171
 
@@ -11450,7 +11236,7 @@
11450
11236
  var fullResult = resultContent;
11451
11237
 
11452
11238
  var expandedHtml = "";
11453
- var shouldExpand = getExpandState(expandKey, "inline-tool");
11239
+ var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11454
11240
  if (hasResult) {
11455
11241
  expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
11456
11242
  '<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
@@ -11490,6 +11276,7 @@
11490
11276
  var resultContent = extractToolResultText(toolResult && toolResult.content);
11491
11277
  var toolId = block.id || "tool-" + toolName;
11492
11278
  var expandKey = buildExpandKey("terminal", [messageKey, toolId || index, index]);
11279
+ var persistedExpanded = getPersistedExpandState(expandKey);
11493
11280
 
11494
11281
  var isError = toolResult && toolResult.is_error;
11495
11282
  var exitCode = inputData.exitCode;
@@ -11527,7 +11314,7 @@
11527
11314
 
11528
11315
  // Show command preview in header (truncate long commands)
11529
11316
  var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
11530
- var shouldExpand = getExpandState(expandKey, "terminal");
11317
+ var shouldExpand = persistedExpanded === null ? false : persistedExpanded;
11531
11318
 
11532
11319
  return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
11533
11320
  '<div class="term-header" onclick="__terminalExpand(this)">' +
@@ -11653,33 +11440,116 @@
11653
11440
  return renderDiffTool(block, toolResult, toolName);
11654
11441
  }
11655
11442
 
11656
- // ── AskUserQuestion tool — special card
11443
+ // ── AskUserQuestion tool — special card with batch submit
11657
11444
  if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
11658
11445
  var questions = block.input.questions;
11659
11446
  if (questions && questions.length > 0) {
11447
+ var isAnswered = !!toolResult;
11448
+ var sel = state.askUserSelections[toolId] || {};
11449
+ var isSubmitted = !!sel.submitted;
11450
+ var answerText = isAnswered ? extractToolResultText(toolResult.content) : "";
11451
+ var answerLines = answerText ? answerText.trim().split("\n") : [];
11452
+
11453
+ // Build header summary
11454
+ var headerLabel = "";
11455
+ for (var hi = 0; hi < questions.length; hi++) {
11456
+ if (questions[hi].header) { headerLabel = questions[hi].header; break; }
11457
+ }
11458
+ var headerSummary = headerLabel ? '<span class="tool-use-summary">' + escapeHtml(headerLabel) + '</span>' : "";
11459
+
11660
11460
  var questionsHtml = "";
11661
11461
  questions.forEach(function(question, qIdx) {
11462
+ var isMulti = !!question.multiSelect;
11662
11463
  var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
11663
11464
  var optionsHtml = "";
11664
11465
  if (question.options && question.options.length > 0) {
11665
- optionsHtml = '<div class="ask-user-options">';
11466
+ optionsHtml = '<div class="ask-user-options" data-multi-select="' + isMulti + '">';
11666
11467
  question.options.forEach(function(opt, idx) {
11667
11468
  var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
11668
- optionsHtml += '<button class="ask-user-option" data-option-index="' + idx + '" data-question-index="' + qIdx + '" data-option-label="' + escapeHtml(label) + '" onclick="__askOption(this)">' +
11669
- '<div class="ask-user-option-label">' + label + '</div>' +
11670
- '</button>';
11469
+ var descHtml = opt.description ? '<div class="ask-user-option-desc">' + escapeHtml(opt.description) + '</div>' : "";
11470
+
11471
+ if (isAnswered) {
11472
+ // Read-only: check if this option was the chosen answer
11473
+ var answerLine = answerLines[qIdx] || answerLines[0] || "";
11474
+ var chosenLabels = answerLine.split(",").map(function(s) { return s.trim(); });
11475
+ var isChosen = chosenLabels.indexOf(opt.label || "") !== -1;
11476
+ optionsHtml += '<div class="ask-user-option ask-user-option-readonly' + (isChosen ? ' ask-user-option-chosen' : '') + '">' +
11477
+ '<span class="ask-user-indicator"></span>' +
11478
+ '<div class="ask-user-option-content">' +
11479
+ '<div class="ask-user-option-label">' + label + '</div>' +
11480
+ descHtml +
11481
+ '</div>' +
11482
+ '</div>';
11483
+ } else {
11484
+ // Interactive: selection state from askUserSelections
11485
+ var isSelected = (sel[qIdx] || []).indexOf(idx) !== -1;
11486
+ var disabledAttr = isSubmitted ? ' disabled' : '';
11487
+ optionsHtml += '<button class="ask-user-option' + (isSelected ? ' selected' : '') + '"' +
11488
+ ' data-option-index="' + idx + '"' +
11489
+ ' data-question-index="' + qIdx + '"' +
11490
+ ' data-option-label="' + escapeHtml(opt.label || "选项 " + (idx + 1)) + '"' +
11491
+ ' onclick="__askSelect(\'' + escapeHtml(toolId) + '\',' + qIdx + ',' + idx + ',' + isMulti + ')"' +
11492
+ disabledAttr + '>' +
11493
+ '<span class="ask-user-indicator"></span>' +
11494
+ '<div class="ask-user-option-content">' +
11495
+ '<div class="ask-user-option-label">' + label + '</div>' +
11496
+ descHtml +
11497
+ '</div>' +
11498
+ '</button>';
11499
+ }
11671
11500
  });
11672
11501
  optionsHtml += '</div>';
11673
11502
  }
11674
- questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
11503
+ questionsHtml += '<div class="ask-user-question-group" data-question-index="' + qIdx + '">' + questionText + optionsHtml + '</div>';
11675
11504
  });
11676
- return '<div class="tool-use-card ask-user" data-tool-use-id="' + escapeHtml(toolId) + '">' +
11505
+
11506
+ // Submit button (only for interactive state)
11507
+ var actionsHtml = "";
11508
+ if (!isAnswered) {
11509
+ var allAnsweredCheck = true;
11510
+ for (var qi = 0; qi < questions.length; qi++) {
11511
+ if (!sel[qi] || sel[qi].length === 0) { allAnsweredCheck = false; break; }
11512
+ }
11513
+ var submitDisabled = (!allAnsweredCheck || isSubmitted) ? " disabled" : "";
11514
+ var submitClass = isSubmitted ? " ask-user-submitted" : "";
11515
+ var submitText = isSubmitted ? "已提交..." : "确认提交";
11516
+ actionsHtml = '<div class="ask-user-actions">' +
11517
+ '<button class="ask-user-submit' + submitClass + '" data-tool-use-id="' + escapeHtml(toolId) + '"' +
11518
+ ' onclick="__askSubmit(\'' + escapeHtml(toolId) + '\')"' + submitDisabled + '>' +
11519
+ submitText +
11520
+ '</button>' +
11521
+ '</div>';
11522
+ }
11523
+
11524
+ // Answered summary for header
11525
+ var answeredSummary = "";
11526
+ if (isAnswered && answerText) {
11527
+ var shortAnswer = answerText.trim().replace(/\n/g, ", ");
11528
+ if (shortAnswer.length > 40) shortAnswer = shortAnswer.slice(0, 37) + "...";
11529
+ answeredSummary = '<span class="tool-use-file">' + escapeHtml(shortAnswer) + '</span>';
11530
+ }
11531
+
11532
+ // Expand state: default expanded when unanswered, collapsed when answered
11533
+ var askExpandKey = buildExpandKey("tool-card", [messageKey, toolId]);
11534
+ var askPersisted = getPersistedExpandState(askExpandKey);
11535
+ var askShouldExpand = askPersisted === null ? !isAnswered : askPersisted;
11536
+ var askCollapsed = askShouldExpand ? "" : " collapsed";
11537
+ var answeredClass = isAnswered ? " ask-user-answered" : "";
11538
+
11539
+ return '<div class="tool-use-card ask-user' + answeredClass + askCollapsed + '"' +
11540
+ ' data-tool-use-id="' + escapeHtml(toolId) + '"' +
11541
+ ' data-expand-kind="tool-card"' +
11542
+ ' data-expand-key="' + escapeHtml(askExpandKey) + '">' +
11677
11543
  '<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
11678
- '<span class="tool-use-icon">?</span>' +
11544
+ '<span class="tool-use-icon">' + (isAnswered ? '✓' : '?') + '</span>' +
11679
11545
  '<span class="tool-use-name">提问</span>' +
11546
+ headerSummary +
11547
+ answeredSummary +
11548
+ '<span class="tool-use-toggle">▼</span>' +
11680
11549
  '</div>' +
11681
11550
  '<div class="tool-use-body ask-user-body">' +
11682
11551
  questionsHtml +
11552
+ actionsHtml +
11683
11553
  '</div>' +
11684
11554
  '</div>';
11685
11555
  }
@@ -11725,7 +11595,8 @@
11725
11595
  }
11726
11596
 
11727
11597
  var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
11728
- var shouldExpand = getExpandState(expandKey, "tool-card", statusClass === "loading");
11598
+ var persistedExpanded = getPersistedExpandState(expandKey);
11599
+ var shouldExpand = persistedExpanded === null ? statusClass === "loading" : persistedExpanded;
11729
11600
  var collapsedClass = shouldExpand ? "" : " collapsed";
11730
11601
  var toggleHtml = '<span class="tool-use-toggle">▼</span>';
11731
11602
  return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
@@ -12149,8 +12020,8 @@
12149
12020
 
12150
12021
  var notificationStack = [];
12151
12022
  var notificationIdCounter = 0;
12152
- var NOTIFICATION_GAP = 8;
12153
- var NOTIFICATION_TOP = 24;
12023
+ var NOTIFICATION_GAP = 6;
12024
+ var NOTIFICATION_TOP = 16;
12154
12025
 
12155
12026
  /**
12156
12027
  * Show an in-app notification bubble at bottom-right.
@@ -12165,6 +12036,12 @@
12165
12036
  * @returns {{ dismiss: function }} handle
12166
12037
  */
12167
12038
  function showNotificationBubble(opts) {
12039
+ // Play sound for important notifications — independent of bubble setting
12040
+ if (opts.actionLabel || opts.playSound) playNotificationSound();
12041
+
12042
+ // Respect user preference (skip if bubbles disabled)
12043
+ if (!state.notifBubble) return { dismiss: function() {} };
12044
+
12168
12045
  var id = ++notificationIdCounter;
12169
12046
  var type = opts.type || "info";
12170
12047
  var icon = opts.icon || (type === "warning" ? "!" : type === "success" ? "\u2713" : "i");
@@ -12275,6 +12152,221 @@
12275
12152
  }
12276
12153
  }
12277
12154
 
12155
+ /**
12156
+ * Play a soft, rounded notification chime using Web Audio API.
12157
+ * Two ascending sine tones with smooth gain envelope — gentle on the ears.
12158
+ */
12159
+ function playNotificationSound() {
12160
+ if (!state.notifSound) return;
12161
+ _doPlaySound();
12162
+ }
12163
+
12164
+ /**
12165
+ * Try to play the notification sound regardless of user preference.
12166
+ * Returns true if playback was initiated successfully.
12167
+ * Used by the test function to always attempt playback.
12168
+ */
12169
+ function tryPlayNotificationSound() {
12170
+ return _doPlaySound();
12171
+ }
12172
+
12173
+ function _doPlaySound() {
12174
+ try {
12175
+ var AudioCtx = window.AudioContext || window.webkitAudioContext;
12176
+ if (!AudioCtx) return false;
12177
+ var ctx = new AudioCtx();
12178
+
12179
+ // Some browsers suspend AudioContext until user gesture — resume it
12180
+ if (ctx.state === "suspended") ctx.resume();
12181
+
12182
+ function tone(freq, start, dur) {
12183
+ var osc = ctx.createOscillator();
12184
+ var gain = ctx.createGain();
12185
+ osc.type = "sine";
12186
+ osc.frequency.value = freq;
12187
+ gain.gain.setValueAtTime(0, ctx.currentTime + start);
12188
+ gain.gain.linearRampToValueAtTime(0.18, ctx.currentTime + start + 0.04);
12189
+ gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur);
12190
+ osc.connect(gain);
12191
+ gain.connect(ctx.destination);
12192
+ osc.start(ctx.currentTime + start);
12193
+ osc.stop(ctx.currentTime + start + dur);
12194
+ }
12195
+
12196
+ // Two-tone ascending chime: C5 → E5, soft and brief
12197
+ tone(523, 0, 0.25);
12198
+ tone(659, 0.12, 0.3);
12199
+
12200
+ // Clean up context after playback
12201
+ setTimeout(function() { ctx.close(); }, 600);
12202
+ return true;
12203
+ } catch (_e) {
12204
+ // Web Audio not available or blocked
12205
+ return false;
12206
+ }
12207
+ }
12208
+
12209
+ /**
12210
+ * Show an interactive update bubble that allows updating and restarting
12211
+ * directly from the notification, without navigating to settings.
12212
+ */
12213
+ function showUpdateBubble(currentVer, latestVer) {
12214
+ // Prevent duplicate bubbles
12215
+ if (state._updateBubbleShown) return;
12216
+ state._updateBubbleShown = true;
12217
+
12218
+ playNotificationSound();
12219
+
12220
+ var id = ++notificationIdCounter;
12221
+ var bubble = document.createElement("div");
12222
+ bubble.className = "notification-bubble";
12223
+ bubble.setAttribute("data-nid", id);
12224
+
12225
+ bubble.innerHTML =
12226
+ '<div class="notification-bubble-header">' +
12227
+ '<span class="notification-bubble-icon info">\u2191</span>' +
12228
+ '<span class="notification-bubble-title">\u53d1\u73b0\u65b0\u7248\u672c</span>' +
12229
+ '<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
12230
+ '</div>' +
12231
+ '<div class="notification-bubble-body">' +
12232
+ escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
12233
+ '</div>' +
12234
+ '<div class="notification-bubble-actions">' +
12235
+ '<button class="primary" id="update-bubble-action">\u7acb\u5373\u66f4\u65b0</button>' +
12236
+ '</div>';
12237
+
12238
+ document.body.appendChild(bubble);
12239
+
12240
+ var entry = { id: id, el: bubble };
12241
+ notificationStack.push(entry);
12242
+ repositionNotifications();
12243
+
12244
+ var closeBtn = bubble.querySelector(".notification-bubble-close");
12245
+ if (closeBtn) closeBtn.onclick = function() {
12246
+ dismissNotification(id);
12247
+ state._updateBubbleShown = false;
12248
+ };
12249
+
12250
+ var actionBtn = bubble.querySelector("#update-bubble-action");
12251
+ var bodyEl = bubble.querySelector(".notification-bubble-body");
12252
+
12253
+ if (actionBtn) actionBtn.onclick = function() {
12254
+ // Phase 1: Performing update
12255
+ actionBtn.disabled = true;
12256
+ actionBtn.textContent = "\u66f4\u65b0\u4e2d\u2026";
12257
+ if (bodyEl) bodyEl.textContent = "\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026";
12258
+
12259
+ fetch("/api/update", {
12260
+ method: "POST",
12261
+ headers: { "Content-Type": "application/json" },
12262
+ credentials: "same-origin"
12263
+ })
12264
+ .then(function(res) { return res.json(); })
12265
+ .then(function(data) {
12266
+ if (data.error) {
12267
+ // Update failed
12268
+ if (bodyEl) {
12269
+ bodyEl.textContent = data.error;
12270
+ bodyEl.style.color = "var(--error)";
12271
+ }
12272
+ actionBtn.disabled = false;
12273
+ actionBtn.textContent = "\u91cd\u8bd5";
12274
+ return;
12275
+ }
12276
+ // Phase 2: Update succeeded, show restart button
12277
+ if (bodyEl) {
12278
+ bodyEl.textContent = data.message || "\u66f4\u65b0\u5b8c\u6210";
12279
+ bodyEl.style.color = "var(--success)";
12280
+ }
12281
+ actionBtn.textContent = "\u91cd\u542f\u751f\u6548";
12282
+ actionBtn.disabled = false;
12283
+ actionBtn.className = "primary success";
12284
+ actionBtn.onclick = function() {
12285
+ performRestart(actionBtn, bodyEl);
12286
+ };
12287
+ })
12288
+ .catch(function() {
12289
+ if (bodyEl) {
12290
+ bodyEl.textContent = "\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002";
12291
+ bodyEl.style.color = "var(--error)";
12292
+ }
12293
+ actionBtn.disabled = false;
12294
+ actionBtn.textContent = "\u91cd\u8bd5";
12295
+ });
12296
+ };
12297
+ }
12298
+
12299
+ /**
12300
+ * Call POST /api/restart and show the restart overlay.
12301
+ */
12302
+ function performRestart(btn, msgEl) {
12303
+ if (btn) {
12304
+ btn.disabled = true;
12305
+ btn.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
12306
+ }
12307
+ if (msgEl) {
12308
+ msgEl.textContent = "\u670d\u52a1\u6b63\u5728\u91cd\u542f\u2026";
12309
+ msgEl.style.color = "var(--text-secondary)";
12310
+ }
12311
+
12312
+ fetch("/api/restart", {
12313
+ method: "POST",
12314
+ headers: { "Content-Type": "application/json" },
12315
+ credentials: "same-origin"
12316
+ })
12317
+ .then(function(res) { return res.json(); })
12318
+ .then(function() {
12319
+ showRestartOverlay();
12320
+ })
12321
+ .catch(function() {
12322
+ // Network error likely means server already shut down — show overlay anyway
12323
+ showRestartOverlay();
12324
+ });
12325
+ }
12326
+
12327
+ /**
12328
+ * Full-screen overlay shown during server restart.
12329
+ * Polls /api/config until the server comes back, then reloads the page.
12330
+ */
12331
+ function showRestartOverlay() {
12332
+ // Avoid duplicates
12333
+ if (document.getElementById("restart-overlay")) return;
12334
+
12335
+ var overlay = document.createElement("div");
12336
+ overlay.id = "restart-overlay";
12337
+ overlay.className = "restart-overlay";
12338
+ overlay.innerHTML =
12339
+ '<div class="restart-overlay-content">' +
12340
+ '<div class="restart-spinner"></div>' +
12341
+ '<div class="restart-title">\u670d\u52a1\u6b63\u5728\u91cd\u542f</div>' +
12342
+ '<div class="restart-subtitle">\u7a0d\u540e\u5c06\u81ea\u52a8\u5237\u65b0\u9875\u9762\u2026</div>' +
12343
+ '</div>';
12344
+ document.body.appendChild(overlay);
12345
+
12346
+ var attempts = 0;
12347
+ var maxAttempts = 20; // 20 * 2s = 40s
12348
+ var timer = setInterval(function() {
12349
+ attempts++;
12350
+ fetch("/api/config", { credentials: "same-origin" })
12351
+ .then(function(res) {
12352
+ if (res.ok) {
12353
+ clearInterval(timer);
12354
+ location.reload();
12355
+ }
12356
+ })
12357
+ .catch(function() {
12358
+ // Server not ready yet
12359
+ });
12360
+ if (attempts >= maxAttempts) {
12361
+ clearInterval(timer);
12362
+ var subtitle = overlay.querySelector(".restart-subtitle");
12363
+ if (subtitle) {
12364
+ subtitle.innerHTML = '\u91cd\u542f\u8d85\u65f6\uff0c\u8bf7 <a href="javascript:location.reload()" style="color:var(--accent);text-decoration:underline">\u624b\u52a8\u5237\u65b0</a> \u9875\u9762\u3002';
12365
+ }
12366
+ }
12367
+ }, 2000);
12368
+ }
12369
+
12278
12370
  function escapeHtml(value) {
12279
12371
  return String(value)
12280
12372
  .replace(/&/g, "&amp;")