@co0ontty/wand 1.9.0 → 1.14.2
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/README.md +48 -12
- package/dist/config.d.ts +2 -1
- package/dist/config.js +51 -40
- package/dist/message-truncator.d.ts +16 -0
- package/dist/message-truncator.js +76 -0
- package/dist/process-manager.d.ts +4 -0
- package/dist/process-manager.js +74 -21
- package/dist/resume-policy.d.ts +0 -77
- package/dist/resume-policy.js +0 -162
- package/dist/server-session-routes.js +29 -1
- package/dist/server.js +302 -45
- package/dist/storage.js +38 -112
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +10 -0
- package/dist/types.d.ts +27 -16
- package/dist/web-ui/content/scripts.js +1587 -780
- package/dist/web-ui/content/styles.css +677 -734
- package/dist/web-ui/scripts.js +3 -6
- package/dist/ws-broadcast.d.ts +3 -2
- package/dist/ws-broadcast.js +8 -2
- 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,14 @@
|
|
|
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
|
+
})(),
|
|
140
|
+
toolContentCache: {},
|
|
462
141
|
currentView: "terminal",
|
|
463
142
|
terminalScale: (function() {
|
|
464
143
|
try {
|
|
@@ -470,7 +149,13 @@
|
|
|
470
149
|
})(),
|
|
471
150
|
terminalBaseFontSize: 13,
|
|
472
151
|
keyboardPopupOpen: false,
|
|
473
|
-
filePanelOpen:
|
|
152
|
+
filePanelOpen: (function() {
|
|
153
|
+
try {
|
|
154
|
+
return localStorage.getItem("wand-file-panel-open") === "true";
|
|
155
|
+
} catch (e) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
})(),
|
|
474
159
|
chatAutoFollow: (function() {
|
|
475
160
|
try {
|
|
476
161
|
var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
|
|
@@ -491,21 +176,25 @@
|
|
|
491
176
|
lastRenderedMsgCount: 0,
|
|
492
177
|
lastRenderedEmpty: null,
|
|
493
178
|
renderPending: false,
|
|
179
|
+
chatPageSize: 20,
|
|
180
|
+
chatRenderedCount: 20,
|
|
494
181
|
currentTask: null, // Current task title from Claude
|
|
495
182
|
terminalInteractive: false,
|
|
496
183
|
miniKeyboardVisible: false,
|
|
497
|
-
shortcutsExpanded:
|
|
184
|
+
shortcutsExpanded: false,
|
|
498
185
|
modifiers: { ctrl: false, alt: false, shift: false },
|
|
499
186
|
fileSearchQuery: "",
|
|
500
187
|
fileExplorerLoading: false,
|
|
501
188
|
allFiles: [],
|
|
502
189
|
claudeHistory: [],
|
|
503
190
|
claudeHistoryLoaded: false,
|
|
504
|
-
claudeHistoryExpanded:
|
|
191
|
+
claudeHistoryExpanded: true,
|
|
505
192
|
claudeHistoryExpandedDirs: {},
|
|
506
193
|
sessionsManageMode: false,
|
|
507
194
|
selectedSessionIds: {},
|
|
508
195
|
selectedClaudeHistoryIds: {},
|
|
196
|
+
askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
|
|
197
|
+
queueEpoch: 0, // Monotonic counter for queue state freshness
|
|
509
198
|
// Load last used working directory from localStorage
|
|
510
199
|
workingDir: (function() {
|
|
511
200
|
try {
|
|
@@ -650,6 +339,8 @@
|
|
|
650
339
|
if (button) {
|
|
651
340
|
button.classList.toggle("visible", shouldShow);
|
|
652
341
|
}
|
|
342
|
+
var chatContainer = document.getElementById("chat-output");
|
|
343
|
+
if (chatContainer) chatContainer.classList.toggle("has-jump-btn", shouldShow);
|
|
653
344
|
}
|
|
654
345
|
|
|
655
346
|
function scrollChatToBottom(smooth) {
|
|
@@ -714,6 +405,35 @@
|
|
|
714
405
|
updateChatJumpToBottomButton();
|
|
715
406
|
}
|
|
716
407
|
|
|
408
|
+
/** Load older messages by expanding the visible window */
|
|
409
|
+
function loadMoreChatMessages() {
|
|
410
|
+
if (state.chatRenderedCount >= state.currentMessages.length) return;
|
|
411
|
+
state.chatRenderedCount += state.chatPageSize;
|
|
412
|
+
renderChat(true);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Observe the "load more" sentinel for auto-loading when scrolled into view
|
|
416
|
+
var _loadMoreObserver = null;
|
|
417
|
+
function observeLoadMoreSentinel() {
|
|
418
|
+
if (_loadMoreObserver) { _loadMoreObserver.disconnect(); _loadMoreObserver = null; }
|
|
419
|
+
var sentinel = document.getElementById("chat-load-more-sentinel");
|
|
420
|
+
if (!sentinel) return;
|
|
421
|
+
// Click handler for the button
|
|
422
|
+
var btn = sentinel.querySelector(".chat-load-more-btn");
|
|
423
|
+
if (btn) btn.onclick = function() { loadMoreChatMessages(); };
|
|
424
|
+
// IntersectionObserver for auto-load on scroll
|
|
425
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
426
|
+
_loadMoreObserver = new IntersectionObserver(function(entries) {
|
|
427
|
+
for (var i = 0; i < entries.length; i++) {
|
|
428
|
+
if (entries[i].isIntersecting) {
|
|
429
|
+
loadMoreChatMessages();
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}, { root: getChatScrollElement(), rootMargin: "200px" });
|
|
434
|
+
_loadMoreObserver.observe(sentinel);
|
|
435
|
+
}
|
|
436
|
+
|
|
717
437
|
// Helper function to persist selected session ID to localStorage
|
|
718
438
|
function persistSelectedId() {
|
|
719
439
|
try {
|
|
@@ -863,56 +583,6 @@
|
|
|
863
583
|
}
|
|
864
584
|
}
|
|
865
585
|
|
|
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
586
|
function saveChatExpandStateMap(map) {
|
|
917
587
|
try {
|
|
918
588
|
if (!map || Object.keys(map).length === 0) {
|
|
@@ -933,6 +603,12 @@
|
|
|
933
603
|
return sessionState && typeof sessionState === "object" ? sessionState : {};
|
|
934
604
|
}
|
|
935
605
|
|
|
606
|
+
function getPersistedExpandState(itemKey) {
|
|
607
|
+
if (!itemKey || !state.selectedId) return null;
|
|
608
|
+
var sessionState = getCurrentChatExpandState();
|
|
609
|
+
return typeof sessionState[itemKey] === "boolean" ? sessionState[itemKey] : null;
|
|
610
|
+
}
|
|
611
|
+
|
|
936
612
|
function setPersistedExpandState(itemKey, expanded) {
|
|
937
613
|
if (!itemKey || !state.selectedId) return;
|
|
938
614
|
var map = loadChatExpandStateMap();
|
|
@@ -1049,8 +725,9 @@
|
|
|
1049
725
|
container.querySelectorAll("[data-expand-key]").forEach(function(el) {
|
|
1050
726
|
var itemKey = getElementExpandKey(el);
|
|
1051
727
|
var kind = el.dataset.expandKind || "";
|
|
1052
|
-
|
|
1053
|
-
|
|
728
|
+
var persisted = getPersistedExpandState(itemKey);
|
|
729
|
+
if (persisted === null || !kind) return;
|
|
730
|
+
applyExpandedState(el, kind, persisted);
|
|
1054
731
|
});
|
|
1055
732
|
}
|
|
1056
733
|
|
|
@@ -1059,6 +736,8 @@
|
|
|
1059
736
|
state.lastRenderedMsgCount = 0;
|
|
1060
737
|
state.lastRenderedEmpty = null;
|
|
1061
738
|
state.renderPending = false;
|
|
739
|
+
state.chatRenderedCount = state.chatPageSize;
|
|
740
|
+
state.askUserSelections = {};
|
|
1062
741
|
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
1063
742
|
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
1064
743
|
}
|
|
@@ -1193,7 +872,7 @@
|
|
|
1193
872
|
})
|
|
1194
873
|
.then(function(config) {
|
|
1195
874
|
if (!config) return;
|
|
1196
|
-
|
|
875
|
+
state.config = config;
|
|
1197
876
|
state.loginChecked = true;
|
|
1198
877
|
requestAnimationFrame(function() {
|
|
1199
878
|
try {
|
|
@@ -1207,18 +886,7 @@
|
|
|
1207
886
|
refreshAll();
|
|
1208
887
|
requestNotificationPermission();
|
|
1209
888
|
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
|
-
});
|
|
889
|
+
showUpdateBubble(config.currentVersion || "-", config.latestVersion);
|
|
1222
890
|
sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
|
|
1223
891
|
}
|
|
1224
892
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
@@ -1413,8 +1081,6 @@
|
|
|
1413
1081
|
var preferredTool = getComposerTool();
|
|
1414
1082
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
1415
1083
|
|
|
1416
|
-
var showTerminalHeaderControls = !!selectedSession && state.currentView === "terminal";
|
|
1417
|
-
var showChatHeaderControls = !!selectedSession && state.currentView !== "terminal";
|
|
1418
1084
|
return '<div class="app-container">' +
|
|
1419
1085
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
1420
1086
|
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
|
|
@@ -1457,37 +1123,14 @@
|
|
|
1457
1123
|
'</div>' +
|
|
1458
1124
|
'</aside>' +
|
|
1459
1125
|
'<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>' +
|
|
1126
|
+
'<div class="main-header-row">' +
|
|
1127
|
+
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="切换会话侧栏" type="button">' +
|
|
1128
|
+
'<span class="hamburger-icon">' +
|
|
1129
|
+
'<span></span><span></span><span></span>' +
|
|
1130
|
+
'</span>' +
|
|
1131
|
+
'</button>' +
|
|
1132
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
1489
1133
|
'</div>' +
|
|
1490
|
-
'<div class="main-content-body">' +
|
|
1491
1134
|
// File panel backdrop (mobile)
|
|
1492
1135
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
1493
1136
|
// File side panel
|
|
@@ -1508,8 +1151,22 @@
|
|
|
1508
1151
|
'<div class="file-explorer" id="file-explorer">' + renderFileExplorer(selectedSession && selectedSession.cwd ? selectedSession.cwd : getConfigCwd()) + '</div>' +
|
|
1509
1152
|
'</div>' +
|
|
1510
1153
|
'</div>' +
|
|
1511
|
-
'<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active"
|
|
1512
|
-
|
|
1154
|
+
'<div id="output" class="terminal-container' + (state.selectedId ? "" : " hidden") + ' active">' +
|
|
1155
|
+
'<div class="terminal-scale-overlay" aria-label="终端缩放控件">' +
|
|
1156
|
+
'<button id="terminal-scale-down-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="缩小">−</button>' +
|
|
1157
|
+
'<span class="terminal-scale-overlay-label terminal-scale-label" id="terminal-scale-label-top">' + Math.round(state.terminalScale * 100) + '%</span>' +
|
|
1158
|
+
'<button id="terminal-scale-up-top" class="terminal-scale-overlay-btn terminal-scale-btn" type="button" title="放大">+</button>' +
|
|
1159
|
+
'<span class="terminal-scale-overlay-divider"></span>' +
|
|
1160
|
+
'<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
|
|
1161
|
+
'</div>' +
|
|
1162
|
+
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部">↓ 最新</button>' +
|
|
1163
|
+
'</div>' +
|
|
1164
|
+
'<div id="chat-output" class="chat-container hidden">' +
|
|
1165
|
+
'<div class="chat-overlay-controls">' +
|
|
1166
|
+
'<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>' +
|
|
1167
|
+
'</div>' +
|
|
1168
|
+
'<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底">↓ 最新</button>' +
|
|
1169
|
+
'</div>' +
|
|
1513
1170
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
1514
1171
|
'<div class="blank-chat-inner">' +
|
|
1515
1172
|
'<div class="blank-chat-logo">W</div>' +
|
|
@@ -1651,9 +1308,18 @@
|
|
|
1651
1308
|
'<h2 class="modal-title">设置</h2>' +
|
|
1652
1309
|
'<button id="close-settings-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1653
1310
|
'</div>' +
|
|
1654
|
-
'<div class="modal-body
|
|
1655
|
-
|
|
1656
|
-
'<div class="settings-
|
|
1311
|
+
'<div class="modal-body">' +
|
|
1312
|
+
// Tabs
|
|
1313
|
+
'<div class="settings-tabs">' +
|
|
1314
|
+
'<button class="settings-tab active" data-tab="about">\u5173\u4e8e</button>' +
|
|
1315
|
+
'<button class="settings-tab" data-tab="general">\u57fa\u672c\u914d\u7f6e</button>' +
|
|
1316
|
+
'<button class="settings-tab" data-tab="notifications">\u901a\u77e5</button>' +
|
|
1317
|
+
'<button class="settings-tab" data-tab="security">\u5b89\u5168</button>' +
|
|
1318
|
+
'<button class="settings-tab" data-tab="presets">\u547d\u4ee4\u9884\u8bbe</button>' +
|
|
1319
|
+
'<button class="settings-tab" data-tab="display">\u663e\u793a</button>' +
|
|
1320
|
+
'</div>' +
|
|
1321
|
+
|
|
1322
|
+
// About tab
|
|
1657
1323
|
'<div class="settings-panel active" id="settings-tab-about">' +
|
|
1658
1324
|
'<div class="settings-about-info">' +
|
|
1659
1325
|
'<div class="settings-about-row"><span class="settings-label">包名</span><span class="settings-value" id="settings-pkg-name">-</span></div>' +
|
|
@@ -1667,26 +1333,145 @@
|
|
|
1667
1333
|
'<span class="settings-value" id="settings-latest-version">-</span>' +
|
|
1668
1334
|
'</div>' +
|
|
1669
1335
|
'<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"
|
|
1336
|
+
'<button id="check-update-button" class="btn btn-ghost btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
|
|
1337
|
+
'<button id="do-update-button" class="btn btn-primary btn-sm hidden">\u66f4\u65b0\u5230\u6700\u65b0\u7248</button>' +
|
|
1338
|
+
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1672
1339
|
'</div>' +
|
|
1673
1340
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1674
1341
|
'</div>' +
|
|
1675
|
-
'<div class="settings-
|
|
1676
|
-
'<div class="settings-
|
|
1342
|
+
'<div class="settings-update-section" id="android-apk-section">' +
|
|
1343
|
+
'<div id="android-apk-current-row" class="settings-about-row hidden">' +
|
|
1344
|
+
'<span class="settings-label">当前版本</span>' +
|
|
1345
|
+
'<span class="settings-value" id="settings-android-apk-current">-</span>' +
|
|
1346
|
+
'</div>' +
|
|
1347
|
+
'<div id="android-apk-github-row" class="settings-about-row hidden">' +
|
|
1348
|
+
'<span class="settings-label">线上版本</span>' +
|
|
1349
|
+
'<span class="settings-value" id="settings-android-apk-github" style="flex:1">-</span>' +
|
|
1350
|
+
'<button id="download-github-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
|
|
1351
|
+
'</div>' +
|
|
1352
|
+
'<div id="android-apk-local-row" class="settings-about-row hidden">' +
|
|
1353
|
+
'<span class="settings-label">本地版本</span>' +
|
|
1354
|
+
'<span class="settings-value" id="settings-android-apk-local" style="flex:1">-</span>' +
|
|
1355
|
+
'<button id="download-local-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
|
|
1356
|
+
'</div>' +
|
|
1357
|
+
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
1358
|
+
'</div>' +
|
|
1359
|
+
'<div class="settings-update-section" id="android-connect-section">' +
|
|
1360
|
+
'<div class="settings-section-title" style="margin-bottom:8px">App 连接码</div>' +
|
|
1361
|
+
'<div class="settings-connect-url-box">' +
|
|
1362
|
+
'<code id="android-connect-code" class="settings-connect-url-text" style="font-size:12px;word-break:break-all">-</code>' +
|
|
1363
|
+
'<button id="copy-connect-code-button" class="btn btn-ghost btn-sm" type="button" title="复制连接码">复制</button>' +
|
|
1364
|
+
'</div>' +
|
|
1365
|
+
'<p class="hint">复制此连接码粘贴到 Android App 即可自动连接,无需输入密码。修改密码后连接码自动失效。</p>' +
|
|
1366
|
+
'</div>' +
|
|
1367
|
+
'</div>' +
|
|
1368
|
+
|
|
1369
|
+
// Notifications tab
|
|
1370
|
+
'<div class="settings-panel" id="settings-tab-notifications">' +
|
|
1371
|
+
'<div class="settings-section-title">\u901a\u77e5\u504f\u597d</div>' +
|
|
1372
|
+
'<div class="field field-inline">' +
|
|
1373
|
+
'<input id="cfg-notif-sound" type="checkbox" class="field-checkbox" />' +
|
|
1374
|
+
'<label class="field-label" for="cfg-notif-sound">\u64ad\u653e\u63d0\u793a\u97f3</label>' +
|
|
1375
|
+
'</div>' +
|
|
1376
|
+
'<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>' +
|
|
1377
|
+
'<div class="field field-inline">' +
|
|
1378
|
+
'<input id="cfg-notif-bubble" type="checkbox" class="field-checkbox" />' +
|
|
1379
|
+
'<label class="field-label" for="cfg-notif-bubble">\u5e94\u7528\u5185\u901a\u77e5\u6c14\u6ce1</label>' +
|
|
1380
|
+
'</div>' +
|
|
1381
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">\u5728\u9875\u9762\u9876\u90e8\u5f39\u51fa\u6d6e\u52a8\u901a\u77e5\u6c14\u6ce1</p>' +
|
|
1382
|
+
'<div class="settings-notification-section" style="margin-top:6px">' +
|
|
1383
|
+
'<div class="settings-section-title">\u6d4f\u89c8\u5668\u901a\u77e5</div>' +
|
|
1677
1384
|
'<div class="settings-about-row">' +
|
|
1678
|
-
'<span class="settings-label">\
|
|
1385
|
+
'<span class="settings-label">\u6388\u6743\u72b6\u6001</span>' +
|
|
1679
1386
|
'<span class="settings-value" id="notification-permission-status">-</span>' +
|
|
1680
1387
|
'</div>' +
|
|
1681
1388
|
'<div class="settings-update-actions">' +
|
|
1682
1389
|
'<button id="notification-request-btn" class="btn btn-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
|
|
1390
|
+
'<button id="notification-reset-btn" class="btn btn-ghost btn-sm hidden">\u91cd\u65b0\u6388\u6743</button>' +
|
|
1683
1391
|
'<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
|
|
1684
1392
|
'</div>' +
|
|
1685
1393
|
'<p id="notification-test-message" class="hint hidden"></p>' +
|
|
1686
1394
|
'</div>' +
|
|
1687
1395
|
'</div>' +
|
|
1688
1396
|
|
|
1689
|
-
|
|
1397
|
+
// General config tab
|
|
1398
|
+
'<div class="settings-panel" id="settings-tab-general">' +
|
|
1399
|
+
'<div class="field-row">' +
|
|
1400
|
+
'<div class="field">' +
|
|
1401
|
+
'<label class="field-label" for="cfg-host">监听地址 (host)</label>' +
|
|
1402
|
+
'<input id="cfg-host" type="text" class="field-input" placeholder="127.0.0.1" />' +
|
|
1403
|
+
'</div>' +
|
|
1404
|
+
'<div class="field">' +
|
|
1405
|
+
'<label class="field-label" for="cfg-port">端口 (port)</label>' +
|
|
1406
|
+
'<input id="cfg-port" type="number" class="field-input" placeholder="8443" min="1" max="65535" />' +
|
|
1407
|
+
'</div>' +
|
|
1408
|
+
'</div>' +
|
|
1409
|
+
'<div class="field field-inline">' +
|
|
1410
|
+
'<input id="cfg-https" type="checkbox" class="field-checkbox" />' +
|
|
1411
|
+
'<label class="field-label" for="cfg-https">启用 HTTPS</label>' +
|
|
1412
|
+
'</div>' +
|
|
1413
|
+
'<div class="field-row">' +
|
|
1414
|
+
'<div class="field">' +
|
|
1415
|
+
'<label class="field-label" for="cfg-mode">默认执行模式</label>' +
|
|
1416
|
+
'<select id="cfg-mode" class="field-input">' +
|
|
1417
|
+
'<option value="default">default</option>' +
|
|
1418
|
+
'<option value="assist">assist</option>' +
|
|
1419
|
+
'<option value="agent">agent</option>' +
|
|
1420
|
+
'<option value="agent-max">agent-max</option>' +
|
|
1421
|
+
'<option value="auto-edit">auto-edit</option>' +
|
|
1422
|
+
'<option value="full-access">full-access</option>' +
|
|
1423
|
+
'<option value="native">native</option>' +
|
|
1424
|
+
'<option value="managed">managed</option>' +
|
|
1425
|
+
'</select>' +
|
|
1426
|
+
'</div>' +
|
|
1427
|
+
'<div class="field">' +
|
|
1428
|
+
'<label class="field-label" for="cfg-language">回复语言</label>' +
|
|
1429
|
+
'<select id="cfg-language" class="field-input">' +
|
|
1430
|
+
'<option value="">自动(不指定)</option>' +
|
|
1431
|
+
'<option value="中文">中文</option>' +
|
|
1432
|
+
'<option value="English">English</option>' +
|
|
1433
|
+
'<option value="日本語">日本語</option>' +
|
|
1434
|
+
'<option value="한국어">한국어</option>' +
|
|
1435
|
+
'<option value="Español">Español</option>' +
|
|
1436
|
+
'<option value="Français">Français</option>' +
|
|
1437
|
+
'<option value="Deutsch">Deutsch</option>' +
|
|
1438
|
+
'<option value="Русский">Русский</option>' +
|
|
1439
|
+
'</select>' +
|
|
1440
|
+
'</div>' +
|
|
1441
|
+
'</div>' +
|
|
1442
|
+
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
1443
|
+
'<div class="field">' +
|
|
1444
|
+
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
1445
|
+
'<input id="cfg-cwd" type="text" class="field-input" placeholder="/home/user" />' +
|
|
1446
|
+
'</div>' +
|
|
1447
|
+
'<div class="field">' +
|
|
1448
|
+
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
1449
|
+
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
1450
|
+
'</div>' +
|
|
1451
|
+
(typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function" ?
|
|
1452
|
+
'<div style="margin-bottom:16px">' +
|
|
1453
|
+
'<div class="settings-section-title">应用图标</div>' +
|
|
1454
|
+
'<p class="hint" style="margin-top:-4px;margin-bottom:10px">选择 App 启动器图标,返回桌面后生效</p>' +
|
|
1455
|
+
'<div id="app-icon-picker" style="display:flex;gap:16px">' +
|
|
1456
|
+
'<div class="app-icon-option" data-icon="shorthair" style="cursor:pointer;text-align:center">' +
|
|
1457
|
+
'<div class="app-icon-preview" style="width:56px;height:56px;border-radius:12px;overflow:hidden;border:3px solid transparent;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;margin-bottom:4px">' +
|
|
1458
|
+
PIXEL_AVATAR.user +
|
|
1459
|
+
'</div>' +
|
|
1460
|
+
'<span style="font-size:0.72rem;color:var(--text-secondary)">赛博虎妞</span>' +
|
|
1461
|
+
'</div>' +
|
|
1462
|
+
'<div class="app-icon-option" data-icon="garfield" style="cursor:pointer;text-align:center">' +
|
|
1463
|
+
'<div class="app-icon-preview" style="width:56px;height:56px;border-radius:12px;overflow:hidden;border:3px solid transparent;background:var(--bg-tertiary);display:flex;align-items:center;justify-content:center;margin-bottom:4px">' +
|
|
1464
|
+
PIXEL_AVATAR.assistant +
|
|
1465
|
+
'</div>' +
|
|
1466
|
+
'<span style="font-size:0.72rem;color:var(--text-secondary)">勤劳初二</span>' +
|
|
1467
|
+
'</div>' +
|
|
1468
|
+
'</div>' +
|
|
1469
|
+
'<p id="app-icon-message" class="hint hidden" style="margin-top:8px"></p>' +
|
|
1470
|
+
'</div>'
|
|
1471
|
+
: '') +
|
|
1472
|
+
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
1473
|
+
'<p id="config-message" class="hint hidden"></p>' +
|
|
1474
|
+
'</div>' +
|
|
1690
1475
|
|
|
1691
1476
|
// Security tab
|
|
1692
1477
|
'<div class="settings-panel" id="settings-tab-security">' +
|
|
@@ -1724,6 +1509,38 @@
|
|
|
1724
1509
|
'<div class="settings-panel" id="settings-tab-presets">' +
|
|
1725
1510
|
'<div id="presets-list" class="presets-list"></div>' +
|
|
1726
1511
|
'</div>' +
|
|
1512
|
+
|
|
1513
|
+
// Display settings tab
|
|
1514
|
+
'<div class="settings-panel" id="settings-tab-display">' +
|
|
1515
|
+
'<div class="settings-section-title">卡片默认展开状态</div>' +
|
|
1516
|
+
'<p class="hint" style="margin-top:-4px;margin-bottom:12px">设置结构化聊天视图中各类卡片的默认展开/折叠状态。手动操作的展开状态优先于此默认设置。</p>' +
|
|
1517
|
+
'<div class="field field-inline">' +
|
|
1518
|
+
'<input id="cfg-card-edit" type="checkbox" class="field-checkbox" />' +
|
|
1519
|
+
'<label class="field-label" for="cfg-card-edit">编辑卡片 (Edit/Write)</label>' +
|
|
1520
|
+
'</div>' +
|
|
1521
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">文件编辑和写入操作的 diff 视图</p>' +
|
|
1522
|
+
'<div class="field field-inline">' +
|
|
1523
|
+
'<input id="cfg-card-inline" type="checkbox" class="field-checkbox" />' +
|
|
1524
|
+
'<label class="field-label" for="cfg-card-inline">内联工具 (Read/Glob/Grep)</label>' +
|
|
1525
|
+
'</div>' +
|
|
1526
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">文件读取、搜索等工具的结果</p>' +
|
|
1527
|
+
'<div class="field field-inline">' +
|
|
1528
|
+
'<input id="cfg-card-terminal" type="checkbox" class="field-checkbox" />' +
|
|
1529
|
+
'<label class="field-label" for="cfg-card-terminal">终端输出 (Bash)</label>' +
|
|
1530
|
+
'</div>' +
|
|
1531
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">命令行执行结果</p>' +
|
|
1532
|
+
'<div class="field field-inline">' +
|
|
1533
|
+
'<input id="cfg-card-thinking" type="checkbox" class="field-checkbox" />' +
|
|
1534
|
+
'<label class="field-label" for="cfg-card-thinking">思考过程 (Thinking)</label>' +
|
|
1535
|
+
'</div>' +
|
|
1536
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">Claude 的思考过程块</p>' +
|
|
1537
|
+
'<div class="field field-inline">' +
|
|
1538
|
+
'<input id="cfg-card-toolgroup" type="checkbox" class="field-checkbox" />' +
|
|
1539
|
+
'<label class="field-label" for="cfg-card-toolgroup">工具组</label>' +
|
|
1540
|
+
'</div>' +
|
|
1541
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">连续同类工具调用的折叠组</p>' +
|
|
1542
|
+
'<button id="save-display-button" class="btn btn-primary btn-block">保存显示设置</button>' +
|
|
1543
|
+
'<p id="display-message" class="hint hidden"></p>' +
|
|
1727
1544
|
'</div>' +
|
|
1728
1545
|
'</div>' +
|
|
1729
1546
|
'</div>' +
|
|
@@ -2087,7 +1904,9 @@
|
|
|
2087
1904
|
|
|
2088
1905
|
function setFilePanelOpen(nextOpen) {
|
|
2089
1906
|
state.filePanelOpen = nextOpen;
|
|
2090
|
-
|
|
1907
|
+
try {
|
|
1908
|
+
localStorage.setItem("wand-file-panel-open", String(state.filePanelOpen));
|
|
1909
|
+
} catch (e) {}
|
|
2091
1910
|
if (state.filePanelOpen && isMobileLayout()) {
|
|
2092
1911
|
state.sessionsDrawerOpen = false;
|
|
2093
1912
|
}
|
|
@@ -2150,7 +1969,10 @@
|
|
|
2150
1969
|
} catch (e) {}
|
|
2151
1970
|
applyTerminalScale();
|
|
2152
1971
|
updateScaleLabel();
|
|
2153
|
-
|
|
1972
|
+
// Force refit: font size changed but container dimensions didn't,
|
|
1973
|
+
// so ensureTerminalFit (which resets viewport tracking) is needed
|
|
1974
|
+
// instead of scheduleTerminalResize (which skips when size unchanged).
|
|
1975
|
+
ensureTerminalFit();
|
|
2154
1976
|
}
|
|
2155
1977
|
|
|
2156
1978
|
function applyTerminalScale() {
|
|
@@ -2589,6 +2411,35 @@
|
|
|
2589
2411
|
'</div>';
|
|
2590
2412
|
}
|
|
2591
2413
|
|
|
2414
|
+
function timeAgo(isoString) {
|
|
2415
|
+
if (!isoString) return "";
|
|
2416
|
+
var now = Date.now();
|
|
2417
|
+
var then = new Date(isoString).getTime();
|
|
2418
|
+
var diff = Math.max(0, now - then);
|
|
2419
|
+
var seconds = Math.floor(diff / 1000);
|
|
2420
|
+
if (seconds < 60) return "刚刚";
|
|
2421
|
+
var minutes = Math.floor(seconds / 60);
|
|
2422
|
+
if (minutes < 60) return minutes + "分钟前";
|
|
2423
|
+
var hours = Math.floor(minutes / 60);
|
|
2424
|
+
if (hours < 24) return hours + "小时前";
|
|
2425
|
+
var days = Math.floor(hours / 24);
|
|
2426
|
+
if (days < 30) return days + "天前";
|
|
2427
|
+
return Math.floor(days / 30) + "个月前";
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function elapsedTime(isoString) {
|
|
2431
|
+
if (!isoString) return "";
|
|
2432
|
+
var now = Date.now();
|
|
2433
|
+
var then = new Date(isoString).getTime();
|
|
2434
|
+
var diff = Math.max(0, now - then);
|
|
2435
|
+
var seconds = Math.floor(diff / 1000);
|
|
2436
|
+
var minutes = Math.floor(seconds / 60);
|
|
2437
|
+
var hours = Math.floor(minutes / 60);
|
|
2438
|
+
if (hours > 0) return hours + "h" + (minutes % 60 > 0 ? (minutes % 60) + "m" : "");
|
|
2439
|
+
if (minutes > 0) return minutes + "m";
|
|
2440
|
+
return seconds + "s";
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2592
2443
|
function getSessionStatusLabel(session) {
|
|
2593
2444
|
if (!session) return "";
|
|
2594
2445
|
if (session.archived) return "已归档";
|
|
@@ -2611,29 +2462,55 @@
|
|
|
2611
2462
|
return session.status || "";
|
|
2612
2463
|
}
|
|
2613
2464
|
|
|
2465
|
+
/** Get a human-readable activity description for a running session */
|
|
2466
|
+
function getSessionActivityDesc(session) {
|
|
2467
|
+
if (!session) return "";
|
|
2468
|
+
if (session.permissionBlocked) return "等待你的授权";
|
|
2469
|
+
if (session.status !== "running") return "";
|
|
2470
|
+
// Check WebSocket-delivered currentTask first
|
|
2471
|
+
if (session.id === state.selectedId && state.currentTask && state.currentTask.title) {
|
|
2472
|
+
return state.currentTask.title;
|
|
2473
|
+
}
|
|
2474
|
+
// Fall back to snapshot-delivered currentTaskTitle
|
|
2475
|
+
if (session.currentTaskTitle) return session.currentTaskTitle;
|
|
2476
|
+
return "";
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
/** Get the last meaningful assistant text from messages for notification/display */
|
|
2480
|
+
function getLastAssistantSummary(session) {
|
|
2481
|
+
var msgs = session && session.messages;
|
|
2482
|
+
if (!msgs || msgs.length === 0) return "";
|
|
2483
|
+
for (var i = msgs.length - 1; i >= 0; i--) {
|
|
2484
|
+
var msg = msgs[i];
|
|
2485
|
+
if (msg.role !== "assistant") continue;
|
|
2486
|
+
var blocks = msg.content || [];
|
|
2487
|
+
for (var j = 0; j < blocks.length; j++) {
|
|
2488
|
+
if (blocks[j].type === "text" && blocks[j].text && blocks[j].text.trim()) {
|
|
2489
|
+
var text = blocks[j].text.trim();
|
|
2490
|
+
// Strip markdown formatting for compact display
|
|
2491
|
+
text = text.replace(/^#+\s+/gm, "").replace(/\*\*/g, "").replace(/`/g, "");
|
|
2492
|
+
var firstLine = text.split("\n")[0].trim();
|
|
2493
|
+
return firstLine.slice(0, 100);
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
return "";
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2614
2500
|
function renderSessionItem(session) {
|
|
2615
2501
|
var activeClass = session.id === state.selectedId ? " active" : "";
|
|
2616
2502
|
var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
|
|
2617
2503
|
var metaStatus = getSessionStatusLabel(session);
|
|
2618
2504
|
var metaStatusClass = getSessionStatusClass(session);
|
|
2619
|
-
var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
|
|
2620
2505
|
var resumeButton = "";
|
|
2621
|
-
var sessionIdDisplay = "";
|
|
2622
|
-
var recoveryHint = "";
|
|
2623
2506
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
2624
2507
|
|
|
2625
2508
|
if (session.provider === "claude" && session.claudeSessionId) {
|
|
2626
|
-
var shortId = session.claudeSessionId.slice(0, 8);
|
|
2627
|
-
sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
|
|
2628
2509
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
2629
2510
|
resumeButton = '<button class="session-action-btn" data-action="resume" data-session-id="' + session.id + '" type="button" aria-label="恢复会话" title="恢复 Claude 会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 4v6h6"/><path d="M3.51 15a9 9 0 105.64-11.36L3 10"/></svg></button>';
|
|
2630
2511
|
}
|
|
2631
2512
|
}
|
|
2632
2513
|
|
|
2633
|
-
if (session.autoRecovered) {
|
|
2634
|
-
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
2514
|
var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
2638
2515
|
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
2639
2516
|
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
@@ -2644,23 +2521,50 @@
|
|
|
2644
2521
|
? '<button class="session-action-btn merge-btn" data-action="worktree-cleanup" data-session-id="' + session.id + '" type="button" aria-label="重试清理 worktree" title="重试清理 worktree"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>'
|
|
2645
2522
|
: "";
|
|
2646
2523
|
var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
2647
|
-
var modeBadge = renderSessionKindBadge(session);
|
|
2648
2524
|
var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
|
|
2649
2525
|
|
|
2526
|
+
// Title: summary or command
|
|
2527
|
+
var titleHtml = session.summary
|
|
2528
|
+
? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
|
|
2529
|
+
: '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>';
|
|
2530
|
+
|
|
2531
|
+
// Activity description for running sessions
|
|
2532
|
+
var activityDesc = getSessionActivityDesc(session);
|
|
2533
|
+
var activityHtml = "";
|
|
2534
|
+
if (session.status === "running" && activityDesc) {
|
|
2535
|
+
activityHtml = '<div class="session-activity">' + escapeHtml(activityDesc) + '</div>';
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
// Time display
|
|
2539
|
+
var timeDisplay = "";
|
|
2540
|
+
if (session.status === "running") {
|
|
2541
|
+
timeDisplay = '<span class="session-time" title="已运行 ' + escapeHtml(elapsedTime(session.startedAt)) + '">' + escapeHtml(elapsedTime(session.startedAt)) + '</span>';
|
|
2542
|
+
} else if (session.endedAt) {
|
|
2543
|
+
timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.endedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.endedAt)) + '</span>';
|
|
2544
|
+
} else if (session.startedAt) {
|
|
2545
|
+
timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.startedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.startedAt)) + '</span>';
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2548
|
+
// Badges: worktree only (removed PTY/Structured and mode badges for cleaner look)
|
|
2549
|
+
var badgesHtml = renderWorktreeBadge(session);
|
|
2550
|
+
|
|
2551
|
+
// Recovery hint
|
|
2552
|
+
var recoveryHtml = session.autoRecovered ? '<span class="session-recovery-hint">自动恢复</span>' : '';
|
|
2553
|
+
|
|
2650
2554
|
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
2651
2555
|
'<div class="session-item-content">' +
|
|
2652
2556
|
'<div class="session-item-row">' +
|
|
2653
2557
|
checkbox +
|
|
2654
2558
|
'<div class="session-main">' +
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2559
|
+
'<div class="session-title-row">' +
|
|
2560
|
+
titleHtml +
|
|
2561
|
+
timeDisplay +
|
|
2562
|
+
'</div>' +
|
|
2563
|
+
activityHtml +
|
|
2658
2564
|
'<div class="session-meta">' +
|
|
2659
|
-
modeBadge +
|
|
2660
|
-
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
2661
2565
|
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
2662
|
-
|
|
2663
|
-
|
|
2566
|
+
badgesHtml +
|
|
2567
|
+
recoveryHtml +
|
|
2664
2568
|
'</div>' +
|
|
2665
2569
|
'</div>' +
|
|
2666
2570
|
actionsHtml +
|
|
@@ -2819,6 +2723,8 @@
|
|
|
2819
2723
|
'<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
|
|
2820
2724
|
'<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
|
|
2821
2725
|
'</div>' +
|
|
2726
|
+
'</div>' +
|
|
2727
|
+
'<div class="modal-footer">' +
|
|
2822
2728
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
2823
2729
|
'<p id="modal-error" class="error-message hidden"></p>' +
|
|
2824
2730
|
'</div>' +
|
|
@@ -2827,11 +2733,53 @@
|
|
|
2827
2733
|
}
|
|
2828
2734
|
|
|
2829
2735
|
// Global toggle function for tool card headers — called via onclick attribute
|
|
2736
|
+
// Lazy-load tool content for truncated results
|
|
2737
|
+
function __fetchToolContent(toolUseId, callback) {
|
|
2738
|
+
if (!state.selectedId || !toolUseId) return;
|
|
2739
|
+
var cacheKey = state.selectedId + ":" + toolUseId;
|
|
2740
|
+
if (state.toolContentCache[cacheKey]) {
|
|
2741
|
+
callback(null, state.toolContentCache[cacheKey]);
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/tool-content/" + encodeURIComponent(toolUseId), { credentials: "same-origin" })
|
|
2745
|
+
.then(function(res) { return res.json(); })
|
|
2746
|
+
.then(function(data) {
|
|
2747
|
+
if (data.error) {
|
|
2748
|
+
callback(data.error, null);
|
|
2749
|
+
} else {
|
|
2750
|
+
state.toolContentCache[cacheKey] = data;
|
|
2751
|
+
callback(null, data);
|
|
2752
|
+
}
|
|
2753
|
+
})
|
|
2754
|
+
.catch(function() {
|
|
2755
|
+
callback("加载失败", null);
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2830
2759
|
window.__tcToggle = function(e, headerEl) {
|
|
2831
2760
|
var card = headerEl.closest(".tool-use-card");
|
|
2832
2761
|
if (card) {
|
|
2762
|
+
var wasCollapsed = card.classList.contains("collapsed");
|
|
2833
2763
|
card.classList.toggle("collapsed");
|
|
2834
2764
|
persistElementExpandState(card, "tool-card");
|
|
2765
|
+
// Lazy-load truncated content on expand
|
|
2766
|
+
if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
|
|
2767
|
+
var toolUseId = card.dataset.toolUseId;
|
|
2768
|
+
var resultDiv = card.querySelector(".tool-use-result");
|
|
2769
|
+
if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2770
|
+
card.dataset.loaded = "loading";
|
|
2771
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2772
|
+
if (err) {
|
|
2773
|
+
if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-error" onclick="__tcToggle(null, card.querySelector(\'.tool-use-header\'))">加载失败,点击重试</div>';
|
|
2774
|
+
card.dataset.loaded = "";
|
|
2775
|
+
} else {
|
|
2776
|
+
card.dataset.truncated = "false";
|
|
2777
|
+
card.dataset.loaded = "true";
|
|
2778
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2779
|
+
if (resultDiv) resultDiv.innerHTML = '<pre class="tool-use-result-content">' + escapeHtml(content) + '</pre>';
|
|
2780
|
+
}
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2835
2783
|
}
|
|
2836
2784
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
2837
2785
|
};
|
|
@@ -2870,6 +2818,24 @@
|
|
|
2870
2818
|
statusSpan.textContent = "✓";
|
|
2871
2819
|
}
|
|
2872
2820
|
}
|
|
2821
|
+
// Lazy-load truncated content on expand
|
|
2822
|
+
if (expanded && el.dataset.truncated === "true" && el.dataset.loaded !== "true") {
|
|
2823
|
+
var toolUseId = el.dataset.toolUseId;
|
|
2824
|
+
if (body) body.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2825
|
+
el.dataset.loaded = "loading";
|
|
2826
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2827
|
+
if (err) {
|
|
2828
|
+
if (body) body.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
|
|
2829
|
+
el.dataset.loaded = "";
|
|
2830
|
+
} else {
|
|
2831
|
+
el.dataset.truncated = "false";
|
|
2832
|
+
el.dataset.loaded = "true";
|
|
2833
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2834
|
+
el.dataset.result = content;
|
|
2835
|
+
if (body) body.innerHTML = '<div class="inline-tool-result">' + formatInlineResult(content, "") + '</div>';
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
}
|
|
2873
2839
|
persistElementExpandState(el, "inline-tool");
|
|
2874
2840
|
};
|
|
2875
2841
|
// Toggle function for terminal tool blocks
|
|
@@ -2884,7 +2850,32 @@
|
|
|
2884
2850
|
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
2885
2851
|
if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
|
|
2886
2852
|
persistElementExpandState(container, "terminal");
|
|
2887
|
-
|
|
2853
|
+
// Lazy-load truncated content on expand
|
|
2854
|
+
if (isHidden && container.dataset.truncated === "true" && container.dataset.loaded !== "true") {
|
|
2855
|
+
var toolUseId = container.dataset.toolUseId;
|
|
2856
|
+
var termOutput = body.querySelector(".term-output");
|
|
2857
|
+
if (termOutput) termOutput.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2858
|
+
container.dataset.loaded = "loading";
|
|
2859
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2860
|
+
if (err) {
|
|
2861
|
+
if (termOutput) termOutput.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
|
|
2862
|
+
container.dataset.loaded = "";
|
|
2863
|
+
} else {
|
|
2864
|
+
container.dataset.truncated = "false";
|
|
2865
|
+
container.dataset.loaded = "true";
|
|
2866
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2867
|
+
if (termOutput) {
|
|
2868
|
+
var lines = content.split("\n");
|
|
2869
|
+
var html = "";
|
|
2870
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2871
|
+
if (!lines[i] && i === lines.length - 1) continue;
|
|
2872
|
+
html += '<div class="term-line">' + escapeHtml(lines[i]) + '</div>';
|
|
2873
|
+
}
|
|
2874
|
+
termOutput.innerHTML = html;
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2888
2879
|
};
|
|
2889
2880
|
// Update streaming thinking content (called from WebSocket handler)
|
|
2890
2881
|
function updateStreamingThinking(text) {
|
|
@@ -2901,33 +2892,85 @@
|
|
|
2901
2892
|
}
|
|
2902
2893
|
}
|
|
2903
2894
|
}
|
|
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
|
-
|
|
2895
|
+
// ── AskUserQuestion handlers: select → render → submit ──
|
|
2896
|
+
window.__askSelect = function(toolUseId, qIdx, optIdx, isMulti) {
|
|
2897
|
+
var sel = state.askUserSelections[toolUseId];
|
|
2898
|
+
if (!sel) {
|
|
2899
|
+
sel = { submitted: false };
|
|
2900
|
+
state.askUserSelections[toolUseId] = sel;
|
|
2901
|
+
}
|
|
2902
|
+
if (sel.submitted) return;
|
|
2903
|
+
var current = sel[qIdx] || [];
|
|
2904
|
+
if (isMulti) {
|
|
2905
|
+
var pos = current.indexOf(optIdx);
|
|
2906
|
+
if (pos === -1) { current.push(optIdx); } else { current.splice(pos, 1); }
|
|
2907
|
+
} else {
|
|
2908
|
+
current = current[0] === optIdx ? [] : [optIdx];
|
|
2909
|
+
}
|
|
2910
|
+
sel[qIdx] = current;
|
|
2911
|
+
window.__askRender(toolUseId);
|
|
2912
|
+
};
|
|
2913
|
+
|
|
2914
|
+
window.__askRender = function(toolUseId) {
|
|
2915
|
+
var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
|
|
2916
|
+
if (!card) return;
|
|
2917
|
+
var sel = state.askUserSelections[toolUseId] || {};
|
|
2918
|
+
// Update option selected states
|
|
2919
|
+
card.querySelectorAll(".ask-user-option").forEach(function(btn) {
|
|
2920
|
+
var qIdx = parseInt(btn.dataset.questionIndex, 10);
|
|
2921
|
+
var oIdx = parseInt(btn.dataset.optionIndex, 10);
|
|
2922
|
+
var chosen = (sel[qIdx] || []).indexOf(oIdx) !== -1;
|
|
2923
|
+
btn.classList.toggle("selected", chosen);
|
|
2924
|
+
});
|
|
2925
|
+
// Update submit button: enabled only when every question has at least one selection
|
|
2926
|
+
var submitBtn = card.querySelector(".ask-user-submit");
|
|
2927
|
+
if (submitBtn) {
|
|
2928
|
+
var groups = card.querySelectorAll(".ask-user-question-group");
|
|
2929
|
+
var allAnswered = true;
|
|
2930
|
+
groups.forEach(function(g, i) {
|
|
2931
|
+
if (!sel[i] || sel[i].length === 0) allAnswered = false;
|
|
2928
2932
|
});
|
|
2933
|
+
submitBtn.disabled = !allAnswered || !!sel.submitted;
|
|
2934
|
+
if (sel.submitted) {
|
|
2935
|
+
submitBtn.textContent = "已提交...";
|
|
2936
|
+
submitBtn.classList.add("ask-user-submitted");
|
|
2937
|
+
}
|
|
2929
2938
|
}
|
|
2930
2939
|
};
|
|
2940
|
+
|
|
2941
|
+
window.__askSubmit = function(toolUseId) {
|
|
2942
|
+
var sel = state.askUserSelections[toolUseId];
|
|
2943
|
+
if (!sel || sel.submitted || !state.selectedId) return;
|
|
2944
|
+
var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
|
|
2945
|
+
if (!card) return;
|
|
2946
|
+
var groups = card.querySelectorAll(".ask-user-question-group");
|
|
2947
|
+
var lines = [];
|
|
2948
|
+
var allAnswered = true;
|
|
2949
|
+
groups.forEach(function(group, qIdx) {
|
|
2950
|
+
var selected = sel[qIdx] || [];
|
|
2951
|
+
if (selected.length === 0) { allAnswered = false; return; }
|
|
2952
|
+
var labels = [];
|
|
2953
|
+
selected.forEach(function(optIdx) {
|
|
2954
|
+
var btn = group.querySelector('[data-option-index="' + optIdx + '"]');
|
|
2955
|
+
if (btn) labels.push(btn.dataset.optionLabel);
|
|
2956
|
+
});
|
|
2957
|
+
lines.push(labels.join(", "));
|
|
2958
|
+
});
|
|
2959
|
+
if (!allAnswered) return;
|
|
2960
|
+
sel.submitted = true;
|
|
2961
|
+
window.__askRender(toolUseId);
|
|
2962
|
+
var answerText = lines.join("\n");
|
|
2963
|
+
fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
2964
|
+
method: "POST",
|
|
2965
|
+
headers: { "Content-Type": "application/json" },
|
|
2966
|
+
credentials: "same-origin",
|
|
2967
|
+
body: JSON.stringify({ input: answerText + "\n", view: state.currentView })
|
|
2968
|
+
}).catch(function(err) {
|
|
2969
|
+
console.error("[wand] Error sending answer:", err);
|
|
2970
|
+
sel.submitted = false;
|
|
2971
|
+
window.__askRender(toolUseId);
|
|
2972
|
+
});
|
|
2973
|
+
};
|
|
2931
2974
|
function attachEventListeners() {
|
|
2932
2975
|
|
|
2933
2976
|
var loginButton = document.getElementById("login-button");
|
|
@@ -3093,20 +3136,86 @@
|
|
|
3093
3136
|
});
|
|
3094
3137
|
var savePassBtn = document.getElementById("save-password-button");
|
|
3095
3138
|
if (savePassBtn) savePassBtn.addEventListener("click", savePassword);
|
|
3096
|
-
|
|
3139
|
+
// Settings tab clicks
|
|
3140
|
+
var settingsTabs = document.querySelectorAll(".settings-tab");
|
|
3141
|
+
for (var ti = 0; ti < settingsTabs.length; ti++) {
|
|
3142
|
+
settingsTabs[ti].addEventListener("click", function(e) {
|
|
3143
|
+
switchSettingsTab(e.target.getAttribute("data-tab"));
|
|
3144
|
+
});
|
|
3145
|
+
}
|
|
3146
|
+
var saveConfigBtn = document.getElementById("save-config-button");
|
|
3147
|
+
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
3148
|
+
var saveDisplayBtn = document.getElementById("save-display-button");
|
|
3149
|
+
if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
|
|
3150
|
+
// App icon picker (APK only)
|
|
3151
|
+
var appIconPicker = document.getElementById("app-icon-picker");
|
|
3152
|
+
if (appIconPicker) {
|
|
3153
|
+
var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
|
|
3154
|
+
for (var ai = 0; ai < appIconOpts.length; ai++) {
|
|
3155
|
+
appIconOpts[ai].addEventListener("click", function() {
|
|
3156
|
+
var iconName = this.getAttribute("data-icon");
|
|
3157
|
+
if (!iconName || typeof WandNative === "undefined" || typeof WandNative.setAppIcon !== "function") return;
|
|
3158
|
+
try {
|
|
3159
|
+
WandNative.setAppIcon(iconName);
|
|
3160
|
+
_updateAppIconSelection(iconName);
|
|
3161
|
+
var msgEl = document.getElementById("app-icon-message");
|
|
3162
|
+
if (msgEl) {
|
|
3163
|
+
msgEl.textContent = "图标已切换,返回桌面后生效";
|
|
3164
|
+
msgEl.style.color = "var(--success)";
|
|
3165
|
+
msgEl.classList.remove("hidden");
|
|
3166
|
+
setTimeout(function() { msgEl.classList.add("hidden"); }, 3000);
|
|
3167
|
+
}
|
|
3168
|
+
} catch (_e) {}
|
|
3169
|
+
});
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3097
3172
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
3098
3173
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
3099
3174
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
3100
3175
|
if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
|
|
3101
3176
|
var doUpdateBtn = document.getElementById("do-update-button");
|
|
3102
3177
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
3103
|
-
|
|
3178
|
+
var doRestartBtn = document.getElementById("do-restart-button");
|
|
3179
|
+
if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
|
|
3180
|
+
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
3181
|
+
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
3182
|
+
var text = document.getElementById("android-connect-code");
|
|
3183
|
+
if (text) copyToClipboard(text.textContent, copyConnectCodeBtn);
|
|
3184
|
+
});
|
|
3185
|
+
// Notification preferences
|
|
3186
|
+
var notifSoundEl = document.getElementById("cfg-notif-sound");
|
|
3187
|
+
if (notifSoundEl) {
|
|
3188
|
+
notifSoundEl.checked = state.notifSound;
|
|
3189
|
+
notifSoundEl.addEventListener("change", function() {
|
|
3190
|
+
state.notifSound = notifSoundEl.checked;
|
|
3191
|
+
try { localStorage.setItem("wand-notif-sound", String(state.notifSound)); } catch (e) {}
|
|
3192
|
+
// Preview sound when toggling on
|
|
3193
|
+
if (state.notifSound) _doPlaySound();
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
var notifBubbleEl = document.getElementById("cfg-notif-bubble");
|
|
3197
|
+
if (notifBubbleEl) {
|
|
3198
|
+
notifBubbleEl.checked = state.notifBubble;
|
|
3199
|
+
notifBubbleEl.addEventListener("change", function() {
|
|
3200
|
+
state.notifBubble = notifBubbleEl.checked;
|
|
3201
|
+
try { localStorage.setItem("wand-notif-bubble", String(state.notifBubble)); } catch (e) {}
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
// Browser notification section
|
|
3104
3205
|
var notifRequestBtn = document.getElementById("notification-request-btn");
|
|
3105
3206
|
if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
|
|
3106
|
-
if (
|
|
3207
|
+
if (_hasNativeBridge) {
|
|
3208
|
+
window._onNativePermissionResult = function() {
|
|
3209
|
+
updateNotificationStatus();
|
|
3210
|
+
delete window._onNativePermissionResult;
|
|
3211
|
+
};
|
|
3212
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
3213
|
+
} else if (typeof Notification !== "undefined") {
|
|
3107
3214
|
Notification.requestPermission().then(function() { updateNotificationStatus(); });
|
|
3108
3215
|
}
|
|
3109
3216
|
});
|
|
3217
|
+
var notifResetBtn = document.getElementById("notification-reset-btn");
|
|
3218
|
+
if (notifResetBtn) notifResetBtn.addEventListener("click", resetNotificationPermission);
|
|
3110
3219
|
var notifTestBtn = document.getElementById("notification-test-btn");
|
|
3111
3220
|
if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
|
|
3112
3221
|
updateNotificationStatus();
|
|
@@ -3169,11 +3278,6 @@
|
|
|
3169
3278
|
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
3170
3279
|
}
|
|
3171
3280
|
|
|
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
3281
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
3178
3282
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
3179
3283
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -3190,8 +3294,15 @@
|
|
|
3190
3294
|
if (shortcutsToggleBtn) shortcutsToggleBtn.addEventListener("click", function(e) {
|
|
3191
3295
|
e.stopPropagation();
|
|
3192
3296
|
state.shortcutsExpanded = !state.shortcutsExpanded;
|
|
3193
|
-
|
|
3194
|
-
|
|
3297
|
+
var wrap = document.querySelector(".inline-shortcuts-wrap");
|
|
3298
|
+
var toggle = document.querySelector(".shortcuts-toggle");
|
|
3299
|
+
var row = document.querySelector(".inline-shortcuts-expanded-row");
|
|
3300
|
+
if (wrap) wrap.classList.toggle("expanded", state.shortcutsExpanded);
|
|
3301
|
+
if (row) row.classList.toggle("visible", state.shortcutsExpanded);
|
|
3302
|
+
if (toggle) {
|
|
3303
|
+
toggle.classList.toggle("active", state.shortcutsExpanded);
|
|
3304
|
+
toggle.textContent = state.shortcutsExpanded ? "\u203a" : "\u2039";
|
|
3305
|
+
}
|
|
3195
3306
|
});
|
|
3196
3307
|
// Close shortcuts strip on outside click
|
|
3197
3308
|
document.addEventListener("click", function(e) {
|
|
@@ -3201,8 +3312,13 @@
|
|
|
3201
3312
|
var clickedInsideRow = expandedRow && expandedRow.contains(e.target);
|
|
3202
3313
|
if (wrap && !wrap.contains(e.target) && !clickedInsideRow) {
|
|
3203
3314
|
state.shortcutsExpanded = false;
|
|
3204
|
-
|
|
3205
|
-
|
|
3315
|
+
wrap.classList.remove("expanded");
|
|
3316
|
+
if (expandedRow) expandedRow.classList.remove("visible");
|
|
3317
|
+
var toggle = document.querySelector(".shortcuts-toggle");
|
|
3318
|
+
if (toggle) {
|
|
3319
|
+
toggle.classList.remove("active");
|
|
3320
|
+
toggle.textContent = "\u2039";
|
|
3321
|
+
}
|
|
3206
3322
|
}
|
|
3207
3323
|
});
|
|
3208
3324
|
|
|
@@ -3681,7 +3797,6 @@
|
|
|
3681
3797
|
event.preventDefault();
|
|
3682
3798
|
event.stopPropagation();
|
|
3683
3799
|
state.claudeHistoryExpanded = !state.claudeHistoryExpanded;
|
|
3684
|
-
persistHistoryPanelState();
|
|
3685
3800
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
3686
3801
|
loadClaudeHistory();
|
|
3687
3802
|
}
|
|
@@ -3851,6 +3966,8 @@
|
|
|
3851
3966
|
if (button) {
|
|
3852
3967
|
button.classList.toggle("visible", shouldShow);
|
|
3853
3968
|
}
|
|
3969
|
+
var termContainer = document.getElementById("output");
|
|
3970
|
+
if (termContainer) termContainer.classList.toggle("has-jump-btn", shouldShow);
|
|
3854
3971
|
}
|
|
3855
3972
|
|
|
3856
3973
|
function isTerminalNearBottom() {
|
|
@@ -4353,7 +4470,7 @@
|
|
|
4353
4470
|
})
|
|
4354
4471
|
.then(function(res) { return res.json(); })
|
|
4355
4472
|
.then(function(config) {
|
|
4356
|
-
|
|
4473
|
+
state.config = config;
|
|
4357
4474
|
var statusDot = document.getElementById("status-dot");
|
|
4358
4475
|
var statusText = document.getElementById("status-text");
|
|
4359
4476
|
if (statusDot) statusDot.classList.add("active");
|
|
@@ -4391,16 +4508,9 @@
|
|
|
4391
4508
|
state.sessions = [];
|
|
4392
4509
|
state.claudeHistory = [];
|
|
4393
4510
|
state.claudeHistoryLoaded = false;
|
|
4394
|
-
state.claudeHistoryExpanded =
|
|
4395
|
-
persistHistoryPanelState();
|
|
4511
|
+
state.claudeHistoryExpanded = true;
|
|
4396
4512
|
state.claudeHistoryExpandedDirs = {};
|
|
4397
|
-
state.sessionsDrawerOpen =
|
|
4398
|
-
persistDrawerState();
|
|
4399
|
-
state.filePanelOpen = getConfiguredPanelDefaults().filePanelOpen;
|
|
4400
|
-
persistFilePanelState();
|
|
4401
|
-
state.shortcutsExpanded = getConfiguredPanelDefaults().shortcutsExpanded;
|
|
4402
|
-
persistShortcutsExpandedState();
|
|
4403
|
-
updateCollapsedShortcutsUi();
|
|
4513
|
+
state.sessionsDrawerOpen = false;
|
|
4404
4514
|
render();
|
|
4405
4515
|
}
|
|
4406
4516
|
|
|
@@ -4588,9 +4698,6 @@
|
|
|
4588
4698
|
|
|
4589
4699
|
function applyCurrentView() {
|
|
4590
4700
|
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
4701
|
var terminalContainer = document.getElementById("output");
|
|
4595
4702
|
var chatContainer = document.getElementById("chat-output");
|
|
4596
4703
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
@@ -4603,17 +4710,6 @@
|
|
|
4603
4710
|
state.currentView = "terminal";
|
|
4604
4711
|
}
|
|
4605
4712
|
|
|
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
4713
|
if (terminalContainer) {
|
|
4618
4714
|
terminalContainer.classList.toggle("active", showTerminal);
|
|
4619
4715
|
terminalContainer.classList.toggle("hidden", !showTerminal);
|
|
@@ -4888,6 +4984,15 @@
|
|
|
4888
4984
|
});
|
|
4889
4985
|
}
|
|
4890
4986
|
|
|
4987
|
+
var _sessionListUpdateTimer = null;
|
|
4988
|
+
function scheduleSessionListUpdate() {
|
|
4989
|
+
if (_sessionListUpdateTimer) return;
|
|
4990
|
+
_sessionListUpdateTimer = setTimeout(function() {
|
|
4991
|
+
_sessionListUpdateTimer = null;
|
|
4992
|
+
updateSessionsList();
|
|
4993
|
+
}, 200);
|
|
4994
|
+
}
|
|
4995
|
+
|
|
4891
4996
|
function updateSessionsList() {
|
|
4892
4997
|
var listEl = document.getElementById("sessions-list");
|
|
4893
4998
|
var countEl = document.getElementById("session-count");
|
|
@@ -5016,6 +5121,8 @@
|
|
|
5016
5121
|
}
|
|
5017
5122
|
state.selectedId = id;
|
|
5018
5123
|
persistSelectedId();
|
|
5124
|
+
// Clear tool content cache on session switch
|
|
5125
|
+
state.toolContentCache = {};
|
|
5019
5126
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
5020
5127
|
state.messageQueue = [];
|
|
5021
5128
|
state.pendingMessages = [];
|
|
@@ -5066,10 +5173,11 @@
|
|
|
5066
5173
|
|
|
5067
5174
|
function toggleSessionsDrawer() {
|
|
5068
5175
|
state.sessionsDrawerOpen = !state.sessionsDrawerOpen;
|
|
5069
|
-
persistDrawerState();
|
|
5070
5176
|
if (state.sessionsDrawerOpen && isMobileLayout()) {
|
|
5071
5177
|
state.filePanelOpen = false;
|
|
5072
|
-
|
|
5178
|
+
try {
|
|
5179
|
+
localStorage.setItem("wand-file-panel-open", "false");
|
|
5180
|
+
} catch (e) {}
|
|
5073
5181
|
}
|
|
5074
5182
|
updateLayoutState();
|
|
5075
5183
|
}
|
|
@@ -5078,7 +5186,6 @@
|
|
|
5078
5186
|
if (!state.sessionsDrawerOpen) return;
|
|
5079
5187
|
closeSwipedItem();
|
|
5080
5188
|
state.sessionsDrawerOpen = false;
|
|
5081
|
-
persistDrawerState();
|
|
5082
5189
|
updateLayoutState();
|
|
5083
5190
|
}
|
|
5084
5191
|
|
|
@@ -5323,7 +5430,6 @@
|
|
|
5323
5430
|
function openSettingsModal() {
|
|
5324
5431
|
// Close session modal first if open (mutual exclusion)
|
|
5325
5432
|
closeSessionModal();
|
|
5326
|
-
refreshSettingsModalUi();
|
|
5327
5433
|
var modal = document.getElementById("settings-modal");
|
|
5328
5434
|
if (modal) {
|
|
5329
5435
|
modal.classList.remove("hidden");
|
|
@@ -5334,9 +5440,20 @@
|
|
|
5334
5440
|
if (confirmEl) confirmEl.value = "";
|
|
5335
5441
|
hideSettingsMessages();
|
|
5336
5442
|
setupFocusTrap(modal);
|
|
5337
|
-
|
|
5443
|
+
// Activate first tab
|
|
5338
5444
|
switchSettingsTab("about");
|
|
5445
|
+
// Load settings data
|
|
5339
5446
|
loadSettingsData();
|
|
5447
|
+
// Sync notification preferences
|
|
5448
|
+
var soundEl = document.getElementById("cfg-notif-sound");
|
|
5449
|
+
var bubbleEl = document.getElementById("cfg-notif-bubble");
|
|
5450
|
+
if (soundEl) soundEl.checked = state.notifSound;
|
|
5451
|
+
if (bubbleEl) bubbleEl.checked = state.notifBubble;
|
|
5452
|
+
updateNotificationStatus();
|
|
5453
|
+
// Load current app icon selection (APK only)
|
|
5454
|
+
if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
|
|
5455
|
+
try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
|
|
5456
|
+
}
|
|
5340
5457
|
}
|
|
5341
5458
|
}
|
|
5342
5459
|
|
|
@@ -5426,138 +5543,273 @@
|
|
|
5426
5543
|
panels[j].classList.remove("active");
|
|
5427
5544
|
}
|
|
5428
5545
|
}
|
|
5429
|
-
updateSettingsActiveNav();
|
|
5430
5546
|
}
|
|
5431
5547
|
|
|
5432
|
-
function
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5548
|
+
function copyToClipboard(text, triggerBtn) {
|
|
5549
|
+
if (!text) return;
|
|
5550
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
5551
|
+
if (triggerBtn) {
|
|
5552
|
+
var orig = triggerBtn.textContent;
|
|
5553
|
+
triggerBtn.textContent = "已复制";
|
|
5554
|
+
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
5555
|
+
}
|
|
5556
|
+
}).catch(function() {
|
|
5557
|
+
// Fallback for non-secure contexts
|
|
5558
|
+
var ta = document.createElement("textarea");
|
|
5559
|
+
ta.value = text;
|
|
5560
|
+
ta.style.position = "fixed";
|
|
5561
|
+
ta.style.opacity = "0";
|
|
5562
|
+
document.body.appendChild(ta);
|
|
5563
|
+
ta.select();
|
|
5564
|
+
document.execCommand("copy");
|
|
5565
|
+
document.body.removeChild(ta);
|
|
5566
|
+
if (triggerBtn) {
|
|
5567
|
+
var orig = triggerBtn.textContent;
|
|
5568
|
+
triggerBtn.textContent = "已复制";
|
|
5569
|
+
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
5570
|
+
}
|
|
5571
|
+
});
|
|
5443
5572
|
}
|
|
5444
5573
|
|
|
5445
|
-
function
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5574
|
+
function formatBytes(value) {
|
|
5575
|
+
if (typeof value !== "number" || !isFinite(value) || value < 0) return "-";
|
|
5576
|
+
if (value < 1024) return value + " B";
|
|
5577
|
+
var units = ["KB", "MB", "GB", "TB"];
|
|
5578
|
+
var size = value / 1024;
|
|
5579
|
+
var unitIndex = 0;
|
|
5580
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
5581
|
+
size = size / 1024;
|
|
5582
|
+
unitIndex += 1;
|
|
5451
5583
|
}
|
|
5584
|
+
var display = size >= 10 ? size.toFixed(0) : size.toFixed(1);
|
|
5585
|
+
return display + " " + units[unitIndex];
|
|
5452
5586
|
}
|
|
5453
5587
|
|
|
5454
|
-
function
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5588
|
+
function loadSettingsData() {
|
|
5589
|
+
fetch("/api/settings", { credentials: "same-origin" })
|
|
5590
|
+
.then(function(res) { return res.json(); })
|
|
5591
|
+
.then(function(data) {
|
|
5592
|
+
// About
|
|
5593
|
+
var nameEl = document.getElementById("settings-pkg-name");
|
|
5594
|
+
var verEl = document.getElementById("settings-version");
|
|
5595
|
+
var nodeEl = document.getElementById("settings-node-req");
|
|
5596
|
+
var repoEl = document.getElementById("settings-repo-url");
|
|
5597
|
+
if (nameEl) nameEl.textContent = data.packageName || "-";
|
|
5598
|
+
if (verEl) verEl.textContent = data.version || "-";
|
|
5599
|
+
if (nodeEl) nodeEl.textContent = data.nodeVersion || "-";
|
|
5600
|
+
if (repoEl && data.repoUrl) {
|
|
5601
|
+
repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
|
|
5602
|
+
}
|
|
5603
|
+
|
|
5604
|
+
// Prefill update info if available
|
|
5605
|
+
var latestEl = document.getElementById("settings-latest-version");
|
|
5606
|
+
var updateBtn = document.getElementById("do-update-button");
|
|
5607
|
+
if (data.latestVersion && latestEl) {
|
|
5608
|
+
latestEl.textContent = data.latestVersion;
|
|
5609
|
+
if (data.updateAvailable && updateBtn) {
|
|
5610
|
+
updateBtn.classList.remove("hidden");
|
|
5611
|
+
}
|
|
5612
|
+
}
|
|
5458
5613
|
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5614
|
+
// ── Android APK version display ──
|
|
5615
|
+
var apkSection = document.getElementById("android-apk-section");
|
|
5616
|
+
var apkCurrentRow = document.getElementById("android-apk-current-row");
|
|
5617
|
+
var apkCurrentEl = document.getElementById("settings-android-apk-current");
|
|
5618
|
+
var apkGithubRow = document.getElementById("android-apk-github-row");
|
|
5619
|
+
var apkGithubEl = document.getElementById("settings-android-apk-github");
|
|
5620
|
+
var apkGithubBtn = document.getElementById("download-github-apk-btn");
|
|
5621
|
+
var apkLocalRow = document.getElementById("android-apk-local-row");
|
|
5622
|
+
var apkLocalEl = document.getElementById("settings-android-apk-local");
|
|
5623
|
+
var apkLocalBtn = document.getElementById("download-local-apk-btn");
|
|
5624
|
+
var apkMessageEl = document.getElementById("android-apk-message");
|
|
5625
|
+
var androidApk = data.androidApk || {};
|
|
5626
|
+
var isInApk = !!_apkVersion;
|
|
5627
|
+
|
|
5628
|
+
if (isInApk) {
|
|
5629
|
+
// ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
|
|
5630
|
+
if (apkCurrentRow && apkCurrentEl) {
|
|
5631
|
+
apkCurrentEl.textContent = "v" + _apkVersion;
|
|
5632
|
+
apkCurrentRow.classList.remove("hidden");
|
|
5633
|
+
}
|
|
5634
|
+
// 线上版本
|
|
5635
|
+
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
5636
|
+
var ghLabel = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
|
|
5637
|
+
if (typeof androidApk.github.size === "number") ghLabel += " · " + formatBytes(androidApk.github.size);
|
|
5638
|
+
apkGithubEl.textContent = ghLabel;
|
|
5639
|
+
apkGithubRow.classList.remove("hidden");
|
|
5640
|
+
if (apkGithubBtn) {
|
|
5641
|
+
apkGithubBtn.textContent = "下载安装";
|
|
5642
|
+
apkGithubBtn.classList.remove("hidden");
|
|
5643
|
+
apkGithubBtn.onclick = function() {
|
|
5644
|
+
try {
|
|
5645
|
+
WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
|
|
5646
|
+
} catch (e) {
|
|
5647
|
+
alert("调用下载失败: " + e.message);
|
|
5648
|
+
}
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
// 本地版本
|
|
5653
|
+
if (androidApk.local && apkLocalRow && apkLocalEl) {
|
|
5654
|
+
var lcLabel = androidApk.local.version ? ("v" + androidApk.local.version) : androidApk.local.fileName;
|
|
5655
|
+
if (typeof androidApk.local.size === "number") lcLabel += " · " + formatBytes(androidApk.local.size);
|
|
5656
|
+
apkLocalEl.textContent = lcLabel;
|
|
5657
|
+
apkLocalRow.classList.remove("hidden");
|
|
5658
|
+
if (apkLocalBtn) {
|
|
5659
|
+
apkLocalBtn.textContent = "下载安装";
|
|
5660
|
+
apkLocalBtn.classList.remove("hidden");
|
|
5661
|
+
apkLocalBtn.onclick = function() {
|
|
5662
|
+
try {
|
|
5663
|
+
WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
|
|
5664
|
+
} catch (e) {
|
|
5665
|
+
alert("调用下载失败: " + e.message);
|
|
5666
|
+
}
|
|
5667
|
+
};
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
// 都没有时
|
|
5671
|
+
if (!androidApk.github && !androidApk.local && apkMessageEl) {
|
|
5672
|
+
apkMessageEl.textContent = "暂无可用更新";
|
|
5673
|
+
apkMessageEl.classList.remove("hidden");
|
|
5674
|
+
}
|
|
5675
|
+
} else {
|
|
5676
|
+
// ── 浏览器模式:只显示线上版本 + 下载按钮 ──
|
|
5677
|
+
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
5678
|
+
var ghLabel2 = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
|
|
5679
|
+
if (typeof androidApk.github.size === "number") ghLabel2 += " · " + formatBytes(androidApk.github.size);
|
|
5680
|
+
apkGithubEl.textContent = ghLabel2;
|
|
5681
|
+
apkGithubRow.classList.remove("hidden");
|
|
5682
|
+
if (apkGithubBtn) {
|
|
5683
|
+
apkGithubBtn.textContent = "下载";
|
|
5684
|
+
apkGithubBtn.classList.remove("hidden");
|
|
5685
|
+
apkGithubBtn.onclick = function() {
|
|
5686
|
+
window.open(androidApk.github.downloadUrl, "_blank");
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5689
|
+
} else if (apkMessageEl) {
|
|
5690
|
+
apkMessageEl.textContent = "暂未提供";
|
|
5691
|
+
apkMessageEl.classList.remove("hidden");
|
|
5692
|
+
}
|
|
5693
|
+
}
|
|
5463
5694
|
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5695
|
+
// App connect code (encrypted)
|
|
5696
|
+
var connectCodeEl = document.getElementById("android-connect-code");
|
|
5697
|
+
if (connectCodeEl) {
|
|
5698
|
+
connectCodeEl.textContent = "加载中...";
|
|
5699
|
+
fetch("/api/app-connect-code").then(function(r) { return r.json(); }).then(function(d) {
|
|
5700
|
+
if (d.code) connectCodeEl.textContent = d.code;
|
|
5701
|
+
else connectCodeEl.textContent = "生成失败";
|
|
5702
|
+
}).catch(function() { connectCodeEl.textContent = "获取失败"; });
|
|
5703
|
+
}
|
|
5704
|
+
|
|
5705
|
+
// Config fields
|
|
5706
|
+
var cfg = data.config || {};
|
|
5707
|
+
var hostEl = document.getElementById("cfg-host");
|
|
5708
|
+
var portEl = document.getElementById("cfg-port");
|
|
5709
|
+
var httpsEl = document.getElementById("cfg-https");
|
|
5710
|
+
var modeEl = document.getElementById("cfg-mode");
|
|
5711
|
+
var cwdEl = document.getElementById("cfg-cwd");
|
|
5712
|
+
var shellEl = document.getElementById("cfg-shell");
|
|
5713
|
+
if (hostEl) hostEl.value = cfg.host || "";
|
|
5714
|
+
if (portEl) portEl.value = cfg.port || "";
|
|
5715
|
+
if (httpsEl) httpsEl.checked = cfg.https === true;
|
|
5716
|
+
if (modeEl) modeEl.value = cfg.defaultMode || "default";
|
|
5717
|
+
if (cwdEl) cwdEl.value = cfg.defaultCwd || "";
|
|
5718
|
+
if (shellEl) shellEl.value = cfg.shell || "";
|
|
5719
|
+
var langEl = document.getElementById("cfg-language");
|
|
5720
|
+
if (langEl) langEl.value = cfg.language || "";
|
|
5721
|
+
|
|
5722
|
+
// Cert status
|
|
5723
|
+
var certStatus = document.getElementById("cert-status");
|
|
5724
|
+
if (certStatus) {
|
|
5725
|
+
certStatus.textContent = data.hasCert ? "已安装 SSL 证书" : "未安装证书(使用自签名或 HTTP)";
|
|
5726
|
+
certStatus.style.color = data.hasCert ? "var(--success)" : "var(--text-secondary)";
|
|
5727
|
+
}
|
|
5728
|
+
|
|
5729
|
+
// Presets
|
|
5730
|
+
var presetsList = document.getElementById("presets-list");
|
|
5731
|
+
if (presetsList && cfg.commandPresets) {
|
|
5732
|
+
var html = "";
|
|
5733
|
+
for (var i = 0; i < cfg.commandPresets.length; i++) {
|
|
5734
|
+
var p = cfg.commandPresets[i];
|
|
5735
|
+
html += '<div class="preset-item">' +
|
|
5736
|
+
'<span class="preset-label">' + escapeHtml(p.label) + '</span>' +
|
|
5737
|
+
'<span class="preset-detail">' + escapeHtml(p.command) + (p.mode ? ' (' + escapeHtml(p.mode) + ')' : '') + '</span>' +
|
|
5738
|
+
'</div>';
|
|
5739
|
+
}
|
|
5740
|
+
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>';
|
|
5741
|
+
presetsList.innerHTML = html;
|
|
5742
|
+
}
|
|
5743
|
+
|
|
5744
|
+
// Card expand defaults
|
|
5745
|
+
var cd = cfg.cardDefaults || {};
|
|
5746
|
+
var cdEditEl = document.getElementById("cfg-card-edit");
|
|
5747
|
+
var cdInlineEl = document.getElementById("cfg-card-inline");
|
|
5748
|
+
var cdTerminalEl = document.getElementById("cfg-card-terminal");
|
|
5749
|
+
var cdThinkingEl = document.getElementById("cfg-card-thinking");
|
|
5750
|
+
var cdToolgroupEl = document.getElementById("cfg-card-toolgroup");
|
|
5751
|
+
if (cdEditEl) cdEditEl.checked = cd.editCards === true;
|
|
5752
|
+
if (cdInlineEl) cdInlineEl.checked = cd.inlineTools === true;
|
|
5753
|
+
if (cdTerminalEl) cdTerminalEl.checked = cd.terminal === true;
|
|
5754
|
+
if (cdThinkingEl) cdThinkingEl.checked = cd.thinking === true;
|
|
5755
|
+
if (cdToolgroupEl) cdToolgroupEl.checked = cd.toolGroup === true;
|
|
5756
|
+
})
|
|
5757
|
+
.catch(function() {});
|
|
5467
5758
|
}
|
|
5468
5759
|
|
|
5469
|
-
function
|
|
5470
|
-
var
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
structuredTerminalExpanded: panelState && typeof panelState.structuredTerminalExpanded === "boolean" ? panelState.structuredTerminalExpanded : defaults.structuredTerminalExpanded,
|
|
5482
|
-
structuredToolCardExpanded: panelState && typeof panelState.structuredToolCardExpanded === "boolean" ? panelState.structuredToolCardExpanded : defaults.structuredToolCardExpanded,
|
|
5760
|
+
function saveConfigSettings() {
|
|
5761
|
+
var msgEl = document.getElementById("config-message");
|
|
5762
|
+
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
5763
|
+
|
|
5764
|
+
var body = {
|
|
5765
|
+
host: (document.getElementById("cfg-host") || {}).value,
|
|
5766
|
+
port: Number((document.getElementById("cfg-port") || {}).value),
|
|
5767
|
+
https: (document.getElementById("cfg-https") || {}).checked,
|
|
5768
|
+
defaultMode: (document.getElementById("cfg-mode") || {}).value,
|
|
5769
|
+
defaultCwd: (document.getElementById("cfg-cwd") || {}).value,
|
|
5770
|
+
shell: (document.getElementById("cfg-shell") || {}).value,
|
|
5771
|
+
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
5483
5772
|
};
|
|
5484
|
-
}
|
|
5485
5773
|
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
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>';
|
|
5774
|
+
fetch("/api/settings/config", {
|
|
5775
|
+
method: "POST",
|
|
5776
|
+
headers: { "Content-Type": "application/json" },
|
|
5777
|
+
credentials: "same-origin",
|
|
5778
|
+
body: JSON.stringify(body)
|
|
5779
|
+
})
|
|
5780
|
+
.then(function(res) { return res.json(); })
|
|
5781
|
+
.then(function(data) {
|
|
5782
|
+
if (msgEl) {
|
|
5783
|
+
if (data.error) {
|
|
5784
|
+
msgEl.textContent = data.error;
|
|
5785
|
+
msgEl.style.color = "var(--error)";
|
|
5786
|
+
} else {
|
|
5787
|
+
msgEl.textContent = "配置已保存,部分更改需要重启后生效。";
|
|
5788
|
+
msgEl.style.color = "var(--success)";
|
|
5789
|
+
}
|
|
5790
|
+
msgEl.classList.remove("hidden");
|
|
5791
|
+
}
|
|
5792
|
+
})
|
|
5793
|
+
.catch(function() {
|
|
5794
|
+
if (msgEl) {
|
|
5795
|
+
msgEl.textContent = "保存失败。";
|
|
5796
|
+
msgEl.style.color = "var(--error)";
|
|
5797
|
+
msgEl.classList.remove("hidden");
|
|
5532
5798
|
}
|
|
5533
|
-
|
|
5534
|
-
presetsList.innerHTML = html;
|
|
5535
|
-
}
|
|
5536
|
-
}
|
|
5537
|
-
|
|
5538
|
-
function loadSettingsData() {
|
|
5539
|
-
fetch("/api/settings", { credentials: "same-origin" })
|
|
5540
|
-
.then(function(res) { return res.json(); })
|
|
5541
|
-
.then(function(data) {
|
|
5542
|
-
applyLoadedSettingsData(data);
|
|
5543
|
-
})
|
|
5544
|
-
.catch(function() {});
|
|
5799
|
+
});
|
|
5545
5800
|
}
|
|
5546
5801
|
|
|
5547
|
-
function
|
|
5548
|
-
var msgEl = document.getElementById("
|
|
5802
|
+
function saveDisplaySettings() {
|
|
5803
|
+
var msgEl = document.getElementById("display-message");
|
|
5549
5804
|
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
5550
5805
|
|
|
5551
5806
|
var body = {
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
language: (document.getElementById("cfg-language") || {}).value || "",
|
|
5559
|
-
uiPreferences: {
|
|
5560
|
-
defaultPanelState: getPanelStateSettingsFormValues(),
|
|
5807
|
+
cardDefaults: {
|
|
5808
|
+
editCards: !!(document.getElementById("cfg-card-edit") || {}).checked,
|
|
5809
|
+
inlineTools: !!(document.getElementById("cfg-card-inline") || {}).checked,
|
|
5810
|
+
terminal: !!(document.getElementById("cfg-card-terminal") || {}).checked,
|
|
5811
|
+
thinking: !!(document.getElementById("cfg-card-thinking") || {}).checked,
|
|
5812
|
+
toolGroup: !!(document.getElementById("cfg-card-toolgroup") || {}).checked,
|
|
5561
5813
|
}
|
|
5562
5814
|
};
|
|
5563
5815
|
|
|
@@ -5574,12 +5826,15 @@
|
|
|
5574
5826
|
msgEl.textContent = data.error;
|
|
5575
5827
|
msgEl.style.color = "var(--error)";
|
|
5576
5828
|
} else {
|
|
5577
|
-
msgEl.textContent = "
|
|
5829
|
+
msgEl.textContent = "显示设置已保存";
|
|
5578
5830
|
msgEl.style.color = "var(--success)";
|
|
5579
|
-
handleSettingsConfigSaved(data.config);
|
|
5580
5831
|
}
|
|
5581
5832
|
msgEl.classList.remove("hidden");
|
|
5582
5833
|
}
|
|
5834
|
+
// Update local config so card defaults take effect immediately
|
|
5835
|
+
if (!data.error && state.config) {
|
|
5836
|
+
state.config.cardDefaults = body.cardDefaults;
|
|
5837
|
+
}
|
|
5583
5838
|
})
|
|
5584
5839
|
.catch(function() {
|
|
5585
5840
|
if (msgEl) {
|
|
@@ -5590,7 +5845,6 @@
|
|
|
5590
5845
|
});
|
|
5591
5846
|
}
|
|
5592
5847
|
|
|
5593
|
-
|
|
5594
5848
|
function uploadCertificates() {
|
|
5595
5849
|
var keyFile = document.getElementById("cert-key-file");
|
|
5596
5850
|
var certFile = document.getElementById("cert-cert-file");
|
|
@@ -5699,7 +5953,7 @@
|
|
|
5699
5953
|
.then(function(res) { return res.json(); })
|
|
5700
5954
|
.then(function(data) {
|
|
5701
5955
|
if (msgEl) {
|
|
5702
|
-
msgEl.textContent = data.message || data.error || "
|
|
5956
|
+
msgEl.textContent = data.message || data.error || "\u66f4\u65b0\u5b8c\u6210\u3002";
|
|
5703
5957
|
msgEl.style.color = data.error ? "var(--error)" : "var(--success)";
|
|
5704
5958
|
msgEl.classList.remove("hidden");
|
|
5705
5959
|
}
|
|
@@ -5707,11 +5961,14 @@
|
|
|
5707
5961
|
updateBtn.disabled = false;
|
|
5708
5962
|
} else {
|
|
5709
5963
|
updateBtn.classList.add("hidden");
|
|
5964
|
+
// Show restart button
|
|
5965
|
+
var restartBtn = document.getElementById("do-restart-button");
|
|
5966
|
+
if (restartBtn) restartBtn.classList.remove("hidden");
|
|
5710
5967
|
}
|
|
5711
5968
|
})
|
|
5712
5969
|
.catch(function() {
|
|
5713
5970
|
if (msgEl) {
|
|
5714
|
-
msgEl.textContent = "
|
|
5971
|
+
msgEl.textContent = "\u66f4\u65b0\u5931\u8d25\u3002";
|
|
5715
5972
|
msgEl.style.color = "var(--error)";
|
|
5716
5973
|
msgEl.classList.remove("hidden");
|
|
5717
5974
|
}
|
|
@@ -5719,101 +5976,224 @@
|
|
|
5719
5976
|
});
|
|
5720
5977
|
}
|
|
5721
5978
|
|
|
5979
|
+
function performSettingsRestart() {
|
|
5980
|
+
var restartBtn = document.getElementById("do-restart-button");
|
|
5981
|
+
var msgEl = document.getElementById("update-message");
|
|
5982
|
+
performRestart(restartBtn, msgEl);
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5722
5985
|
// ── Notification Settings Helpers ──
|
|
5723
5986
|
|
|
5987
|
+
function _updateAppIconSelection(activeIcon) {
|
|
5988
|
+
var opts = document.querySelectorAll(".app-icon-option");
|
|
5989
|
+
for (var i = 0; i < opts.length; i++) {
|
|
5990
|
+
var preview = opts[i].querySelector(".app-icon-preview");
|
|
5991
|
+
if (preview) {
|
|
5992
|
+
preview.style.borderColor = opts[i].getAttribute("data-icon") === activeIcon ? "var(--accent)" : "transparent";
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
}
|
|
5996
|
+
|
|
5724
5997
|
function updateNotificationStatus() {
|
|
5725
5998
|
var statusEl = document.getElementById("notification-permission-status");
|
|
5726
5999
|
var requestBtn = document.getElementById("notification-request-btn");
|
|
6000
|
+
var resetBtn = document.getElementById("notification-reset-btn");
|
|
5727
6001
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
5728
6002
|
if (!statusEl) return;
|
|
5729
6003
|
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
6004
|
+
// Determine permission state: native bridge or browser API
|
|
6005
|
+
var perm = _getNativePermission();
|
|
6006
|
+
if (perm === null) {
|
|
6007
|
+
// No native bridge — fall back to browser Notification API
|
|
6008
|
+
if (typeof Notification === "undefined") {
|
|
6009
|
+
statusEl.textContent = "\u4e0d\u652f\u6301";
|
|
6010
|
+
statusEl.style.color = "var(--fg-muted)";
|
|
6011
|
+
if (requestBtn) requestBtn.classList.add("hidden");
|
|
6012
|
+
if (resetBtn) resetBtn.classList.add("hidden");
|
|
6013
|
+
return;
|
|
6014
|
+
}
|
|
6015
|
+
perm = Notification.permission;
|
|
5735
6016
|
}
|
|
5736
6017
|
|
|
5737
|
-
var perm = Notification.permission;
|
|
5738
6018
|
if (perm === "granted") {
|
|
5739
6019
|
statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
|
|
5740
6020
|
statusEl.style.color = "var(--success)";
|
|
5741
6021
|
if (requestBtn) requestBtn.classList.add("hidden");
|
|
6022
|
+
if (resetBtn) resetBtn.classList.add("hidden");
|
|
5742
6023
|
} else if (perm === "denied") {
|
|
5743
6024
|
statusEl.textContent = "\u5df2\u62d2\u7edd";
|
|
5744
6025
|
statusEl.style.color = "var(--danger)";
|
|
5745
6026
|
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
|
-
}
|
|
6027
|
+
if (resetBtn) resetBtn.classList.remove("hidden");
|
|
5751
6028
|
} else {
|
|
5752
6029
|
statusEl.textContent = "\u672a\u6388\u6743";
|
|
5753
6030
|
statusEl.style.color = "var(--warning)";
|
|
5754
6031
|
if (requestBtn) requestBtn.classList.remove("hidden");
|
|
6032
|
+
if (resetBtn) resetBtn.classList.remove("hidden");
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
function resetNotificationPermission() {
|
|
6037
|
+
var testMsgEl = document.getElementById("notification-test-message");
|
|
6038
|
+
|
|
6039
|
+
// Native bridge path — trigger Android system permission dialog
|
|
6040
|
+
if (_hasNativeBridge) {
|
|
6041
|
+
// Listen for permission result callback from native
|
|
6042
|
+
window._onNativePermissionResult = function(result) {
|
|
6043
|
+
updateNotificationStatus();
|
|
6044
|
+
if (testMsgEl) {
|
|
6045
|
+
if (result === "granted") {
|
|
6046
|
+
testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
|
|
6047
|
+
testMsgEl.style.color = "var(--success)";
|
|
6048
|
+
} else {
|
|
6049
|
+
testMsgEl.textContent = "\u2717 \u672a\u6388\u6743\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f Wand \u7684\u901a\u77e5\u6743\u9650";
|
|
6050
|
+
testMsgEl.style.color = "var(--danger)";
|
|
6051
|
+
}
|
|
6052
|
+
testMsgEl.classList.remove("hidden");
|
|
6053
|
+
}
|
|
6054
|
+
delete window._onNativePermissionResult;
|
|
6055
|
+
};
|
|
6056
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
6057
|
+
return;
|
|
5755
6058
|
}
|
|
6059
|
+
|
|
6060
|
+
if (typeof Notification === "undefined") return;
|
|
6061
|
+
|
|
6062
|
+
// Always call requestPermission — this triggers the browser's native
|
|
6063
|
+
// permission dialog when allowed. In "default" state it always works.
|
|
6064
|
+
// In "denied" state, some browsers (newer Chrome) re-prompt, others don't.
|
|
6065
|
+
Notification.requestPermission().then(function(result) {
|
|
6066
|
+
updateNotificationStatus();
|
|
6067
|
+
if (result === "granted") {
|
|
6068
|
+
if (testMsgEl) {
|
|
6069
|
+
testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
|
|
6070
|
+
testMsgEl.style.color = "var(--success)";
|
|
6071
|
+
testMsgEl.classList.remove("hidden");
|
|
6072
|
+
}
|
|
6073
|
+
} else if (result === "denied") {
|
|
6074
|
+
// Browser blocked re-prompting — show inline guide with site-settings shortcut
|
|
6075
|
+
if (testMsgEl) {
|
|
6076
|
+
var origin = location.origin;
|
|
6077
|
+
testMsgEl.innerHTML =
|
|
6078
|
+
"\u6d4f\u89c8\u5668\u5df2\u62e6\u622a\u6388\u6743\u5f39\u7a97\uff0c\u8bf7\u624b\u52a8\u91cd\u7f6e\uff1a<br>" +
|
|
6079
|
+
'<span style="display:inline-flex;align-items:center;gap:4px;margin:4px 0">' +
|
|
6080
|
+
"\u2460 \u70b9\u51fb\u5730\u5740\u680f\u5de6\u4fa7\u7684 " +
|
|
6081
|
+
'<span style="display:inline-flex;align-items:center;justify-content:center;' +
|
|
6082
|
+
"width:16px;height:16px;border-radius:50%;border:1px solid var(--border);" +
|
|
6083
|
+
'font-size:11px;vertical-align:middle">i</span>' +
|
|
6084
|
+
" \u6216\u9501\u56fe\u6807" +
|
|
6085
|
+
"</span><br>" +
|
|
6086
|
+
"\u2461 \u627e\u5230\u300c\u901a\u77e5\u300d\u2192 \u6539\u4e3a\u300c\u5141\u8bb8\u300d<br>" +
|
|
6087
|
+
"\u2462 \u5237\u65b0\u9875\u9762\u5373\u53ef";
|
|
6088
|
+
testMsgEl.style.color = "var(--fg-muted)";
|
|
6089
|
+
testMsgEl.classList.remove("hidden");
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
});
|
|
5756
6093
|
}
|
|
5757
6094
|
|
|
5758
6095
|
function testNotification() {
|
|
5759
6096
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
6097
|
+
var results = [];
|
|
5760
6098
|
|
|
5761
|
-
//
|
|
6099
|
+
// 1. Test sound playback
|
|
6100
|
+
var soundOk = tryPlayNotificationSound();
|
|
6101
|
+
results.push(soundOk ? "\u2713 \u63d0\u793a\u97f3" : "\u2717 \u63d0\u793a\u97f3\uff08\u65e0\u6cd5\u64ad\u653e\uff09");
|
|
6102
|
+
|
|
6103
|
+
// 2. Test in-app bubble
|
|
6104
|
+
var bubbleEnabled = state.notifBubble;
|
|
5762
6105
|
showNotificationBubble({
|
|
5763
6106
|
title: "\u6d4b\u8bd5\u901a\u77e5",
|
|
5764
|
-
body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\
|
|
6107
|
+
body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\u3002",
|
|
5765
6108
|
type: "info",
|
|
5766
6109
|
icon: "\u266a",
|
|
5767
6110
|
duration: 5000,
|
|
6111
|
+
playSound: false, // sound already played above
|
|
5768
6112
|
});
|
|
6113
|
+
results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
|
|
5769
6114
|
|
|
5770
|
-
// Test browser
|
|
5771
|
-
if (
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
6115
|
+
// 3. Test system notification (native bridge or browser API)
|
|
6116
|
+
if (_hasNativeBridge) {
|
|
6117
|
+
var nativePerm = _getNativePermission();
|
|
6118
|
+
if (nativePerm === "granted") {
|
|
6119
|
+
try {
|
|
6120
|
+
WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
|
|
6121
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
|
|
6122
|
+
} catch (_e) {
|
|
6123
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
|
|
6124
|
+
}
|
|
6125
|
+
} else if (nativePerm === "denied") {
|
|
6126
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f\uff09");
|
|
6127
|
+
} else {
|
|
6128
|
+
// "default" — request permission, then report
|
|
6129
|
+
window._onNativePermissionResult = function(result) {
|
|
6130
|
+
updateNotificationStatus();
|
|
6131
|
+
if (result === "granted") {
|
|
6132
|
+
try {
|
|
6133
|
+
WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
|
|
6134
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
6135
|
+
} catch (_e2) {
|
|
6136
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
|
|
6137
|
+
}
|
|
6138
|
+
} else {
|
|
6139
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
6140
|
+
}
|
|
6141
|
+
showTestResults(testMsgEl, results);
|
|
6142
|
+
delete window._onNativePermissionResult;
|
|
6143
|
+
};
|
|
6144
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
6145
|
+
return; // async — results shown in callback
|
|
5776
6146
|
}
|
|
6147
|
+
showTestResults(testMsgEl, results);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
|
|
6151
|
+
if (typeof Notification === "undefined") {
|
|
6152
|
+
results.push("\u2013 \u7cfb\u7edf\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
|
|
6153
|
+
showTestResults(testMsgEl, results);
|
|
5777
6154
|
return;
|
|
5778
6155
|
}
|
|
5779
6156
|
|
|
5780
|
-
|
|
6157
|
+
var perm = Notification.permission;
|
|
6158
|
+
if (perm === "granted") {
|
|
5781
6159
|
try {
|
|
5782
6160
|
var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
|
|
5783
|
-
body: "\
|
|
6161
|
+
body: "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
|
|
5784
6162
|
icon: "/favicon.ico",
|
|
5785
6163
|
tag: "wand-test",
|
|
5786
6164
|
});
|
|
5787
6165
|
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
|
-
}
|
|
6166
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
|
|
5793
6167
|
} 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
|
-
}
|
|
6168
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
|
|
5799
6169
|
}
|
|
6170
|
+
showTestResults(testMsgEl, results);
|
|
6171
|
+
} else if (perm === "denied") {
|
|
6172
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
|
|
6173
|
+
showTestResults(testMsgEl, results);
|
|
5800
6174
|
} else {
|
|
5801
|
-
//
|
|
6175
|
+
// "default" — try requesting
|
|
5802
6176
|
Notification.requestPermission().then(function(result) {
|
|
5803
6177
|
updateNotificationStatus();
|
|
5804
6178
|
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
|
-
}
|
|
6179
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
6180
|
+
} else {
|
|
6181
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
5812
6182
|
}
|
|
6183
|
+
showTestResults(testMsgEl, results);
|
|
5813
6184
|
});
|
|
5814
6185
|
}
|
|
5815
6186
|
}
|
|
5816
6187
|
|
|
6188
|
+
function showTestResults(el, results) {
|
|
6189
|
+
if (!el) return;
|
|
6190
|
+
el.innerHTML = results.map(function(r) { return escapeHtml(r); }).join("<br>");
|
|
6191
|
+
// color based on whether all passed
|
|
6192
|
+
var allOk = results.every(function(r) { return r.indexOf("\u2713") === 0 || r.indexOf("\u2013") === 0; });
|
|
6193
|
+
el.style.color = allOk ? "var(--success)" : "var(--warning)";
|
|
6194
|
+
el.classList.remove("hidden");
|
|
6195
|
+
}
|
|
6196
|
+
|
|
5817
6197
|
function quickStartSession() {
|
|
5818
6198
|
var command = getPreferredTool();
|
|
5819
6199
|
var defaultCwd = getEffectiveCwd();
|
|
@@ -6813,13 +7193,17 @@
|
|
|
6813
7193
|
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
6814
7194
|
userMsgs.push(userTurn);
|
|
6815
7195
|
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
7196
|
+
// Write optimistic user turn into session.messages so WS updates
|
|
7197
|
+
// that arrive before the HTTP response don't erase it.
|
|
6816
7198
|
updateSessionSnapshot({
|
|
6817
7199
|
id: session.id,
|
|
6818
7200
|
status: "running",
|
|
7201
|
+
messages: userMsgs,
|
|
6819
7202
|
structuredState: optimisticStructuredState,
|
|
6820
7203
|
});
|
|
6821
7204
|
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
6822
7205
|
status: "running",
|
|
7206
|
+
messages: userMsgs,
|
|
6823
7207
|
structuredState: optimisticStructuredState,
|
|
6824
7208
|
}), userMsgs);
|
|
6825
7209
|
updateInputHint("思考中…");
|
|
@@ -6832,6 +7216,11 @@
|
|
|
6832
7216
|
}
|
|
6833
7217
|
setDraftValue("");
|
|
6834
7218
|
|
|
7219
|
+
// Capture queue epoch before the POST so we can detect whether
|
|
7220
|
+
// a newer WS update has already refreshed the queue by the time
|
|
7221
|
+
// the HTTP response arrives.
|
|
7222
|
+
var epochBeforePost = state.queueEpoch;
|
|
7223
|
+
|
|
6835
7224
|
return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
|
|
6836
7225
|
method: "POST",
|
|
6837
7226
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6844,13 +7233,21 @@
|
|
|
6844
7233
|
throw new Error(snapshot.error);
|
|
6845
7234
|
}
|
|
6846
7235
|
if (snapshot && snapshot.id) {
|
|
7236
|
+
// If a WS update has already bumped the queue epoch, the HTTP
|
|
7237
|
+
// response's queuedMessages is stale — drop it to avoid
|
|
7238
|
+
// re-introducing already-dequeued items.
|
|
7239
|
+
if (state.queueEpoch > epochBeforePost && snapshot.queuedMessages) {
|
|
7240
|
+
delete snapshot.queuedMessages;
|
|
7241
|
+
}
|
|
6847
7242
|
updateSessionSnapshot(snapshot);
|
|
6848
7243
|
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
6849
7244
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
6850
7245
|
renderChat(true);
|
|
6851
7246
|
if (isQueueing) {
|
|
6852
7247
|
var queuedCount = getStructuredQueuedInputs(refreshedSession).length;
|
|
6853
|
-
|
|
7248
|
+
if (queuedCount > 0) {
|
|
7249
|
+
showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
7250
|
+
}
|
|
6854
7251
|
} else {
|
|
6855
7252
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
6856
7253
|
}
|
|
@@ -6902,7 +7299,25 @@
|
|
|
6902
7299
|
}
|
|
6903
7300
|
var queued = getStructuredQueuedInputs(session);
|
|
6904
7301
|
if (queued && queued.length > 0) {
|
|
7302
|
+
// Collect recent user message texts to deduplicate against queued items.
|
|
7303
|
+
// A queued message that already appears as a real user turn should not
|
|
7304
|
+
// be rendered a second time with the "排队中" badge.
|
|
7305
|
+
var existingUserTexts = {};
|
|
7306
|
+
for (var ei = base.length - 1; ei >= 0 && Object.keys(existingUserTexts).length < queued.length + 5; ei--) {
|
|
7307
|
+
var em = base[ei];
|
|
7308
|
+
if (em && em.role === "user" && Array.isArray(em.content)) {
|
|
7309
|
+
for (var ej = 0; ej < em.content.length; ej++) {
|
|
7310
|
+
if (em.content[ej] && em.content[ej].type === "text" && em.content[ej].text) {
|
|
7311
|
+
existingUserTexts[em.content[ej].text] = (existingUserTexts[em.content[ej].text] || 0) + 1;
|
|
7312
|
+
}
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
}
|
|
6905
7316
|
for (var qi = 0; qi < queued.length; qi++) {
|
|
7317
|
+
if (existingUserTexts[queued[qi]]) {
|
|
7318
|
+
existingUserTexts[queued[qi]]--;
|
|
7319
|
+
continue; // Skip — this queued text is already shown as a real message
|
|
7320
|
+
}
|
|
6906
7321
|
base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
|
|
6907
7322
|
}
|
|
6908
7323
|
}
|
|
@@ -8017,25 +8432,15 @@
|
|
|
8017
8432
|
|
|
8018
8433
|
function handleInputBoxBlur() {
|
|
8019
8434
|
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
8435
|
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
8436
|
window.scrollTo(0, 0);
|
|
8437
|
+
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
8438
|
+
// The container height restores but xterm needs an explicit refit to
|
|
8439
|
+
// fill the expanded space, and the scroll position needs resetting.
|
|
8440
|
+
if (isTouchDevice()) {
|
|
8441
|
+
ensureTerminalFit();
|
|
8442
|
+
maybeScrollTerminalToBottom("force");
|
|
8443
|
+
}
|
|
8039
8444
|
}, 100);
|
|
8040
8445
|
}
|
|
8041
8446
|
|
|
@@ -8757,24 +9162,19 @@
|
|
|
8757
9162
|
var isKeyboardOpen = offsetBottom > 50;
|
|
8758
9163
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
8759
9164
|
|
|
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
9165
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
8775
9166
|
syncInputBoxScroll(inputBox);
|
|
8776
9167
|
}
|
|
8777
9168
|
|
|
9169
|
+
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
9170
|
+
// after a delay so the keyboard dismiss animation and layout settle.
|
|
9171
|
+
if (keyboardOpen && !isKeyboardOpen) {
|
|
9172
|
+
setTimeout(function() {
|
|
9173
|
+
ensureTerminalFit();
|
|
9174
|
+
maybeScrollTerminalToBottom("force");
|
|
9175
|
+
}, 200);
|
|
9176
|
+
}
|
|
9177
|
+
|
|
8778
9178
|
keyboardOpen = isKeyboardOpen;
|
|
8779
9179
|
lastHeight = vv.height;
|
|
8780
9180
|
}
|
|
@@ -8879,6 +9279,11 @@
|
|
|
8879
9279
|
}
|
|
8880
9280
|
state.resizeHandler = function() { scheduleTerminalResize(true); };
|
|
8881
9281
|
window.addEventListener("resize", state.resizeHandler);
|
|
9282
|
+
// Also listen to visualViewport resize for pinch-zoom / browser zoom
|
|
9283
|
+
if (window.visualViewport) {
|
|
9284
|
+
state.visualViewportHandler = function() { scheduleTerminalResize(true); };
|
|
9285
|
+
window.visualViewport.addEventListener("resize", state.visualViewportHandler);
|
|
9286
|
+
}
|
|
8882
9287
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
8883
9288
|
}
|
|
8884
9289
|
|
|
@@ -8895,6 +9300,10 @@
|
|
|
8895
9300
|
window.removeEventListener("resize", state.resizeHandler);
|
|
8896
9301
|
state.resizeHandler = null;
|
|
8897
9302
|
}
|
|
9303
|
+
if (state.visualViewportHandler && window.visualViewport) {
|
|
9304
|
+
window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
|
|
9305
|
+
state.visualViewportHandler = null;
|
|
9306
|
+
}
|
|
8898
9307
|
[["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
|
|
8899
9308
|
["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
|
|
8900
9309
|
].forEach(function(pair) {
|
|
@@ -9027,6 +9436,12 @@
|
|
|
9027
9436
|
state.pollTimer = setInterval(refreshAll, 1600);
|
|
9028
9437
|
}
|
|
9029
9438
|
|
|
9439
|
+
// Periodically refresh session time displays (30s)
|
|
9440
|
+
setInterval(function() {
|
|
9441
|
+
var timeEls = document.querySelectorAll(".session-time");
|
|
9442
|
+
if (timeEls.length > 0) scheduleSessionListUpdate();
|
|
9443
|
+
}, 30000);
|
|
9444
|
+
|
|
9030
9445
|
function initWebSocket() {
|
|
9031
9446
|
if (!window.WebSocket) return false;
|
|
9032
9447
|
|
|
@@ -9095,8 +9510,9 @@
|
|
|
9095
9510
|
if (msg.data.messages) {
|
|
9096
9511
|
snapshot.messages = msg.data.messages;
|
|
9097
9512
|
}
|
|
9098
|
-
if (msg.data
|
|
9099
|
-
snapshot.queuedMessages = msg.data.queuedMessages;
|
|
9513
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
|
|
9514
|
+
snapshot.queuedMessages = msg.data.queuedMessages || [];
|
|
9515
|
+
state.queueEpoch++;
|
|
9100
9516
|
}
|
|
9101
9517
|
if (msg.data.structuredState) {
|
|
9102
9518
|
snapshot.structuredState = msg.data.structuredState;
|
|
@@ -9164,14 +9580,26 @@
|
|
|
9164
9580
|
// Trigger status bar completion animation
|
|
9165
9581
|
scheduleChatRender(true);
|
|
9166
9582
|
}
|
|
9167
|
-
// Notify user when a session completes
|
|
9583
|
+
// Notify user when a session completes — show what was accomplished
|
|
9168
9584
|
var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
9169
|
-
var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
|
|
9170
9585
|
var endedExitCode = msg.data && msg.data.exitCode;
|
|
9171
9586
|
var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
|
|
9587
|
+
// Build meaningful notification body
|
|
9588
|
+
var endedTaskSummary = endedSession ? (endedSession.summary || "") : "";
|
|
9589
|
+
var endedLastReply = endedSession ? getLastAssistantSummary(endedSession) : "";
|
|
9590
|
+
var endedNotifTitle = endedIsError ? "任务异常结束" : "任务已完成";
|
|
9591
|
+
var endedNotifBody = "";
|
|
9592
|
+
if (endedTaskSummary) {
|
|
9593
|
+
endedNotifBody = endedTaskSummary;
|
|
9594
|
+
if (endedLastReply && !endedIsError) {
|
|
9595
|
+
endedNotifBody += "\n" + endedLastReply;
|
|
9596
|
+
}
|
|
9597
|
+
} else {
|
|
9598
|
+
endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
|
|
9599
|
+
}
|
|
9172
9600
|
sendBrowserNotification(
|
|
9173
|
-
|
|
9174
|
-
|
|
9601
|
+
endedNotifTitle,
|
|
9602
|
+
endedNotifBody,
|
|
9175
9603
|
{
|
|
9176
9604
|
tag: "wand-ended-" + msg.sessionId,
|
|
9177
9605
|
onClick: function() {
|
|
@@ -9181,8 +9609,8 @@
|
|
|
9181
9609
|
);
|
|
9182
9610
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
9183
9611
|
showNotificationBubble({
|
|
9184
|
-
title:
|
|
9185
|
-
body:
|
|
9612
|
+
title: endedNotifTitle,
|
|
9613
|
+
body: endedNotifBody,
|
|
9186
9614
|
type: endedIsError ? "warning" : "success",
|
|
9187
9615
|
icon: endedIsError ? "!" : "\u2713",
|
|
9188
9616
|
duration: 6000,
|
|
@@ -9264,6 +9692,8 @@
|
|
|
9264
9692
|
state.currentTask = msg.data || null;
|
|
9265
9693
|
updateTaskDisplay();
|
|
9266
9694
|
}
|
|
9695
|
+
// Update session list to reflect current activity (debounced)
|
|
9696
|
+
scheduleSessionListUpdate();
|
|
9267
9697
|
break;
|
|
9268
9698
|
case 'status':
|
|
9269
9699
|
if (msg.sessionId && msg.data) {
|
|
@@ -9284,6 +9714,10 @@
|
|
|
9284
9714
|
});
|
|
9285
9715
|
}
|
|
9286
9716
|
}
|
|
9717
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
|
|
9718
|
+
statusUpdate.queuedMessages = msg.data.queuedMessages || [];
|
|
9719
|
+
state.queueEpoch++;
|
|
9720
|
+
}
|
|
9287
9721
|
if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
|
|
9288
9722
|
statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
|
|
9289
9723
|
}
|
|
@@ -9295,10 +9729,18 @@
|
|
|
9295
9729
|
};
|
|
9296
9730
|
// Browser notification for permission waiting (background tab)
|
|
9297
9731
|
var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
9298
|
-
var
|
|
9732
|
+
var permTaskName = permSession ? (permSession.summary || permSession.command || msg.sessionId) : msg.sessionId;
|
|
9733
|
+
var permDetail = msg.data.permissionRequest.prompt || "需要权限审批";
|
|
9734
|
+
var permTarget = msg.data.permissionRequest.target;
|
|
9735
|
+
var permBody = permTaskName;
|
|
9736
|
+
if (permTarget) {
|
|
9737
|
+
permBody += "\n" + permDetail + " · " + permTarget;
|
|
9738
|
+
} else {
|
|
9739
|
+
permBody += "\n" + permDetail;
|
|
9740
|
+
}
|
|
9299
9741
|
sendBrowserNotification(
|
|
9300
|
-
"
|
|
9301
|
-
|
|
9742
|
+
"需要你的授权",
|
|
9743
|
+
permBody,
|
|
9302
9744
|
{
|
|
9303
9745
|
tag: "wand-perm-" + msg.sessionId,
|
|
9304
9746
|
onClick: function() {
|
|
@@ -9311,12 +9753,12 @@
|
|
|
9311
9753
|
// In-app bubble if not currently viewing this session
|
|
9312
9754
|
if (msg.sessionId !== state.selectedId) {
|
|
9313
9755
|
showNotificationBubble({
|
|
9314
|
-
title: "
|
|
9315
|
-
body:
|
|
9756
|
+
title: "需要你的授权",
|
|
9757
|
+
body: permBody,
|
|
9316
9758
|
type: "warning",
|
|
9317
9759
|
icon: "!",
|
|
9318
9760
|
duration: 0,
|
|
9319
|
-
actionLabel: "
|
|
9761
|
+
actionLabel: "去处理",
|
|
9320
9762
|
action: function() {
|
|
9321
9763
|
selectSession(msg.sessionId);
|
|
9322
9764
|
}
|
|
@@ -9337,12 +9779,13 @@
|
|
|
9337
9779
|
}
|
|
9338
9780
|
// Re-render chat when structured session inFlight state changes
|
|
9339
9781
|
if (statusUpdate.structuredState) {
|
|
9340
|
-
|
|
9341
|
-
//
|
|
9782
|
+
// Flush queued structured messages synchronously before render
|
|
9783
|
+
// so the chat view uses up-to-date queue state.
|
|
9342
9784
|
if (!statusUpdate.structuredState.inFlight) {
|
|
9343
9785
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
9344
|
-
|
|
9786
|
+
flushStructuredInputQueue();
|
|
9345
9787
|
}
|
|
9788
|
+
scheduleChatRender();
|
|
9346
9789
|
}
|
|
9347
9790
|
}
|
|
9348
9791
|
}
|
|
@@ -9350,23 +9793,14 @@
|
|
|
9350
9793
|
case 'notification':
|
|
9351
9794
|
if (msg.data) {
|
|
9352
9795
|
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
|
-
});
|
|
9796
|
+
showUpdateBubble(msg.data.current || "-", msg.data.latest || "-");
|
|
9365
9797
|
sendBrowserNotification(
|
|
9366
9798
|
"Wand \u53d1\u73b0\u65b0\u7248\u672c",
|
|
9367
9799
|
"\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
|
|
9368
9800
|
{ tag: "wand-update" }
|
|
9369
9801
|
);
|
|
9802
|
+
} else if (msg.data.kind === "restart") {
|
|
9803
|
+
showRestartOverlay();
|
|
9370
9804
|
}
|
|
9371
9805
|
}
|
|
9372
9806
|
break;
|
|
@@ -9758,9 +10192,9 @@
|
|
|
9758
10192
|
return;
|
|
9759
10193
|
}
|
|
9760
10194
|
|
|
9761
|
-
var
|
|
10195
|
+
var allMessages = state.currentMessages;
|
|
9762
10196
|
|
|
9763
|
-
if (
|
|
10197
|
+
if (allMessages.length === 0) {
|
|
9764
10198
|
if (state.lastRenderedEmpty !== "empty") {
|
|
9765
10199
|
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
|
|
9766
10200
|
state.lastRenderedEmpty = "empty";
|
|
@@ -9769,6 +10203,16 @@
|
|
|
9769
10203
|
return;
|
|
9770
10204
|
}
|
|
9771
10205
|
|
|
10206
|
+
// Lazy loading: only render the most recent chatRenderedCount messages.
|
|
10207
|
+
// Auto-expand when new messages arrive during active streaming to avoid hiding them.
|
|
10208
|
+
var totalMsgCount = allMessages.length;
|
|
10209
|
+
if (totalMsgCount > state.chatRenderedCount && state.chatAutoFollow) {
|
|
10210
|
+
state.chatRenderedCount = totalMsgCount;
|
|
10211
|
+
}
|
|
10212
|
+
var visibleOffset = Math.max(0, totalMsgCount - state.chatRenderedCount);
|
|
10213
|
+
var messages = visibleOffset > 0 ? allMessages.slice(visibleOffset) : allMessages;
|
|
10214
|
+
var hasOlderMessages = visibleOffset > 0;
|
|
10215
|
+
|
|
9772
10216
|
// Check if messages actually changed
|
|
9773
10217
|
var msgCount = messages.length;
|
|
9774
10218
|
var outputHash = selectedSession.output ? selectedSession.output.length : 0;
|
|
@@ -9829,16 +10273,17 @@
|
|
|
9829
10273
|
// Build HTML with system info cards interleaved
|
|
9830
10274
|
var html = '';
|
|
9831
10275
|
var reversedMessages = messages.slice().reverse();
|
|
9832
|
-
var
|
|
10276
|
+
var visibleCount = messages.length;
|
|
9833
10277
|
|
|
9834
10278
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
9835
10279
|
var msg = reversedMessages[i];
|
|
9836
|
-
var
|
|
10280
|
+
var localIndex = visibleCount - 1 - i; // Index within visible slice
|
|
10281
|
+
var originalIndex = localIndex + visibleOffset; // Index in full messages array
|
|
9837
10282
|
|
|
9838
10283
|
// Find system info for this message position
|
|
9839
10284
|
var sysInfo = null;
|
|
9840
10285
|
for (var j = 0; j < systemInfo.length; j++) {
|
|
9841
|
-
if (systemInfo[j].beforeMessage ===
|
|
10286
|
+
if (systemInfo[j].beforeMessage === localIndex) {
|
|
9842
10287
|
sysInfo = systemInfo[j];
|
|
9843
10288
|
break;
|
|
9844
10289
|
}
|
|
@@ -9858,6 +10303,13 @@
|
|
|
9858
10303
|
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
9859
10304
|
}
|
|
9860
10305
|
|
|
10306
|
+
// Add sentinel for loading older messages (DOM end = visual top in column-reverse)
|
|
10307
|
+
if (hasOlderMessages) {
|
|
10308
|
+
html += '<div class="chat-load-more" id="chat-load-more-sentinel">' +
|
|
10309
|
+
'<button class="chat-load-more-btn" type="button">加载更早的 ' + Math.min(state.chatPageSize, visibleOffset) + ' 条消息</button>' +
|
|
10310
|
+
'</div>';
|
|
10311
|
+
}
|
|
10312
|
+
|
|
9861
10313
|
chatMessages.innerHTML = html;
|
|
9862
10314
|
attachAllCopyHandlers(chatMessages);
|
|
9863
10315
|
bindChatScrollListener();
|
|
@@ -9869,12 +10321,12 @@
|
|
|
9869
10321
|
if (cards.length > 0) {
|
|
9870
10322
|
var firstCard = cards[0];
|
|
9871
10323
|
var firstCardKey = getElementExpandKey(firstCard);
|
|
9872
|
-
if (
|
|
10324
|
+
if (getPersistedExpandState(firstCardKey) === null) {
|
|
9873
10325
|
firstCard.classList.remove("collapsed");
|
|
9874
10326
|
}
|
|
9875
10327
|
for (var ci = 1; ci < cards.length; ci++) {
|
|
9876
10328
|
var cardKey = getElementExpandKey(cards[ci]);
|
|
9877
|
-
if (
|
|
10329
|
+
if (getPersistedExpandState(cardKey) === null) {
|
|
9878
10330
|
cards[ci].classList.add("collapsed");
|
|
9879
10331
|
}
|
|
9880
10332
|
}
|
|
@@ -9883,6 +10335,7 @@
|
|
|
9883
10335
|
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
9884
10336
|
requestAnimationFrame(function() {
|
|
9885
10337
|
smartScrollToBottom(chatMessages);
|
|
10338
|
+
observeLoadMoreSentinel();
|
|
9886
10339
|
});
|
|
9887
10340
|
}
|
|
9888
10341
|
|
|
@@ -9892,7 +10345,7 @@
|
|
|
9892
10345
|
var allCards = container.querySelectorAll(".tool-use-card");
|
|
9893
10346
|
allCards.forEach(function(c) {
|
|
9894
10347
|
var cardKey = getElementExpandKey(c);
|
|
9895
|
-
if (
|
|
10348
|
+
if (getPersistedExpandState(cardKey) !== null) return;
|
|
9896
10349
|
// Keep expanded if this card is inside a newly added message
|
|
9897
10350
|
if (newEls) {
|
|
9898
10351
|
for (var i = 0; i < newEls.length; i++) {
|
|
@@ -9903,15 +10356,15 @@
|
|
|
9903
10356
|
});
|
|
9904
10357
|
}
|
|
9905
10358
|
|
|
9906
|
-
// Pre-compute per-round cumulative usage.
|
|
10359
|
+
// Pre-compute per-round cumulative usage using original (full array) indices.
|
|
9907
10360
|
// A "round" starts at a user message and includes all subsequent assistant turns
|
|
9908
10361
|
// until the next user message. Only the last assistant in each round shows the total.
|
|
9909
10362
|
var roundUsageByIndex = {};
|
|
9910
10363
|
(function() {
|
|
9911
10364
|
var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
9912
10365
|
var lastAssistantIdx = -1;
|
|
9913
|
-
for (var mi = 0; mi <
|
|
9914
|
-
var m =
|
|
10366
|
+
for (var mi = 0; mi < allMessages.length; mi++) {
|
|
10367
|
+
var m = allMessages[mi];
|
|
9915
10368
|
if (m.role === "user") {
|
|
9916
10369
|
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
9917
10370
|
roundUsageByIndex[lastAssistantIdx] = {
|
|
@@ -9955,7 +10408,7 @@
|
|
|
9955
10408
|
var insertedEls = [];
|
|
9956
10409
|
for (var i = 0; i < newMessages.length; i++) {
|
|
9957
10410
|
var div = document.createElement("div");
|
|
9958
|
-
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
10411
|
+
var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
|
|
9959
10412
|
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
9960
10413
|
var el = div.firstElementChild;
|
|
9961
10414
|
if (el) {
|
|
@@ -9987,7 +10440,7 @@
|
|
|
9987
10440
|
for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
|
|
9988
10441
|
var currentEl = existingEls[mi];
|
|
9989
10442
|
var tmpWrap = document.createElement("div");
|
|
9990
|
-
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
10443
|
+
var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
|
|
9991
10444
|
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
9992
10445
|
var replacementEl = tmpWrap.firstElementChild;
|
|
9993
10446
|
if (!replacementEl) continue;
|
|
@@ -10016,7 +10469,7 @@
|
|
|
10016
10469
|
var newestCard = null;
|
|
10017
10470
|
allCards.forEach(function(c) {
|
|
10018
10471
|
var cardKey = getElementExpandKey(c);
|
|
10019
|
-
if (
|
|
10472
|
+
if (getPersistedExpandState(cardKey) !== null) return;
|
|
10020
10473
|
if (newestMsgEl && newestMsgEl.contains(c)) {
|
|
10021
10474
|
if (!newestCard) newestCard = c;
|
|
10022
10475
|
else c.classList.add("collapsed");
|
|
@@ -10033,7 +10486,7 @@
|
|
|
10033
10486
|
renderStructuredStatusBar(chatMessages, selectedSession);
|
|
10034
10487
|
|
|
10035
10488
|
// Update todo progress bar from latest messages
|
|
10036
|
-
updateTodoProgress(
|
|
10489
|
+
updateTodoProgress(allMessages);
|
|
10037
10490
|
}
|
|
10038
10491
|
|
|
10039
10492
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
@@ -10043,7 +10496,7 @@
|
|
|
10043
10496
|
updateChatJumpToBottomButton();
|
|
10044
10497
|
return;
|
|
10045
10498
|
}
|
|
10046
|
-
var chatMsgs = container && container.classList && container.classList.contains("chat-messages")
|
|
10499
|
+
var chatMsgs = (container && container.classList && container.classList.contains("chat-messages"))
|
|
10047
10500
|
? container
|
|
10048
10501
|
: getChatScrollElement();
|
|
10049
10502
|
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
@@ -11106,7 +11559,8 @@
|
|
|
11106
11559
|
// Thinking card (deep thought) — from PTY parsing
|
|
11107
11560
|
if (msg.role === "thinking") {
|
|
11108
11561
|
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
11109
|
-
var
|
|
11562
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11563
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
11110
11564
|
return '<div class="chat-message thinking">' +
|
|
11111
11565
|
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
11112
11566
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
@@ -11243,7 +11697,8 @@
|
|
|
11243
11697
|
}
|
|
11244
11698
|
var summaryText = parts.join(" · ");
|
|
11245
11699
|
var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
|
|
11246
|
-
var
|
|
11700
|
+
var persistedExpanded = getPersistedExpandState(groupKey);
|
|
11701
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.toolGroup) : persistedExpanded;
|
|
11247
11702
|
|
|
11248
11703
|
// Render each item's inline-tool card
|
|
11249
11704
|
var innerHtml = "";
|
|
@@ -11354,7 +11809,8 @@
|
|
|
11354
11809
|
'</div>';
|
|
11355
11810
|
}
|
|
11356
11811
|
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
11357
|
-
var
|
|
11812
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11813
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
11358
11814
|
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
11815
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
11360
11816
|
'<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
|
|
@@ -11380,6 +11836,7 @@
|
|
|
11380
11836
|
function renderInlineTool(block, toolResult, toolName, fileInfo, extraInfo, messageKey, index) {
|
|
11381
11837
|
var toolId = block.id || "tool-" + toolName;
|
|
11382
11838
|
var expandKey = buildExpandKey("inline-tool", [messageKey, toolId || index, index]);
|
|
11839
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
11383
11840
|
var inputData = block.input || {};
|
|
11384
11841
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
11385
11842
|
|
|
@@ -11450,7 +11907,7 @@
|
|
|
11450
11907
|
var fullResult = resultContent;
|
|
11451
11908
|
|
|
11452
11909
|
var expandedHtml = "";
|
|
11453
|
-
var shouldExpand =
|
|
11910
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.inlineTools) : persistedExpanded;
|
|
11454
11911
|
if (hasResult) {
|
|
11455
11912
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
11456
11913
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
@@ -11462,16 +11919,23 @@
|
|
|
11462
11919
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
11463
11920
|
}
|
|
11464
11921
|
|
|
11922
|
+
var isTruncated = toolResult && toolResult._truncated === true;
|
|
11923
|
+
|
|
11465
11924
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
11466
11925
|
var extraClass = isError ? 'inline-tool-error-inline' : '';
|
|
11467
11926
|
if (shouldExpand) extraClass += ' inline-tool-open';
|
|
11468
11927
|
|
|
11928
|
+
var truncatedAttrs = isTruncated
|
|
11929
|
+
? 'data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '" '
|
|
11930
|
+
: '';
|
|
11931
|
+
|
|
11469
11932
|
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
11470
11933
|
'data-expand-kind="inline-tool" ' +
|
|
11471
11934
|
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
11472
11935
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
11473
11936
|
'data-preview="' + previewDataAttr + '" ' +
|
|
11474
11937
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
11938
|
+
truncatedAttrs +
|
|
11475
11939
|
'onclick="__inlineToolToggle(this)">' +
|
|
11476
11940
|
'<div class="inline-tool-row">' +
|
|
11477
11941
|
'<span class="inline-tool-status">' + statusIcon + '</span>' +
|
|
@@ -11490,6 +11954,7 @@
|
|
|
11490
11954
|
var resultContent = extractToolResultText(toolResult && toolResult.content);
|
|
11491
11955
|
var toolId = block.id || "tool-" + toolName;
|
|
11492
11956
|
var expandKey = buildExpandKey("terminal", [messageKey, toolId || index, index]);
|
|
11957
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
11493
11958
|
|
|
11494
11959
|
var isError = toolResult && toolResult.is_error;
|
|
11495
11960
|
var exitCode = inputData.exitCode;
|
|
@@ -11527,9 +11992,14 @@
|
|
|
11527
11992
|
|
|
11528
11993
|
// Show command preview in header (truncate long commands)
|
|
11529
11994
|
var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
|
|
11530
|
-
var shouldExpand =
|
|
11995
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.terminal) : persistedExpanded;
|
|
11996
|
+
|
|
11997
|
+
var termTruncated = toolResult && toolResult._truncated === true;
|
|
11998
|
+
var termTruncAttrs = termTruncated
|
|
11999
|
+
? ' data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '"'
|
|
12000
|
+
: '';
|
|
11531
12001
|
|
|
11532
|
-
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '">' +
|
|
12002
|
+
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '"' + termTruncAttrs + '>' +
|
|
11533
12003
|
'<div class="term-header" onclick="__terminalExpand(this)">' +
|
|
11534
12004
|
statusDot +
|
|
11535
12005
|
'<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
|
|
@@ -11653,33 +12123,116 @@
|
|
|
11653
12123
|
return renderDiffTool(block, toolResult, toolName);
|
|
11654
12124
|
}
|
|
11655
12125
|
|
|
11656
|
-
// ── AskUserQuestion tool — special card
|
|
12126
|
+
// ── AskUserQuestion tool — special card with batch submit
|
|
11657
12127
|
if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
|
|
11658
12128
|
var questions = block.input.questions;
|
|
11659
12129
|
if (questions && questions.length > 0) {
|
|
12130
|
+
var isAnswered = !!toolResult;
|
|
12131
|
+
var sel = state.askUserSelections[toolId] || {};
|
|
12132
|
+
var isSubmitted = !!sel.submitted;
|
|
12133
|
+
var answerText = isAnswered ? extractToolResultText(toolResult.content) : "";
|
|
12134
|
+
var answerLines = answerText ? answerText.trim().split("\n") : [];
|
|
12135
|
+
|
|
12136
|
+
// Build header summary
|
|
12137
|
+
var headerLabel = "";
|
|
12138
|
+
for (var hi = 0; hi < questions.length; hi++) {
|
|
12139
|
+
if (questions[hi].header) { headerLabel = questions[hi].header; break; }
|
|
12140
|
+
}
|
|
12141
|
+
var headerSummary = headerLabel ? '<span class="tool-use-summary">' + escapeHtml(headerLabel) + '</span>' : "";
|
|
12142
|
+
|
|
11660
12143
|
var questionsHtml = "";
|
|
11661
12144
|
questions.forEach(function(question, qIdx) {
|
|
12145
|
+
var isMulti = !!question.multiSelect;
|
|
11662
12146
|
var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
|
|
11663
12147
|
var optionsHtml = "";
|
|
11664
12148
|
if (question.options && question.options.length > 0) {
|
|
11665
|
-
optionsHtml = '<div class="ask-user-options">';
|
|
12149
|
+
optionsHtml = '<div class="ask-user-options" data-multi-select="' + isMulti + '">';
|
|
11666
12150
|
question.options.forEach(function(opt, idx) {
|
|
11667
12151
|
var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
|
|
11668
|
-
|
|
11669
|
-
|
|
11670
|
-
|
|
12152
|
+
var descHtml = opt.description ? '<div class="ask-user-option-desc">' + escapeHtml(opt.description) + '</div>' : "";
|
|
12153
|
+
|
|
12154
|
+
if (isAnswered) {
|
|
12155
|
+
// Read-only: check if this option was the chosen answer
|
|
12156
|
+
var answerLine = answerLines[qIdx] || answerLines[0] || "";
|
|
12157
|
+
var chosenLabels = answerLine.split(",").map(function(s) { return s.trim(); });
|
|
12158
|
+
var isChosen = chosenLabels.indexOf(opt.label || "") !== -1;
|
|
12159
|
+
optionsHtml += '<div class="ask-user-option ask-user-option-readonly' + (isChosen ? ' ask-user-option-chosen' : '') + '">' +
|
|
12160
|
+
'<span class="ask-user-indicator"></span>' +
|
|
12161
|
+
'<div class="ask-user-option-content">' +
|
|
12162
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
12163
|
+
descHtml +
|
|
12164
|
+
'</div>' +
|
|
12165
|
+
'</div>';
|
|
12166
|
+
} else {
|
|
12167
|
+
// Interactive: selection state from askUserSelections
|
|
12168
|
+
var isSelected = (sel[qIdx] || []).indexOf(idx) !== -1;
|
|
12169
|
+
var disabledAttr = isSubmitted ? ' disabled' : '';
|
|
12170
|
+
optionsHtml += '<button class="ask-user-option' + (isSelected ? ' selected' : '') + '"' +
|
|
12171
|
+
' data-option-index="' + idx + '"' +
|
|
12172
|
+
' data-question-index="' + qIdx + '"' +
|
|
12173
|
+
' data-option-label="' + escapeHtml(opt.label || "选项 " + (idx + 1)) + '"' +
|
|
12174
|
+
' onclick="__askSelect(\'' + escapeHtml(toolId) + '\',' + qIdx + ',' + idx + ',' + isMulti + ')"' +
|
|
12175
|
+
disabledAttr + '>' +
|
|
12176
|
+
'<span class="ask-user-indicator"></span>' +
|
|
12177
|
+
'<div class="ask-user-option-content">' +
|
|
12178
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
12179
|
+
descHtml +
|
|
12180
|
+
'</div>' +
|
|
12181
|
+
'</button>';
|
|
12182
|
+
}
|
|
11671
12183
|
});
|
|
11672
12184
|
optionsHtml += '</div>';
|
|
11673
12185
|
}
|
|
11674
|
-
questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
|
|
12186
|
+
questionsHtml += '<div class="ask-user-question-group" data-question-index="' + qIdx + '">' + questionText + optionsHtml + '</div>';
|
|
11675
12187
|
});
|
|
11676
|
-
|
|
12188
|
+
|
|
12189
|
+
// Submit button (only for interactive state)
|
|
12190
|
+
var actionsHtml = "";
|
|
12191
|
+
if (!isAnswered) {
|
|
12192
|
+
var allAnsweredCheck = true;
|
|
12193
|
+
for (var qi = 0; qi < questions.length; qi++) {
|
|
12194
|
+
if (!sel[qi] || sel[qi].length === 0) { allAnsweredCheck = false; break; }
|
|
12195
|
+
}
|
|
12196
|
+
var submitDisabled = (!allAnsweredCheck || isSubmitted) ? " disabled" : "";
|
|
12197
|
+
var submitClass = isSubmitted ? " ask-user-submitted" : "";
|
|
12198
|
+
var submitText = isSubmitted ? "已提交..." : "确认提交";
|
|
12199
|
+
actionsHtml = '<div class="ask-user-actions">' +
|
|
12200
|
+
'<button class="ask-user-submit' + submitClass + '" data-tool-use-id="' + escapeHtml(toolId) + '"' +
|
|
12201
|
+
' onclick="__askSubmit(\'' + escapeHtml(toolId) + '\')"' + submitDisabled + '>' +
|
|
12202
|
+
submitText +
|
|
12203
|
+
'</button>' +
|
|
12204
|
+
'</div>';
|
|
12205
|
+
}
|
|
12206
|
+
|
|
12207
|
+
// Answered summary for header
|
|
12208
|
+
var answeredSummary = "";
|
|
12209
|
+
if (isAnswered && answerText) {
|
|
12210
|
+
var shortAnswer = answerText.trim().replace(/\n/g, ", ");
|
|
12211
|
+
if (shortAnswer.length > 40) shortAnswer = shortAnswer.slice(0, 37) + "...";
|
|
12212
|
+
answeredSummary = '<span class="tool-use-file">' + escapeHtml(shortAnswer) + '</span>';
|
|
12213
|
+
}
|
|
12214
|
+
|
|
12215
|
+
// Expand state: default expanded when unanswered, collapsed when answered
|
|
12216
|
+
var askExpandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
12217
|
+
var askPersisted = getPersistedExpandState(askExpandKey);
|
|
12218
|
+
var askShouldExpand = askPersisted === null ? !isAnswered : askPersisted;
|
|
12219
|
+
var askCollapsed = askShouldExpand ? "" : " collapsed";
|
|
12220
|
+
var answeredClass = isAnswered ? " ask-user-answered" : "";
|
|
12221
|
+
|
|
12222
|
+
return '<div class="tool-use-card ask-user' + answeredClass + askCollapsed + '"' +
|
|
12223
|
+
' data-tool-use-id="' + escapeHtml(toolId) + '"' +
|
|
12224
|
+
' data-expand-kind="tool-card"' +
|
|
12225
|
+
' data-expand-key="' + escapeHtml(askExpandKey) + '">' +
|
|
11677
12226
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
11678
|
-
'<span class="tool-use-icon"
|
|
12227
|
+
'<span class="tool-use-icon">' + (isAnswered ? '✓' : '?') + '</span>' +
|
|
11679
12228
|
'<span class="tool-use-name">提问</span>' +
|
|
12229
|
+
headerSummary +
|
|
12230
|
+
answeredSummary +
|
|
12231
|
+
'<span class="tool-use-toggle">▼</span>' +
|
|
11680
12232
|
'</div>' +
|
|
11681
12233
|
'<div class="tool-use-body ask-user-body">' +
|
|
11682
12234
|
questionsHtml +
|
|
12235
|
+
actionsHtml +
|
|
11683
12236
|
'</div>' +
|
|
11684
12237
|
'</div>';
|
|
11685
12238
|
}
|
|
@@ -11725,10 +12278,13 @@
|
|
|
11725
12278
|
}
|
|
11726
12279
|
|
|
11727
12280
|
var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11728
|
-
var
|
|
12281
|
+
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
12282
|
+
var cardDefaultExpand = !!(state.config && state.config.cardDefaults && state.config.cardDefaults.editCards);
|
|
12283
|
+
var shouldExpand = persistedExpanded === null ? (statusClass === "loading" || cardDefaultExpand) : persistedExpanded;
|
|
12284
|
+
var tcTruncated = toolResult && toolResult._truncated === true;
|
|
11729
12285
|
var collapsedClass = shouldExpand ? "" : " collapsed";
|
|
11730
12286
|
var toggleHtml = '<span class="tool-use-toggle">▼</span>';
|
|
11731
|
-
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
12287
|
+
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '"' + (tcTruncated ? ' data-truncated="true"' : '') + '>' +
|
|
11732
12288
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
11733
12289
|
'<span class="tool-use-icon">' + headerIcon + '</span>' +
|
|
11734
12290
|
'<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
|
|
@@ -12149,8 +12705,8 @@
|
|
|
12149
12705
|
|
|
12150
12706
|
var notificationStack = [];
|
|
12151
12707
|
var notificationIdCounter = 0;
|
|
12152
|
-
var NOTIFICATION_GAP =
|
|
12153
|
-
var NOTIFICATION_TOP =
|
|
12708
|
+
var NOTIFICATION_GAP = 6;
|
|
12709
|
+
var NOTIFICATION_TOP = 16;
|
|
12154
12710
|
|
|
12155
12711
|
/**
|
|
12156
12712
|
* Show an in-app notification bubble at bottom-right.
|
|
@@ -12165,6 +12721,12 @@
|
|
|
12165
12721
|
* @returns {{ dismiss: function }} handle
|
|
12166
12722
|
*/
|
|
12167
12723
|
function showNotificationBubble(opts) {
|
|
12724
|
+
// Play sound for important notifications — independent of bubble setting
|
|
12725
|
+
if (opts.actionLabel || opts.playSound) playNotificationSound();
|
|
12726
|
+
|
|
12727
|
+
// Respect user preference (skip if bubbles disabled)
|
|
12728
|
+
if (!state.notifBubble) return { dismiss: function() {} };
|
|
12729
|
+
|
|
12168
12730
|
var id = ++notificationIdCounter;
|
|
12169
12731
|
var type = opts.type || "info";
|
|
12170
12732
|
var icon = opts.icon || (type === "warning" ? "!" : type === "success" ? "\u2713" : "i");
|
|
@@ -12182,7 +12744,7 @@
|
|
|
12182
12744
|
'</div>';
|
|
12183
12745
|
|
|
12184
12746
|
var bodyHtml = opts.body
|
|
12185
|
-
? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
|
|
12747
|
+
? '<div class="notification-bubble-body">' + escapeHtml(opts.body).replace(/\n/g, '<br>') + '</div>'
|
|
12186
12748
|
: '';
|
|
12187
12749
|
|
|
12188
12750
|
var actionsHtml = opts.actionLabel
|
|
@@ -12248,13 +12810,43 @@
|
|
|
12248
12810
|
|
|
12249
12811
|
// ── Browser Notification API ──
|
|
12250
12812
|
|
|
12813
|
+
// Detect Android APK native bridge
|
|
12814
|
+
var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
|
|
12815
|
+
// Detect if running inside APK and extract installed version from User-Agent
|
|
12816
|
+
var _apkVersionMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
|
|
12817
|
+
var _apkVersion = _apkVersionMatch ? _apkVersionMatch[1] : null;
|
|
12818
|
+
|
|
12819
|
+
function _getNativePermission() {
|
|
12820
|
+
if (_hasNativeBridge && typeof WandNative.getPermission === "function") {
|
|
12821
|
+
try { return WandNative.getPermission(); } catch (_e) {}
|
|
12822
|
+
}
|
|
12823
|
+
return null;
|
|
12824
|
+
}
|
|
12825
|
+
|
|
12251
12826
|
function requestNotificationPermission() {
|
|
12827
|
+
if (_hasNativeBridge) {
|
|
12828
|
+
var perm = _getNativePermission();
|
|
12829
|
+
if (perm === "default" || perm === "denied") {
|
|
12830
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
12831
|
+
}
|
|
12832
|
+
return;
|
|
12833
|
+
}
|
|
12252
12834
|
if (typeof Notification !== "undefined" && Notification.permission === "default") {
|
|
12253
12835
|
Notification.requestPermission();
|
|
12254
12836
|
}
|
|
12255
12837
|
}
|
|
12256
12838
|
|
|
12257
12839
|
function sendBrowserNotification(title, body, opts) {
|
|
12840
|
+
// Native Android bridge path
|
|
12841
|
+
if (_hasNativeBridge) {
|
|
12842
|
+
var perm = _getNativePermission();
|
|
12843
|
+
if (perm !== "granted") return;
|
|
12844
|
+
try {
|
|
12845
|
+
WandNative.sendNotification(title || "Wand", body || "", (opts && opts.tag) || "");
|
|
12846
|
+
} catch (_e) {}
|
|
12847
|
+
return;
|
|
12848
|
+
}
|
|
12849
|
+
// Browser Notification API path
|
|
12258
12850
|
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
12259
12851
|
if (!document.hidden) return; // Only notify when tab is in background
|
|
12260
12852
|
try {
|
|
@@ -12275,6 +12867,221 @@
|
|
|
12275
12867
|
}
|
|
12276
12868
|
}
|
|
12277
12869
|
|
|
12870
|
+
/**
|
|
12871
|
+
* Play a soft, rounded notification chime using Web Audio API.
|
|
12872
|
+
* Two ascending sine tones with smooth gain envelope — gentle on the ears.
|
|
12873
|
+
*/
|
|
12874
|
+
function playNotificationSound() {
|
|
12875
|
+
if (!state.notifSound) return;
|
|
12876
|
+
_doPlaySound();
|
|
12877
|
+
}
|
|
12878
|
+
|
|
12879
|
+
/**
|
|
12880
|
+
* Try to play the notification sound regardless of user preference.
|
|
12881
|
+
* Returns true if playback was initiated successfully.
|
|
12882
|
+
* Used by the test function to always attempt playback.
|
|
12883
|
+
*/
|
|
12884
|
+
function tryPlayNotificationSound() {
|
|
12885
|
+
return _doPlaySound();
|
|
12886
|
+
}
|
|
12887
|
+
|
|
12888
|
+
function _doPlaySound() {
|
|
12889
|
+
try {
|
|
12890
|
+
var AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
12891
|
+
if (!AudioCtx) return false;
|
|
12892
|
+
var ctx = new AudioCtx();
|
|
12893
|
+
|
|
12894
|
+
// Some browsers suspend AudioContext until user gesture — resume it
|
|
12895
|
+
if (ctx.state === "suspended") ctx.resume();
|
|
12896
|
+
|
|
12897
|
+
function tone(freq, start, dur) {
|
|
12898
|
+
var osc = ctx.createOscillator();
|
|
12899
|
+
var gain = ctx.createGain();
|
|
12900
|
+
osc.type = "sine";
|
|
12901
|
+
osc.frequency.value = freq;
|
|
12902
|
+
gain.gain.setValueAtTime(0, ctx.currentTime + start);
|
|
12903
|
+
gain.gain.linearRampToValueAtTime(0.18, ctx.currentTime + start + 0.04);
|
|
12904
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur);
|
|
12905
|
+
osc.connect(gain);
|
|
12906
|
+
gain.connect(ctx.destination);
|
|
12907
|
+
osc.start(ctx.currentTime + start);
|
|
12908
|
+
osc.stop(ctx.currentTime + start + dur);
|
|
12909
|
+
}
|
|
12910
|
+
|
|
12911
|
+
// Two-tone ascending chime: C5 → E5, soft and brief
|
|
12912
|
+
tone(523, 0, 0.25);
|
|
12913
|
+
tone(659, 0.12, 0.3);
|
|
12914
|
+
|
|
12915
|
+
// Clean up context after playback
|
|
12916
|
+
setTimeout(function() { ctx.close(); }, 600);
|
|
12917
|
+
return true;
|
|
12918
|
+
} catch (_e) {
|
|
12919
|
+
// Web Audio not available or blocked
|
|
12920
|
+
return false;
|
|
12921
|
+
}
|
|
12922
|
+
}
|
|
12923
|
+
|
|
12924
|
+
/**
|
|
12925
|
+
* Show an interactive update bubble that allows updating and restarting
|
|
12926
|
+
* directly from the notification, without navigating to settings.
|
|
12927
|
+
*/
|
|
12928
|
+
function showUpdateBubble(currentVer, latestVer) {
|
|
12929
|
+
// Prevent duplicate bubbles
|
|
12930
|
+
if (state._updateBubbleShown) return;
|
|
12931
|
+
state._updateBubbleShown = true;
|
|
12932
|
+
|
|
12933
|
+
playNotificationSound();
|
|
12934
|
+
|
|
12935
|
+
var id = ++notificationIdCounter;
|
|
12936
|
+
var bubble = document.createElement("div");
|
|
12937
|
+
bubble.className = "notification-bubble";
|
|
12938
|
+
bubble.setAttribute("data-nid", id);
|
|
12939
|
+
|
|
12940
|
+
bubble.innerHTML =
|
|
12941
|
+
'<div class="notification-bubble-header">' +
|
|
12942
|
+
'<span class="notification-bubble-icon info">\u2191</span>' +
|
|
12943
|
+
'<span class="notification-bubble-title">\u53d1\u73b0\u65b0\u7248\u672c</span>' +
|
|
12944
|
+
'<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
|
|
12945
|
+
'</div>' +
|
|
12946
|
+
'<div class="notification-bubble-body">' +
|
|
12947
|
+
escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
|
|
12948
|
+
'</div>' +
|
|
12949
|
+
'<div class="notification-bubble-actions">' +
|
|
12950
|
+
'<button class="primary" id="update-bubble-action">\u7acb\u5373\u66f4\u65b0</button>' +
|
|
12951
|
+
'</div>';
|
|
12952
|
+
|
|
12953
|
+
document.body.appendChild(bubble);
|
|
12954
|
+
|
|
12955
|
+
var entry = { id: id, el: bubble };
|
|
12956
|
+
notificationStack.push(entry);
|
|
12957
|
+
repositionNotifications();
|
|
12958
|
+
|
|
12959
|
+
var closeBtn = bubble.querySelector(".notification-bubble-close");
|
|
12960
|
+
if (closeBtn) closeBtn.onclick = function() {
|
|
12961
|
+
dismissNotification(id);
|
|
12962
|
+
state._updateBubbleShown = false;
|
|
12963
|
+
};
|
|
12964
|
+
|
|
12965
|
+
var actionBtn = bubble.querySelector("#update-bubble-action");
|
|
12966
|
+
var bodyEl = bubble.querySelector(".notification-bubble-body");
|
|
12967
|
+
|
|
12968
|
+
if (actionBtn) actionBtn.onclick = function() {
|
|
12969
|
+
// Phase 1: Performing update
|
|
12970
|
+
actionBtn.disabled = true;
|
|
12971
|
+
actionBtn.textContent = "\u66f4\u65b0\u4e2d\u2026";
|
|
12972
|
+
if (bodyEl) bodyEl.textContent = "\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026";
|
|
12973
|
+
|
|
12974
|
+
fetch("/api/update", {
|
|
12975
|
+
method: "POST",
|
|
12976
|
+
headers: { "Content-Type": "application/json" },
|
|
12977
|
+
credentials: "same-origin"
|
|
12978
|
+
})
|
|
12979
|
+
.then(function(res) { return res.json(); })
|
|
12980
|
+
.then(function(data) {
|
|
12981
|
+
if (data.error) {
|
|
12982
|
+
// Update failed
|
|
12983
|
+
if (bodyEl) {
|
|
12984
|
+
bodyEl.textContent = data.error;
|
|
12985
|
+
bodyEl.style.color = "var(--error)";
|
|
12986
|
+
}
|
|
12987
|
+
actionBtn.disabled = false;
|
|
12988
|
+
actionBtn.textContent = "\u91cd\u8bd5";
|
|
12989
|
+
return;
|
|
12990
|
+
}
|
|
12991
|
+
// Phase 2: Update succeeded, show restart button
|
|
12992
|
+
if (bodyEl) {
|
|
12993
|
+
bodyEl.textContent = data.message || "\u66f4\u65b0\u5b8c\u6210";
|
|
12994
|
+
bodyEl.style.color = "var(--success)";
|
|
12995
|
+
}
|
|
12996
|
+
actionBtn.textContent = "\u91cd\u542f\u751f\u6548";
|
|
12997
|
+
actionBtn.disabled = false;
|
|
12998
|
+
actionBtn.className = "primary success";
|
|
12999
|
+
actionBtn.onclick = function() {
|
|
13000
|
+
performRestart(actionBtn, bodyEl);
|
|
13001
|
+
};
|
|
13002
|
+
})
|
|
13003
|
+
.catch(function() {
|
|
13004
|
+
if (bodyEl) {
|
|
13005
|
+
bodyEl.textContent = "\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002";
|
|
13006
|
+
bodyEl.style.color = "var(--error)";
|
|
13007
|
+
}
|
|
13008
|
+
actionBtn.disabled = false;
|
|
13009
|
+
actionBtn.textContent = "\u91cd\u8bd5";
|
|
13010
|
+
});
|
|
13011
|
+
};
|
|
13012
|
+
}
|
|
13013
|
+
|
|
13014
|
+
/**
|
|
13015
|
+
* Call POST /api/restart and show the restart overlay.
|
|
13016
|
+
*/
|
|
13017
|
+
function performRestart(btn, msgEl) {
|
|
13018
|
+
if (btn) {
|
|
13019
|
+
btn.disabled = true;
|
|
13020
|
+
btn.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
|
|
13021
|
+
}
|
|
13022
|
+
if (msgEl) {
|
|
13023
|
+
msgEl.textContent = "\u670d\u52a1\u6b63\u5728\u91cd\u542f\u2026";
|
|
13024
|
+
msgEl.style.color = "var(--text-secondary)";
|
|
13025
|
+
}
|
|
13026
|
+
|
|
13027
|
+
fetch("/api/restart", {
|
|
13028
|
+
method: "POST",
|
|
13029
|
+
headers: { "Content-Type": "application/json" },
|
|
13030
|
+
credentials: "same-origin"
|
|
13031
|
+
})
|
|
13032
|
+
.then(function(res) { return res.json(); })
|
|
13033
|
+
.then(function() {
|
|
13034
|
+
showRestartOverlay();
|
|
13035
|
+
})
|
|
13036
|
+
.catch(function() {
|
|
13037
|
+
// Network error likely means server already shut down — show overlay anyway
|
|
13038
|
+
showRestartOverlay();
|
|
13039
|
+
});
|
|
13040
|
+
}
|
|
13041
|
+
|
|
13042
|
+
/**
|
|
13043
|
+
* Full-screen overlay shown during server restart.
|
|
13044
|
+
* Polls /api/config until the server comes back, then reloads the page.
|
|
13045
|
+
*/
|
|
13046
|
+
function showRestartOverlay() {
|
|
13047
|
+
// Avoid duplicates
|
|
13048
|
+
if (document.getElementById("restart-overlay")) return;
|
|
13049
|
+
|
|
13050
|
+
var overlay = document.createElement("div");
|
|
13051
|
+
overlay.id = "restart-overlay";
|
|
13052
|
+
overlay.className = "restart-overlay";
|
|
13053
|
+
overlay.innerHTML =
|
|
13054
|
+
'<div class="restart-overlay-content">' +
|
|
13055
|
+
'<div class="restart-spinner"></div>' +
|
|
13056
|
+
'<div class="restart-title">\u670d\u52a1\u6b63\u5728\u91cd\u542f</div>' +
|
|
13057
|
+
'<div class="restart-subtitle">\u7a0d\u540e\u5c06\u81ea\u52a8\u5237\u65b0\u9875\u9762\u2026</div>' +
|
|
13058
|
+
'</div>';
|
|
13059
|
+
document.body.appendChild(overlay);
|
|
13060
|
+
|
|
13061
|
+
var attempts = 0;
|
|
13062
|
+
var maxAttempts = 20; // 20 * 2s = 40s
|
|
13063
|
+
var timer = setInterval(function() {
|
|
13064
|
+
attempts++;
|
|
13065
|
+
fetch("/api/config", { credentials: "same-origin" })
|
|
13066
|
+
.then(function(res) {
|
|
13067
|
+
if (res.ok) {
|
|
13068
|
+
clearInterval(timer);
|
|
13069
|
+
location.reload();
|
|
13070
|
+
}
|
|
13071
|
+
})
|
|
13072
|
+
.catch(function() {
|
|
13073
|
+
// Server not ready yet
|
|
13074
|
+
});
|
|
13075
|
+
if (attempts >= maxAttempts) {
|
|
13076
|
+
clearInterval(timer);
|
|
13077
|
+
var subtitle = overlay.querySelector(".restart-subtitle");
|
|
13078
|
+
if (subtitle) {
|
|
13079
|
+
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';
|
|
13080
|
+
}
|
|
13081
|
+
}
|
|
13082
|
+
}, 2000);
|
|
13083
|
+
}
|
|
13084
|
+
|
|
12278
13085
|
function escapeHtml(value) {
|
|
12279
13086
|
return String(value)
|
|
12280
13087
|
.replace(/&/g, "&")
|