@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.
- package/dist/config.js +0 -40
- package/dist/resume-policy.d.ts +0 -77
- package/dist/resume-policy.js +0 -162
- package/dist/server.js +27 -35
- package/dist/storage.js +38 -112
- package/dist/types.d.ts +0 -17
- package/dist/web-ui/content/scripts.js +817 -725
- package/dist/web-ui/content/styles.css +568 -758
- package/dist/web-ui/scripts.js +3 -6
- package/package.json +1 -1
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1461
|
-
'<
|
|
1462
|
-
'<
|
|
1463
|
-
'<span
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
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"
|
|
1512
|
-
|
|
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
|
|
1655
|
-
|
|
1656
|
-
'<div class="settings-
|
|
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"
|
|
1671
|
-
'<button id="do-update-button" class="btn btn-primary btn-sm hidden"
|
|
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
|
-
|
|
1676
|
-
|
|
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">\
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
2905
|
-
window.
|
|
2906
|
-
var
|
|
2907
|
-
if (
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3194
|
-
|
|
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
|
-
|
|
3205
|
-
|
|
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
|
-
|
|
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 =
|
|
4395
|
-
persistHistoryPanelState();
|
|
4191
|
+
state.claudeHistoryExpanded = true;
|
|
4396
4192
|
state.claudeHistoryExpandedDirs = {};
|
|
4397
|
-
state.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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\
|
|
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
|
-
|
|
5773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
5578
|
+
// "default" — try requesting
|
|
5802
5579
|
Notification.requestPermission().then(function(result) {
|
|
5803
5580
|
updateNotificationStatus();
|
|
5804
5581
|
if (result === "granted") {
|
|
5805
|
-
|
|
5806
|
-
} else
|
|
5807
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9341
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
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 =
|
|
12153
|
-
var NOTIFICATION_TOP =
|
|
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, "&")
|