@co0ontty/wand 1.7.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/git-worktree.d.ts +17 -1
- package/dist/git-worktree.js +244 -0
- package/dist/resume-policy.d.ts +0 -77
- package/dist/resume-policy.js +0 -162
- package/dist/server-session-routes.js +153 -0
- package/dist/server.js +27 -1
- package/dist/storage.d.ts +1 -0
- package/dist/storage.js +205 -141
- package/dist/types.d.ts +34 -0
- package/dist/web-ui/content/scripts.js +850 -174
- package/dist/web-ui/content/styles.css +854 -144
- package/dist/web-ui/scripts.js +3 -6
- package/package.json +1 -1
|
@@ -117,6 +117,11 @@
|
|
|
117
117
|
sessionCreateKind: "structured",
|
|
118
118
|
sessionCreateWorktree: false,
|
|
119
119
|
sessionTool: "claude",
|
|
120
|
+
activeWorktreeMergeSessionId: null,
|
|
121
|
+
worktreeMergeCheckResult: null,
|
|
122
|
+
worktreeMergeLoading: false,
|
|
123
|
+
worktreeMergeSubmitting: false,
|
|
124
|
+
worktreeMergeError: "",
|
|
120
125
|
preferredCommand: "claude",
|
|
121
126
|
structuredRunner: "claude-cli-print",
|
|
122
127
|
lastResize: { cols: 0, rows: 0 },
|
|
@@ -125,6 +130,13 @@
|
|
|
125
130
|
showInstallPrompt: false,
|
|
126
131
|
ws: null,
|
|
127
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
|
+
})(),
|
|
128
140
|
currentView: "terminal",
|
|
129
141
|
terminalScale: (function() {
|
|
130
142
|
try {
|
|
@@ -178,6 +190,8 @@
|
|
|
178
190
|
sessionsManageMode: false,
|
|
179
191
|
selectedSessionIds: {},
|
|
180
192
|
selectedClaudeHistoryIds: {},
|
|
193
|
+
askUserSelections: {}, // { toolUseId: { 0: [optIdx...], submitted: false } }
|
|
194
|
+
queueEpoch: 0, // Monotonic counter for queue state freshness
|
|
181
195
|
// Load last used working directory from localStorage
|
|
182
196
|
workingDir: (function() {
|
|
183
197
|
try {
|
|
@@ -322,6 +336,8 @@
|
|
|
322
336
|
if (button) {
|
|
323
337
|
button.classList.toggle("visible", shouldShow);
|
|
324
338
|
}
|
|
339
|
+
var chatContainer = document.getElementById("chat-output");
|
|
340
|
+
if (chatContainer) chatContainer.classList.toggle("has-jump-btn", shouldShow);
|
|
325
341
|
}
|
|
326
342
|
|
|
327
343
|
function scrollChatToBottom(smooth) {
|
|
@@ -688,6 +704,7 @@
|
|
|
688
704
|
state.lastRenderedMsgCount = 0;
|
|
689
705
|
state.lastRenderedEmpty = null;
|
|
690
706
|
state.renderPending = false;
|
|
707
|
+
state.askUserSelections = {};
|
|
691
708
|
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
692
709
|
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
693
710
|
}
|
|
@@ -836,18 +853,7 @@
|
|
|
836
853
|
refreshAll();
|
|
837
854
|
requestNotificationPermission();
|
|
838
855
|
if (config.updateAvailable && config.latestVersion) {
|
|
839
|
-
|
|
840
|
-
title: "\u53d1\u73b0\u65b0\u7248\u672c",
|
|
841
|
-
body: "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion,
|
|
842
|
-
type: "info",
|
|
843
|
-
icon: "\u2191",
|
|
844
|
-
duration: 10000,
|
|
845
|
-
actionLabel: "\u53bb\u66f4\u65b0",
|
|
846
|
-
action: function() {
|
|
847
|
-
var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
|
|
848
|
-
if (settingsBtn) settingsBtn.click();
|
|
849
|
-
}
|
|
850
|
-
});
|
|
856
|
+
showUpdateBubble(config.currentVersion || "-", config.latestVersion);
|
|
851
857
|
sendBrowserNotification("Wand \u53d1\u73b0\u65b0\u7248\u672c", "\u5f53\u524d " + (config.currentVersion || "-") + " \u2192 \u6700\u65b0 " + config.latestVersion, { tag: "wand-update" });
|
|
852
858
|
}
|
|
853
859
|
if (state.claudeHistoryExpanded && !state.claudeHistoryLoaded) {
|
|
@@ -1043,11 +1049,6 @@
|
|
|
1043
1049
|
var composerMode = getSafeModeForTool(preferredTool, state.chatMode);
|
|
1044
1050
|
|
|
1045
1051
|
return '<div class="app-container">' +
|
|
1046
|
-
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="Toggle sidebar">' +
|
|
1047
|
-
'<span class="hamburger-icon">' +
|
|
1048
|
-
'<span></span><span></span><span></span>' +
|
|
1049
|
-
'</span>' +
|
|
1050
|
-
'</button>' +
|
|
1051
1052
|
'<div id="sessions-drawer-backdrop" class="drawer-backdrop' + drawerClass + '"></div>' +
|
|
1052
1053
|
'<div class="main-layout' + (state.sessionsDrawerOpen ? ' sidebar-open' : '') + '">' +
|
|
1053
1054
|
'<aside id="sessions-drawer" class="sidebar' + drawerClass + '">' +
|
|
@@ -1089,10 +1090,13 @@
|
|
|
1089
1090
|
'</div>' +
|
|
1090
1091
|
'</aside>' +
|
|
1091
1092
|
'<main class="main-content">' +
|
|
1092
|
-
'<
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1093
|
+
'<div class="main-header-row">' +
|
|
1094
|
+
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="切换会话侧栏" type="button">' +
|
|
1095
|
+
'<span class="hamburger-icon">' +
|
|
1096
|
+
'<span></span><span></span><span></span>' +
|
|
1097
|
+
'</span>' +
|
|
1098
|
+
'</button>' +
|
|
1099
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
1096
1100
|
'</div>' +
|
|
1097
1101
|
// File panel backdrop (mobile)
|
|
1098
1102
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
@@ -1239,7 +1243,29 @@
|
|
|
1239
1243
|
'</section>' +
|
|
1240
1244
|
'</main>' +
|
|
1241
1245
|
'</div>' +
|
|
1242
|
-
'</div>' + renderSessionModal() + renderSettingsModal();
|
|
1246
|
+
'</div>' + renderSessionModal() + renderWorktreeMergeModal() + renderSettingsModal();
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
function renderWorktreeMergeModal() {
|
|
1250
|
+
return '<section id="worktree-merge-modal" class="modal-backdrop hidden">' +
|
|
1251
|
+
'<div class="modal worktree-merge-modal">' +
|
|
1252
|
+
'<div class="modal-header">' +
|
|
1253
|
+
'<div>' +
|
|
1254
|
+
'<h2 class="modal-title">合并 Worktree</h2>' +
|
|
1255
|
+
'<p class="modal-subtitle">检查当前任务分支并快捷合并到主分支。</p>' +
|
|
1256
|
+
'</div>' +
|
|
1257
|
+
'<button id="close-worktree-merge-button" class="btn btn-ghost btn-icon">×</button>' +
|
|
1258
|
+
'</div>' +
|
|
1259
|
+
'<div class="modal-body">' +
|
|
1260
|
+
'<div id="worktree-merge-content" class="worktree-merge-content"></div>' +
|
|
1261
|
+
'<p id="worktree-merge-error" class="error-message hidden"></p>' +
|
|
1262
|
+
'<div class="worktree-merge-actions">' +
|
|
1263
|
+
'<button id="worktree-merge-cancel-button" class="btn btn-secondary">取消</button>' +
|
|
1264
|
+
'<button id="worktree-merge-confirm-button" class="btn btn-primary">确认合并并清理</button>' +
|
|
1265
|
+
'</div>' +
|
|
1266
|
+
'</div>' +
|
|
1267
|
+
'</div>' +
|
|
1268
|
+
'</section>';
|
|
1243
1269
|
}
|
|
1244
1270
|
|
|
1245
1271
|
function renderSettingsModal() {
|
|
@@ -1252,10 +1278,11 @@
|
|
|
1252
1278
|
'<div class="modal-body">' +
|
|
1253
1279
|
// Tabs
|
|
1254
1280
|
'<div class="settings-tabs">' +
|
|
1255
|
-
'<button class="settings-tab active" data-tab="about"
|
|
1256
|
-
'<button class="settings-tab" data-tab="general"
|
|
1257
|
-
'<button class="settings-tab" data-tab="
|
|
1258
|
-
'<button class="settings-tab" data-tab="
|
|
1281
|
+
'<button class="settings-tab active" data-tab="about">\u5173\u4e8e</button>' +
|
|
1282
|
+
'<button class="settings-tab" data-tab="general">\u57fa\u672c\u914d\u7f6e</button>' +
|
|
1283
|
+
'<button class="settings-tab" data-tab="notifications">\u901a\u77e5</button>' +
|
|
1284
|
+
'<button class="settings-tab" data-tab="security">\u5b89\u5168</button>' +
|
|
1285
|
+
'<button class="settings-tab" data-tab="presets">\u547d\u4ee4\u9884\u8bbe</button>' +
|
|
1259
1286
|
'</div>' +
|
|
1260
1287
|
|
|
1261
1288
|
// About tab
|
|
@@ -1272,19 +1299,36 @@
|
|
|
1272
1299
|
'<span class="settings-value" id="settings-latest-version">-</span>' +
|
|
1273
1300
|
'</div>' +
|
|
1274
1301
|
'<div class="settings-update-actions">' +
|
|
1275
|
-
'<button id="check-update-button" class="btn btn-ghost btn-sm"
|
|
1276
|
-
'<button id="do-update-button" class="btn btn-primary btn-sm hidden"
|
|
1302
|
+
'<button id="check-update-button" class="btn btn-ghost btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
|
|
1303
|
+
'<button id="do-update-button" class="btn btn-primary btn-sm hidden">\u66f4\u65b0\u5230\u6700\u65b0\u7248</button>' +
|
|
1304
|
+
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1277
1305
|
'</div>' +
|
|
1278
1306
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1279
1307
|
'</div>' +
|
|
1280
|
-
|
|
1281
|
-
|
|
1308
|
+
'</div>' +
|
|
1309
|
+
|
|
1310
|
+
// Notifications tab
|
|
1311
|
+
'<div class="settings-panel" id="settings-tab-notifications">' +
|
|
1312
|
+
'<div class="settings-section-title">\u901a\u77e5\u504f\u597d</div>' +
|
|
1313
|
+
'<div class="field field-inline">' +
|
|
1314
|
+
'<input id="cfg-notif-sound" type="checkbox" class="field-checkbox" />' +
|
|
1315
|
+
'<label class="field-label" for="cfg-notif-sound">\u64ad\u653e\u63d0\u793a\u97f3</label>' +
|
|
1316
|
+
'</div>' +
|
|
1317
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">\u91cd\u8981\u901a\u77e5\uff08\u7248\u672c\u66f4\u65b0\u3001\u6743\u9650\u7b49\u5f85\u7b49\uff09\u65f6\u64ad\u653e\u67d4\u548c\u7684\u63d0\u793a\u97f3</p>' +
|
|
1318
|
+
'<div class="field field-inline">' +
|
|
1319
|
+
'<input id="cfg-notif-bubble" type="checkbox" class="field-checkbox" />' +
|
|
1320
|
+
'<label class="field-label" for="cfg-notif-bubble">\u5e94\u7528\u5185\u901a\u77e5\u6c14\u6ce1</label>' +
|
|
1321
|
+
'</div>' +
|
|
1322
|
+
'<p class="hint" style="margin-top:0;margin-bottom:10px">\u5728\u9875\u9762\u9876\u90e8\u5f39\u51fa\u6d6e\u52a8\u901a\u77e5\u6c14\u6ce1</p>' +
|
|
1323
|
+
'<div class="settings-notification-section" style="margin-top:6px">' +
|
|
1324
|
+
'<div class="settings-section-title">\u6d4f\u89c8\u5668\u901a\u77e5</div>' +
|
|
1282
1325
|
'<div class="settings-about-row">' +
|
|
1283
|
-
'<span class="settings-label">\
|
|
1326
|
+
'<span class="settings-label">\u6388\u6743\u72b6\u6001</span>' +
|
|
1284
1327
|
'<span class="settings-value" id="notification-permission-status">-</span>' +
|
|
1285
1328
|
'</div>' +
|
|
1286
1329
|
'<div class="settings-update-actions">' +
|
|
1287
1330
|
'<button id="notification-request-btn" class="btn btn-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
|
|
1331
|
+
'<button id="notification-reset-btn" class="btn btn-ghost btn-sm hidden">\u91cd\u65b0\u6388\u6743</button>' +
|
|
1288
1332
|
'<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
|
|
1289
1333
|
'</div>' +
|
|
1290
1334
|
'<p id="notification-test-message" class="hint hidden"></p>' +
|
|
@@ -2296,9 +2340,18 @@
|
|
|
2296
2340
|
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
2297
2341
|
}
|
|
2298
2342
|
|
|
2343
|
+
var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
2344
|
+
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
2345
|
+
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
2346
|
+
var mergeTitle = needsCleanup ? "重试清理 worktree" : "合并到主分支";
|
|
2347
|
+
var mergeButton = canOpenMerge && session.worktreeMergeStatus !== "merged"
|
|
2348
|
+
? '<button class="session-action-btn merge-btn" data-action="worktree-merge" data-session-id="' + session.id + '" type="button" aria-label="' + escapeHtml(mergeTitle) + '" title="' + escapeHtml(mergeTitle) + '"' + (mergeDisabled ? ' disabled' : '') + '><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 7h10"/><path d="M7 12h10"/><path d="M7 17h10"/><path d="M5 7l-2 2 2 2"/><path d="M19 15l2 2-2 2"/></svg></button>'
|
|
2349
|
+
: needsCleanup
|
|
2350
|
+
? '<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>'
|
|
2351
|
+
: "";
|
|
2299
2352
|
var deleteButton = state.sessionsManageMode ? '' : '<button class="session-action-btn delete-btn" data-action="delete-session" data-session-id="' + session.id + '" type="button" aria-label="删除会话" title="删除此会话"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></button>';
|
|
2300
2353
|
var modeBadge = renderSessionKindBadge(session);
|
|
2301
|
-
var actionsHtml = '<span class="session-actions">' + resumeButton + deleteButton + '</span>';
|
|
2354
|
+
var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
|
|
2302
2355
|
|
|
2303
2356
|
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
2304
2357
|
'<div class="session-item-content">' +
|
|
@@ -2322,12 +2375,35 @@
|
|
|
2322
2375
|
'</div>';
|
|
2323
2376
|
}
|
|
2324
2377
|
|
|
2378
|
+
function getWorktreeMergeStatusLabel(session) {
|
|
2379
|
+
if (!session || !session.worktreeMergeStatus) return "";
|
|
2380
|
+
var labels = {
|
|
2381
|
+
ready: "可合并",
|
|
2382
|
+
checking: "检查中",
|
|
2383
|
+
merging: "合并中",
|
|
2384
|
+
merged: session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false ? "已合并待清理" : "已合并",
|
|
2385
|
+
failed: "合并失败"
|
|
2386
|
+
};
|
|
2387
|
+
return labels[session.worktreeMergeStatus] || "";
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function renderWorktreeMergeBadge(session) {
|
|
2391
|
+
var label = getWorktreeMergeStatusLabel(session);
|
|
2392
|
+
if (!label) return "";
|
|
2393
|
+
return '<span class="session-kind-badge worktree-merge ' + escapeHtml(session.worktreeMergeStatus || "") + '">' + escapeHtml(label) + '</span>';
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2325
2396
|
function renderWorktreeBadge(session) {
|
|
2326
2397
|
if (!session || !session.worktreeEnabled) return "";
|
|
2327
|
-
var
|
|
2328
|
-
|
|
2329
|
-
: '
|
|
2330
|
-
|
|
2398
|
+
var titleParts = [];
|
|
2399
|
+
if (session.worktree && session.worktree.branch) {
|
|
2400
|
+
titleParts.push('Worktree: ' + session.worktree.branch);
|
|
2401
|
+
}
|
|
2402
|
+
if (session.worktree && session.worktree.path) {
|
|
2403
|
+
titleParts.push('Path: ' + session.worktree.path);
|
|
2404
|
+
}
|
|
2405
|
+
var title = titleParts.length > 0 ? ' title="' + escapeHtml(titleParts.join('\n')) + '"' : '';
|
|
2406
|
+
return '<span class="session-kind-badge worktree"' + title + '>Worktree</span>' + renderWorktreeMergeBadge(session);
|
|
2331
2407
|
}
|
|
2332
2408
|
|
|
2333
2409
|
function renderSessionKindBadge(session) {
|
|
@@ -2531,33 +2607,85 @@
|
|
|
2531
2607
|
}
|
|
2532
2608
|
}
|
|
2533
2609
|
}
|
|
2534
|
-
//
|
|
2535
|
-
window.
|
|
2536
|
-
var
|
|
2537
|
-
if (
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2610
|
+
// ── AskUserQuestion handlers: select → render → submit ──
|
|
2611
|
+
window.__askSelect = function(toolUseId, qIdx, optIdx, isMulti) {
|
|
2612
|
+
var sel = state.askUserSelections[toolUseId];
|
|
2613
|
+
if (!sel) {
|
|
2614
|
+
sel = { submitted: false };
|
|
2615
|
+
state.askUserSelections[toolUseId] = sel;
|
|
2616
|
+
}
|
|
2617
|
+
if (sel.submitted) return;
|
|
2618
|
+
var current = sel[qIdx] || [];
|
|
2619
|
+
if (isMulti) {
|
|
2620
|
+
var pos = current.indexOf(optIdx);
|
|
2621
|
+
if (pos === -1) { current.push(optIdx); } else { current.splice(pos, 1); }
|
|
2622
|
+
} else {
|
|
2623
|
+
current = current[0] === optIdx ? [] : [optIdx];
|
|
2624
|
+
}
|
|
2625
|
+
sel[qIdx] = current;
|
|
2626
|
+
window.__askRender(toolUseId);
|
|
2627
|
+
};
|
|
2628
|
+
|
|
2629
|
+
window.__askRender = function(toolUseId) {
|
|
2630
|
+
var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
|
|
2631
|
+
if (!card) return;
|
|
2632
|
+
var sel = state.askUserSelections[toolUseId] || {};
|
|
2633
|
+
// Update option selected states
|
|
2634
|
+
card.querySelectorAll(".ask-user-option").forEach(function(btn) {
|
|
2635
|
+
var qIdx = parseInt(btn.dataset.questionIndex, 10);
|
|
2636
|
+
var oIdx = parseInt(btn.dataset.optionIndex, 10);
|
|
2637
|
+
var chosen = (sel[qIdx] || []).indexOf(oIdx) !== -1;
|
|
2638
|
+
btn.classList.toggle("selected", chosen);
|
|
2639
|
+
});
|
|
2640
|
+
// Update submit button: enabled only when every question has at least one selection
|
|
2641
|
+
var submitBtn = card.querySelector(".ask-user-submit");
|
|
2642
|
+
if (submitBtn) {
|
|
2643
|
+
var groups = card.querySelectorAll(".ask-user-question-group");
|
|
2644
|
+
var allAnswered = true;
|
|
2645
|
+
groups.forEach(function(g, i) {
|
|
2646
|
+
if (!sel[i] || sel[i].length === 0) allAnswered = false;
|
|
2558
2647
|
});
|
|
2648
|
+
submitBtn.disabled = !allAnswered || !!sel.submitted;
|
|
2649
|
+
if (sel.submitted) {
|
|
2650
|
+
submitBtn.textContent = "已提交...";
|
|
2651
|
+
submitBtn.classList.add("ask-user-submitted");
|
|
2652
|
+
}
|
|
2559
2653
|
}
|
|
2560
2654
|
};
|
|
2655
|
+
|
|
2656
|
+
window.__askSubmit = function(toolUseId) {
|
|
2657
|
+
var sel = state.askUserSelections[toolUseId];
|
|
2658
|
+
if (!sel || sel.submitted || !state.selectedId) return;
|
|
2659
|
+
var card = document.querySelector('[data-tool-use-id="' + toolUseId + '"]');
|
|
2660
|
+
if (!card) return;
|
|
2661
|
+
var groups = card.querySelectorAll(".ask-user-question-group");
|
|
2662
|
+
var lines = [];
|
|
2663
|
+
var allAnswered = true;
|
|
2664
|
+
groups.forEach(function(group, qIdx) {
|
|
2665
|
+
var selected = sel[qIdx] || [];
|
|
2666
|
+
if (selected.length === 0) { allAnswered = false; return; }
|
|
2667
|
+
var labels = [];
|
|
2668
|
+
selected.forEach(function(optIdx) {
|
|
2669
|
+
var btn = group.querySelector('[data-option-index="' + optIdx + '"]');
|
|
2670
|
+
if (btn) labels.push(btn.dataset.optionLabel);
|
|
2671
|
+
});
|
|
2672
|
+
lines.push(labels.join(", "));
|
|
2673
|
+
});
|
|
2674
|
+
if (!allAnswered) return;
|
|
2675
|
+
sel.submitted = true;
|
|
2676
|
+
window.__askRender(toolUseId);
|
|
2677
|
+
var answerText = lines.join("\n");
|
|
2678
|
+
fetch("/api/sessions/" + state.selectedId + "/input", {
|
|
2679
|
+
method: "POST",
|
|
2680
|
+
headers: { "Content-Type": "application/json" },
|
|
2681
|
+
credentials: "same-origin",
|
|
2682
|
+
body: JSON.stringify({ input: answerText + "\n", view: state.currentView })
|
|
2683
|
+
}).catch(function(err) {
|
|
2684
|
+
console.error("[wand] Error sending answer:", err);
|
|
2685
|
+
sel.submitted = false;
|
|
2686
|
+
window.__askRender(toolUseId);
|
|
2687
|
+
});
|
|
2688
|
+
};
|
|
2561
2689
|
function attachEventListeners() {
|
|
2562
2690
|
|
|
2563
2691
|
var loginButton = document.getElementById("login-button");
|
|
@@ -2738,13 +2866,36 @@
|
|
|
2738
2866
|
if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
|
|
2739
2867
|
var doUpdateBtn = document.getElementById("do-update-button");
|
|
2740
2868
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
2741
|
-
|
|
2869
|
+
var doRestartBtn = document.getElementById("do-restart-button");
|
|
2870
|
+
if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
|
|
2871
|
+
// Notification preferences
|
|
2872
|
+
var notifSoundEl = document.getElementById("cfg-notif-sound");
|
|
2873
|
+
if (notifSoundEl) {
|
|
2874
|
+
notifSoundEl.checked = state.notifSound;
|
|
2875
|
+
notifSoundEl.addEventListener("change", function() {
|
|
2876
|
+
state.notifSound = notifSoundEl.checked;
|
|
2877
|
+
try { localStorage.setItem("wand-notif-sound", String(state.notifSound)); } catch (e) {}
|
|
2878
|
+
// Preview sound when toggling on
|
|
2879
|
+
if (state.notifSound) _doPlaySound();
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
var notifBubbleEl = document.getElementById("cfg-notif-bubble");
|
|
2883
|
+
if (notifBubbleEl) {
|
|
2884
|
+
notifBubbleEl.checked = state.notifBubble;
|
|
2885
|
+
notifBubbleEl.addEventListener("change", function() {
|
|
2886
|
+
state.notifBubble = notifBubbleEl.checked;
|
|
2887
|
+
try { localStorage.setItem("wand-notif-bubble", String(state.notifBubble)); } catch (e) {}
|
|
2888
|
+
});
|
|
2889
|
+
}
|
|
2890
|
+
// Browser notification section
|
|
2742
2891
|
var notifRequestBtn = document.getElementById("notification-request-btn");
|
|
2743
2892
|
if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
|
|
2744
2893
|
if (typeof Notification !== "undefined") {
|
|
2745
2894
|
Notification.requestPermission().then(function() { updateNotificationStatus(); });
|
|
2746
2895
|
}
|
|
2747
2896
|
});
|
|
2897
|
+
var notifResetBtn = document.getElementById("notification-reset-btn");
|
|
2898
|
+
if (notifResetBtn) notifResetBtn.addEventListener("click", resetNotificationPermission);
|
|
2748
2899
|
var notifTestBtn = document.getElementById("notification-test-btn");
|
|
2749
2900
|
if (notifTestBtn) notifTestBtn.addEventListener("click", testNotification);
|
|
2750
2901
|
updateNotificationStatus();
|
|
@@ -2754,6 +2905,12 @@
|
|
|
2754
2905
|
if (drawerNewSessBtn) drawerNewSessBtn.addEventListener("click", openSessionModal);
|
|
2755
2906
|
var closeModalBtn = document.getElementById("close-modal-button");
|
|
2756
2907
|
if (closeModalBtn) closeModalBtn.addEventListener("click", closeSessionModal);
|
|
2908
|
+
var closeWorktreeMergeBtn = document.getElementById("close-worktree-merge-button");
|
|
2909
|
+
if (closeWorktreeMergeBtn) closeWorktreeMergeBtn.addEventListener("click", closeWorktreeMergeModal);
|
|
2910
|
+
var worktreeMergeCancelBtn = document.getElementById("worktree-merge-cancel-button");
|
|
2911
|
+
if (worktreeMergeCancelBtn) worktreeMergeCancelBtn.addEventListener("click", closeWorktreeMergeModal);
|
|
2912
|
+
var worktreeMergeConfirmBtn = document.getElementById("worktree-merge-confirm-button");
|
|
2913
|
+
if (worktreeMergeConfirmBtn) worktreeMergeConfirmBtn.addEventListener("click", confirmWorktreeMerge);
|
|
2757
2914
|
var runBtn = document.getElementById("run-button");
|
|
2758
2915
|
if (runBtn) runBtn.addEventListener("click", runCommand);
|
|
2759
2916
|
var approvePermissionBtn = document.getElementById("approve-permission-btn");
|
|
@@ -2778,6 +2935,7 @@
|
|
|
2778
2935
|
var sessionModal = document.getElementById("session-modal");
|
|
2779
2936
|
if (sessionModal) sessionModal.addEventListener("click", function(e) {
|
|
2780
2937
|
if (e.target.id === "session-modal") closeSessionModal();
|
|
2938
|
+
if (e.target.id === "worktree-merge-modal") closeWorktreeMergeModal();
|
|
2781
2939
|
});
|
|
2782
2940
|
|
|
2783
2941
|
var inputBox = document.getElementById("input-box");
|
|
@@ -2800,11 +2958,6 @@
|
|
|
2800
2958
|
inputBox.addEventListener("blur", handleInputBoxBlur);
|
|
2801
2959
|
}
|
|
2802
2960
|
|
|
2803
|
-
// View toggle handlers
|
|
2804
|
-
var viewTermBtn = document.getElementById("view-terminal-btn");
|
|
2805
|
-
if (viewTermBtn) viewTermBtn.addEventListener("click", function() { setView("terminal"); });
|
|
2806
|
-
var viewChatBtn = document.getElementById("view-chat-btn");
|
|
2807
|
-
if (viewChatBtn) viewChatBtn.addEventListener("click", function() { setView("chat"); });
|
|
2808
2961
|
// Terminal interactive toggle (both topbar and terminal-header)
|
|
2809
2962
|
var terminalInteractiveToggles = ["terminal-interactive-toggle-top"];
|
|
2810
2963
|
terminalInteractiveToggles.forEach(function(id) {
|
|
@@ -3371,6 +3524,10 @@
|
|
|
3371
3524
|
handleResumeAction(actionButton);
|
|
3372
3525
|
} else if (actionButton.dataset.action === "resume-history" && actionButton.dataset.claudeSessionId) {
|
|
3373
3526
|
handleResumeHistoryAction(actionButton);
|
|
3527
|
+
} else if (actionButton.dataset.action === "worktree-merge" && actionButton.dataset.sessionId) {
|
|
3528
|
+
openWorktreeMergeModal(actionButton.dataset.sessionId);
|
|
3529
|
+
} else if (actionButton.dataset.action === "worktree-cleanup" && actionButton.dataset.sessionId) {
|
|
3530
|
+
retryWorktreeCleanup(actionButton.dataset.sessionId);
|
|
3374
3531
|
}
|
|
3375
3532
|
return;
|
|
3376
3533
|
}
|
|
@@ -3489,6 +3646,8 @@
|
|
|
3489
3646
|
if (button) {
|
|
3490
3647
|
button.classList.toggle("visible", shouldShow);
|
|
3491
3648
|
}
|
|
3649
|
+
var termContainer = document.getElementById("output");
|
|
3650
|
+
if (termContainer) termContainer.classList.toggle("has-jump-btn", shouldShow);
|
|
3492
3651
|
}
|
|
3493
3652
|
|
|
3494
3653
|
function isTerminalNearBottom() {
|
|
@@ -4219,9 +4378,6 @@
|
|
|
4219
4378
|
|
|
4220
4379
|
function applyCurrentView() {
|
|
4221
4380
|
var hasSession = !!state.selectedId;
|
|
4222
|
-
var terminalBtn = document.getElementById("view-terminal-btn");
|
|
4223
|
-
var chatBtn = document.getElementById("view-chat-btn");
|
|
4224
|
-
var toggleBar = document.getElementById("view-toggle-bar");
|
|
4225
4381
|
var terminalContainer = document.getElementById("output");
|
|
4226
4382
|
var chatContainer = document.getElementById("chat-output");
|
|
4227
4383
|
var selectedSession = state.sessions.find(function(s) { return s.id === state.selectedId; });
|
|
@@ -4234,17 +4390,6 @@
|
|
|
4234
4390
|
state.currentView = "terminal";
|
|
4235
4391
|
}
|
|
4236
4392
|
|
|
4237
|
-
if (toggleBar) {
|
|
4238
|
-
toggleBar.classList.toggle("hidden", !hasSession);
|
|
4239
|
-
}
|
|
4240
|
-
if (terminalBtn) {
|
|
4241
|
-
terminalBtn.classList.toggle("hidden", structured || !hasSession);
|
|
4242
|
-
terminalBtn.classList.toggle("active", showTerminal);
|
|
4243
|
-
}
|
|
4244
|
-
if (chatBtn) {
|
|
4245
|
-
chatBtn.classList.toggle("hidden", !hasSession);
|
|
4246
|
-
chatBtn.classList.toggle("active", showChat);
|
|
4247
|
-
}
|
|
4248
4393
|
if (terminalContainer) {
|
|
4249
4394
|
terminalContainer.classList.toggle("active", showTerminal);
|
|
4250
4395
|
terminalContainer.classList.toggle("hidden", !showTerminal);
|
|
@@ -4795,6 +4940,162 @@
|
|
|
4795
4940
|
document.addEventListener("keydown", focusTrapHandler);
|
|
4796
4941
|
}
|
|
4797
4942
|
|
|
4943
|
+
function getActiveWorktreeMergeSession() {
|
|
4944
|
+
if (!state.activeWorktreeMergeSessionId) return null;
|
|
4945
|
+
return state.sessions.find(function(session) { return session.id === state.activeWorktreeMergeSessionId; }) || null;
|
|
4946
|
+
}
|
|
4947
|
+
|
|
4948
|
+
function renderWorktreeMergeContent() {
|
|
4949
|
+
var container = document.getElementById("worktree-merge-content");
|
|
4950
|
+
var confirmBtn = document.getElementById("worktree-merge-confirm-button");
|
|
4951
|
+
var errorEl = document.getElementById("worktree-merge-error");
|
|
4952
|
+
var session = getActiveWorktreeMergeSession();
|
|
4953
|
+
var result = state.worktreeMergeCheckResult;
|
|
4954
|
+
if (!container || !confirmBtn) return;
|
|
4955
|
+
if (!session || !session.worktree) {
|
|
4956
|
+
container.innerHTML = '<p class="field-hint">未找到可合并的 worktree 会话。</p>';
|
|
4957
|
+
confirmBtn.disabled = true;
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4960
|
+
if (errorEl) {
|
|
4961
|
+
if (state.worktreeMergeError) {
|
|
4962
|
+
showError(errorEl, state.worktreeMergeError);
|
|
4963
|
+
} else {
|
|
4964
|
+
hideError(errorEl);
|
|
4965
|
+
}
|
|
4966
|
+
}
|
|
4967
|
+
var rows = [
|
|
4968
|
+
'<div class="worktree-merge-row"><span>来源分支</span><strong>' + escapeHtml(session.worktree.branch || "-") + '</strong></div>',
|
|
4969
|
+
'<div class="worktree-merge-row"><span>工作目录</span><strong>' + escapeHtml(session.worktree.path || "-") + '</strong></div>'
|
|
4970
|
+
];
|
|
4971
|
+
if (result) {
|
|
4972
|
+
rows.push('<div class="worktree-merge-row"><span>目标分支</span><strong>' + escapeHtml(result.targetBranch || "-") + '</strong></div>');
|
|
4973
|
+
rows.push('<div class="worktree-merge-row"><span>待合并提交</span><strong>' + escapeHtml(String(result.aheadCount || 0)) + '</strong></div>');
|
|
4974
|
+
rows.push('<div class="worktree-merge-row"><span>未提交改动</span><strong>' + escapeHtml(result.hasUncommittedChanges ? "有" : "无") + '</strong></div>');
|
|
4975
|
+
rows.push('<div class="worktree-merge-row"><span>冲突风险</span><strong>' + escapeHtml(result.hasConflicts ? "有" : "无") + '</strong></div>');
|
|
4976
|
+
if (result.reason) {
|
|
4977
|
+
rows.push('<p class="field-hint">' + escapeHtml(result.reason) + '</p>');
|
|
4978
|
+
}
|
|
4979
|
+
} else if (state.worktreeMergeLoading) {
|
|
4980
|
+
rows.push('<p class="field-hint">正在检查 worktree 合并状态…</p>');
|
|
4981
|
+
}
|
|
4982
|
+
container.innerHTML = rows.join("");
|
|
4983
|
+
confirmBtn.disabled = state.worktreeMergeLoading || state.worktreeMergeSubmitting || !result || result.ok !== true;
|
|
4984
|
+
confirmBtn.textContent = state.worktreeMergeSubmitting ? "合并中..." : "确认合并并清理";
|
|
4985
|
+
}
|
|
4986
|
+
|
|
4987
|
+
function openWorktreeMergeModal(sessionId) {
|
|
4988
|
+
state.activeWorktreeMergeSessionId = sessionId;
|
|
4989
|
+
state.worktreeMergeCheckResult = null;
|
|
4990
|
+
state.worktreeMergeLoading = true;
|
|
4991
|
+
state.worktreeMergeSubmitting = false;
|
|
4992
|
+
state.worktreeMergeError = "";
|
|
4993
|
+
closeSessionModal();
|
|
4994
|
+
closeSettingsModal();
|
|
4995
|
+
var modal = document.getElementById("worktree-merge-modal");
|
|
4996
|
+
if (modal) {
|
|
4997
|
+
modal.classList.remove("hidden");
|
|
4998
|
+
lastFocusedElement = document.activeElement;
|
|
4999
|
+
setupFocusTrap(modal);
|
|
5000
|
+
}
|
|
5001
|
+
renderWorktreeMergeContent();
|
|
5002
|
+
fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/merge/check", {
|
|
5003
|
+
method: "POST",
|
|
5004
|
+
credentials: "same-origin"
|
|
5005
|
+
})
|
|
5006
|
+
.then(function(res) { return res.json(); })
|
|
5007
|
+
.then(function(data) {
|
|
5008
|
+
if (data && data.error) {
|
|
5009
|
+
throw new Error(data.error);
|
|
5010
|
+
}
|
|
5011
|
+
if (data && data.session) {
|
|
5012
|
+
updateSessionSnapshot(data.session);
|
|
5013
|
+
}
|
|
5014
|
+
state.worktreeMergeCheckResult = data.result || null;
|
|
5015
|
+
state.worktreeMergeError = "";
|
|
5016
|
+
})
|
|
5017
|
+
.catch(function(error) {
|
|
5018
|
+
state.worktreeMergeError = (error && error.message) || "无法检查 worktree 合并状态。";
|
|
5019
|
+
})
|
|
5020
|
+
.finally(function() {
|
|
5021
|
+
state.worktreeMergeLoading = false;
|
|
5022
|
+
renderWorktreeMergeContent();
|
|
5023
|
+
});
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
function closeWorktreeMergeModal() {
|
|
5027
|
+
var modal = document.getElementById("worktree-merge-modal");
|
|
5028
|
+
state.activeWorktreeMergeSessionId = null;
|
|
5029
|
+
state.worktreeMergeCheckResult = null;
|
|
5030
|
+
state.worktreeMergeLoading = false;
|
|
5031
|
+
state.worktreeMergeSubmitting = false;
|
|
5032
|
+
state.worktreeMergeError = "";
|
|
5033
|
+
if (modal) {
|
|
5034
|
+
modal.classList.add("hidden");
|
|
5035
|
+
}
|
|
5036
|
+
if (focusTrapHandler) {
|
|
5037
|
+
document.removeEventListener("keydown", focusTrapHandler);
|
|
5038
|
+
focusTrapHandler = null;
|
|
5039
|
+
}
|
|
5040
|
+
if (lastFocusedElement && typeof lastFocusedElement.focus === "function") {
|
|
5041
|
+
lastFocusedElement.focus();
|
|
5042
|
+
}
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
function confirmWorktreeMerge() {
|
|
5046
|
+
if (!state.activeWorktreeMergeSessionId || state.worktreeMergeSubmitting) return;
|
|
5047
|
+
state.worktreeMergeSubmitting = true;
|
|
5048
|
+
state.worktreeMergeError = "";
|
|
5049
|
+
renderWorktreeMergeContent();
|
|
5050
|
+
fetch("/api/sessions/" + encodeURIComponent(state.activeWorktreeMergeSessionId) + "/worktree/merge", {
|
|
5051
|
+
method: "POST",
|
|
5052
|
+
credentials: "same-origin",
|
|
5053
|
+
headers: { "Content-Type": "application/json" },
|
|
5054
|
+
body: JSON.stringify({})
|
|
5055
|
+
})
|
|
5056
|
+
.then(function(res) { return res.json(); })
|
|
5057
|
+
.then(function(data) {
|
|
5058
|
+
if (data && data.error) {
|
|
5059
|
+
throw new Error(data.error);
|
|
5060
|
+
}
|
|
5061
|
+
if (data && data.session) {
|
|
5062
|
+
updateSessionSnapshot(data.session);
|
|
5063
|
+
}
|
|
5064
|
+
showToast("已合并到 " + escapeHtml((data.result && data.result.targetBranch) || "主分支") + ((data.result && data.result.cleanupDone === false) ? ",但工作树待清理。" : "。"), "info");
|
|
5065
|
+
closeWorktreeMergeModal();
|
|
5066
|
+
return refreshAll();
|
|
5067
|
+
})
|
|
5068
|
+
.catch(function(error) {
|
|
5069
|
+
state.worktreeMergeError = (error && error.message) || "无法合并 worktree。";
|
|
5070
|
+
renderWorktreeMergeContent();
|
|
5071
|
+
})
|
|
5072
|
+
.finally(function() {
|
|
5073
|
+
state.worktreeMergeSubmitting = false;
|
|
5074
|
+
renderWorktreeMergeContent();
|
|
5075
|
+
});
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
function retryWorktreeCleanup(sessionId) {
|
|
5079
|
+
fetch("/api/sessions/" + encodeURIComponent(sessionId) + "/worktree/cleanup", {
|
|
5080
|
+
method: "POST",
|
|
5081
|
+
credentials: "same-origin"
|
|
5082
|
+
})
|
|
5083
|
+
.then(function(res) { return res.json(); })
|
|
5084
|
+
.then(function(data) {
|
|
5085
|
+
if (data && data.error) {
|
|
5086
|
+
throw new Error(data.error);
|
|
5087
|
+
}
|
|
5088
|
+
if (data && data.session) {
|
|
5089
|
+
updateSessionSnapshot(data.session);
|
|
5090
|
+
}
|
|
5091
|
+
showToast("已完成 worktree 清理。", "info");
|
|
5092
|
+
return refreshAll();
|
|
5093
|
+
})
|
|
5094
|
+
.catch(function(error) {
|
|
5095
|
+
showToast((error && error.message) || "无法清理 worktree。", "error");
|
|
5096
|
+
});
|
|
5097
|
+
}
|
|
5098
|
+
|
|
4798
5099
|
function openSettingsModal() {
|
|
4799
5100
|
// Close session modal first if open (mutual exclusion)
|
|
4800
5101
|
closeSessionModal();
|
|
@@ -4812,6 +5113,12 @@
|
|
|
4812
5113
|
switchSettingsTab("about");
|
|
4813
5114
|
// Load settings data
|
|
4814
5115
|
loadSettingsData();
|
|
5116
|
+
// Sync notification preferences
|
|
5117
|
+
var soundEl = document.getElementById("cfg-notif-sound");
|
|
5118
|
+
var bubbleEl = document.getElementById("cfg-notif-bubble");
|
|
5119
|
+
if (soundEl) soundEl.checked = state.notifSound;
|
|
5120
|
+
if (bubbleEl) bubbleEl.checked = state.notifBubble;
|
|
5121
|
+
updateNotificationStatus();
|
|
4815
5122
|
}
|
|
4816
5123
|
}
|
|
4817
5124
|
|
|
@@ -4919,6 +5226,16 @@
|
|
|
4919
5226
|
repoEl.innerHTML = '<a href="' + escapeHtml(data.repoUrl) + '" target="_blank" rel="noopener">' + escapeHtml(data.repoUrl) + '</a>';
|
|
4920
5227
|
}
|
|
4921
5228
|
|
|
5229
|
+
// Prefill update info if available
|
|
5230
|
+
var latestEl = document.getElementById("settings-latest-version");
|
|
5231
|
+
var updateBtn = document.getElementById("do-update-button");
|
|
5232
|
+
if (data.latestVersion && latestEl) {
|
|
5233
|
+
latestEl.textContent = data.latestVersion;
|
|
5234
|
+
if (data.updateAvailable && updateBtn) {
|
|
5235
|
+
updateBtn.classList.remove("hidden");
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
|
|
4922
5239
|
// Config fields
|
|
4923
5240
|
var cfg = data.config || {};
|
|
4924
5241
|
var hostEl = document.getElementById("cfg-host");
|
|
@@ -5111,7 +5428,7 @@
|
|
|
5111
5428
|
.then(function(res) { return res.json(); })
|
|
5112
5429
|
.then(function(data) {
|
|
5113
5430
|
if (msgEl) {
|
|
5114
|
-
msgEl.textContent = data.message || data.error || "
|
|
5431
|
+
msgEl.textContent = data.message || data.error || "\u66f4\u65b0\u5b8c\u6210\u3002";
|
|
5115
5432
|
msgEl.style.color = data.error ? "var(--error)" : "var(--success)";
|
|
5116
5433
|
msgEl.classList.remove("hidden");
|
|
5117
5434
|
}
|
|
@@ -5119,11 +5436,14 @@
|
|
|
5119
5436
|
updateBtn.disabled = false;
|
|
5120
5437
|
} else {
|
|
5121
5438
|
updateBtn.classList.add("hidden");
|
|
5439
|
+
// Show restart button
|
|
5440
|
+
var restartBtn = document.getElementById("do-restart-button");
|
|
5441
|
+
if (restartBtn) restartBtn.classList.remove("hidden");
|
|
5122
5442
|
}
|
|
5123
5443
|
})
|
|
5124
5444
|
.catch(function() {
|
|
5125
5445
|
if (msgEl) {
|
|
5126
|
-
msgEl.textContent = "
|
|
5446
|
+
msgEl.textContent = "\u66f4\u65b0\u5931\u8d25\u3002";
|
|
5127
5447
|
msgEl.style.color = "var(--error)";
|
|
5128
5448
|
msgEl.classList.remove("hidden");
|
|
5129
5449
|
}
|
|
@@ -5131,11 +5451,18 @@
|
|
|
5131
5451
|
});
|
|
5132
5452
|
}
|
|
5133
5453
|
|
|
5454
|
+
function performSettingsRestart() {
|
|
5455
|
+
var restartBtn = document.getElementById("do-restart-button");
|
|
5456
|
+
var msgEl = document.getElementById("update-message");
|
|
5457
|
+
performRestart(restartBtn, msgEl);
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5134
5460
|
// ── Notification Settings Helpers ──
|
|
5135
5461
|
|
|
5136
5462
|
function updateNotificationStatus() {
|
|
5137
5463
|
var statusEl = document.getElementById("notification-permission-status");
|
|
5138
5464
|
var requestBtn = document.getElementById("notification-request-btn");
|
|
5465
|
+
var resetBtn = document.getElementById("notification-reset-btn");
|
|
5139
5466
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
5140
5467
|
if (!statusEl) return;
|
|
5141
5468
|
|
|
@@ -5143,6 +5470,7 @@
|
|
|
5143
5470
|
statusEl.textContent = "\u4e0d\u652f\u6301";
|
|
5144
5471
|
statusEl.style.color = "var(--fg-muted)";
|
|
5145
5472
|
if (requestBtn) requestBtn.classList.add("hidden");
|
|
5473
|
+
if (resetBtn) resetBtn.classList.add("hidden");
|
|
5146
5474
|
return;
|
|
5147
5475
|
}
|
|
5148
5476
|
|
|
@@ -5151,45 +5479,86 @@
|
|
|
5151
5479
|
statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
|
|
5152
5480
|
statusEl.style.color = "var(--success)";
|
|
5153
5481
|
if (requestBtn) requestBtn.classList.add("hidden");
|
|
5482
|
+
if (resetBtn) resetBtn.classList.add("hidden");
|
|
5154
5483
|
} else if (perm === "denied") {
|
|
5155
5484
|
statusEl.textContent = "\u5df2\u62d2\u7edd";
|
|
5156
5485
|
statusEl.style.color = "var(--danger)";
|
|
5157
5486
|
if (requestBtn) requestBtn.classList.add("hidden");
|
|
5158
|
-
if (
|
|
5159
|
-
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";
|
|
5160
|
-
testMsgEl.style.color = "var(--fg-muted)";
|
|
5161
|
-
testMsgEl.classList.remove("hidden");
|
|
5162
|
-
}
|
|
5487
|
+
if (resetBtn) resetBtn.classList.remove("hidden");
|
|
5163
5488
|
} else {
|
|
5164
5489
|
statusEl.textContent = "\u672a\u6388\u6743";
|
|
5165
5490
|
statusEl.style.color = "var(--warning)";
|
|
5166
5491
|
if (requestBtn) requestBtn.classList.remove("hidden");
|
|
5492
|
+
if (resetBtn) resetBtn.classList.remove("hidden");
|
|
5167
5493
|
}
|
|
5168
5494
|
}
|
|
5169
5495
|
|
|
5496
|
+
function resetNotificationPermission() {
|
|
5497
|
+
var testMsgEl = document.getElementById("notification-test-message");
|
|
5498
|
+
if (typeof Notification === "undefined") return;
|
|
5499
|
+
|
|
5500
|
+
// Always call requestPermission — this triggers the browser's native
|
|
5501
|
+
// permission dialog when allowed. In "default" state it always works.
|
|
5502
|
+
// In "denied" state, some browsers (newer Chrome) re-prompt, others don't.
|
|
5503
|
+
Notification.requestPermission().then(function(result) {
|
|
5504
|
+
updateNotificationStatus();
|
|
5505
|
+
if (result === "granted") {
|
|
5506
|
+
if (testMsgEl) {
|
|
5507
|
+
testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
|
|
5508
|
+
testMsgEl.style.color = "var(--success)";
|
|
5509
|
+
testMsgEl.classList.remove("hidden");
|
|
5510
|
+
}
|
|
5511
|
+
} else if (result === "denied") {
|
|
5512
|
+
// Browser blocked re-prompting — show inline guide with site-settings shortcut
|
|
5513
|
+
if (testMsgEl) {
|
|
5514
|
+
var origin = location.origin;
|
|
5515
|
+
testMsgEl.innerHTML =
|
|
5516
|
+
"\u6d4f\u89c8\u5668\u5df2\u62e6\u622a\u6388\u6743\u5f39\u7a97\uff0c\u8bf7\u624b\u52a8\u91cd\u7f6e\uff1a<br>" +
|
|
5517
|
+
'<span style="display:inline-flex;align-items:center;gap:4px;margin:4px 0">' +
|
|
5518
|
+
"\u2460 \u70b9\u51fb\u5730\u5740\u680f\u5de6\u4fa7\u7684 " +
|
|
5519
|
+
'<span style="display:inline-flex;align-items:center;justify-content:center;' +
|
|
5520
|
+
"width:16px;height:16px;border-radius:50%;border:1px solid var(--border);" +
|
|
5521
|
+
'font-size:11px;vertical-align:middle">i</span>' +
|
|
5522
|
+
" \u6216\u9501\u56fe\u6807" +
|
|
5523
|
+
"</span><br>" +
|
|
5524
|
+
"\u2461 \u627e\u5230\u300c\u901a\u77e5\u300d\u2192 \u6539\u4e3a\u300c\u5141\u8bb8\u300d<br>" +
|
|
5525
|
+
"\u2462 \u5237\u65b0\u9875\u9762\u5373\u53ef";
|
|
5526
|
+
testMsgEl.style.color = "var(--fg-muted)";
|
|
5527
|
+
testMsgEl.classList.remove("hidden");
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
5532
|
+
|
|
5170
5533
|
function testNotification() {
|
|
5171
5534
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
5535
|
+
var results = [];
|
|
5536
|
+
|
|
5537
|
+
// 1. Test sound playback
|
|
5538
|
+
var soundOk = tryPlayNotificationSound();
|
|
5539
|
+
results.push(soundOk ? "\u2713 \u63d0\u793a\u97f3" : "\u2717 \u63d0\u793a\u97f3\uff08\u65e0\u6cd5\u64ad\u653e\uff09");
|
|
5172
5540
|
|
|
5173
|
-
//
|
|
5541
|
+
// 2. Test in-app bubble
|
|
5542
|
+
var bubbleEnabled = state.notifBubble;
|
|
5174
5543
|
showNotificationBubble({
|
|
5175
5544
|
title: "\u6d4b\u8bd5\u901a\u77e5",
|
|
5176
|
-
body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\
|
|
5545
|
+
body: "\u8fd9\u662f\u4e00\u6761\u6d4b\u8bd5\u901a\u77e5\u3002",
|
|
5177
5546
|
type: "info",
|
|
5178
5547
|
icon: "\u266a",
|
|
5179
5548
|
duration: 5000,
|
|
5549
|
+
playSound: false, // sound already played above
|
|
5180
5550
|
});
|
|
5551
|
+
results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
|
|
5181
5552
|
|
|
5182
|
-
// Test browser notification
|
|
5553
|
+
// 3. Test browser notification
|
|
5183
5554
|
if (typeof Notification === "undefined") {
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
testMsgEl.style.color = "var(--fg-muted)";
|
|
5187
|
-
testMsgEl.classList.remove("hidden");
|
|
5188
|
-
}
|
|
5555
|
+
results.push("\u2013 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
|
|
5556
|
+
showTestResults(testMsgEl, results);
|
|
5189
5557
|
return;
|
|
5190
5558
|
}
|
|
5191
5559
|
|
|
5192
|
-
|
|
5560
|
+
var perm = Notification.permission;
|
|
5561
|
+
if (perm === "granted") {
|
|
5193
5562
|
try {
|
|
5194
5563
|
var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
|
|
5195
5564
|
body: "\u6d4f\u89c8\u5668\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
|
|
@@ -5197,35 +5566,37 @@
|
|
|
5197
5566
|
tag: "wand-test",
|
|
5198
5567
|
});
|
|
5199
5568
|
setTimeout(function() { n.close(); }, 5000);
|
|
5200
|
-
|
|
5201
|
-
testMsgEl.textContent = "\u2713 \u6d4f\u89c8\u5668\u901a\u77e5 + \u5e94\u7528\u5185\u6c14\u6ce1\u5747\u5df2\u53d1\u9001";
|
|
5202
|
-
testMsgEl.style.color = "var(--success)";
|
|
5203
|
-
testMsgEl.classList.remove("hidden");
|
|
5204
|
-
}
|
|
5569
|
+
results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5");
|
|
5205
5570
|
} catch (_e) {
|
|
5206
|
-
|
|
5207
|
-
testMsgEl.textContent = "\u6d4f\u89c8\u5668\u901a\u77e5\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS";
|
|
5208
|
-
testMsgEl.style.color = "var(--warning)";
|
|
5209
|
-
testMsgEl.classList.remove("hidden");
|
|
5210
|
-
}
|
|
5571
|
+
results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
|
|
5211
5572
|
}
|
|
5573
|
+
showTestResults(testMsgEl, results);
|
|
5574
|
+
} else if (perm === "denied") {
|
|
5575
|
+
results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
|
|
5576
|
+
showTestResults(testMsgEl, results);
|
|
5212
5577
|
} else {
|
|
5213
|
-
//
|
|
5578
|
+
// "default" — try requesting
|
|
5214
5579
|
Notification.requestPermission().then(function(result) {
|
|
5215
5580
|
updateNotificationStatus();
|
|
5216
5581
|
if (result === "granted") {
|
|
5217
|
-
|
|
5218
|
-
} else
|
|
5219
|
-
|
|
5220
|
-
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";
|
|
5221
|
-
testMsgEl.style.color = "var(--fg-muted)";
|
|
5222
|
-
testMsgEl.classList.remove("hidden");
|
|
5223
|
-
}
|
|
5582
|
+
results.push("\u2713 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
5583
|
+
} else {
|
|
5584
|
+
results.push("\u2717 \u6d4f\u89c8\u5668\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
5224
5585
|
}
|
|
5586
|
+
showTestResults(testMsgEl, results);
|
|
5225
5587
|
});
|
|
5226
5588
|
}
|
|
5227
5589
|
}
|
|
5228
5590
|
|
|
5591
|
+
function showTestResults(el, results) {
|
|
5592
|
+
if (!el) return;
|
|
5593
|
+
el.innerHTML = results.map(function(r) { return escapeHtml(r); }).join("<br>");
|
|
5594
|
+
// color based on whether all passed
|
|
5595
|
+
var allOk = results.every(function(r) { return r.indexOf("\u2713") === 0 || r.indexOf("\u2013") === 0; });
|
|
5596
|
+
el.style.color = allOk ? "var(--success)" : "var(--warning)";
|
|
5597
|
+
el.classList.remove("hidden");
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5229
5600
|
function quickStartSession() {
|
|
5230
5601
|
var command = getPreferredTool();
|
|
5231
5602
|
var defaultCwd = getEffectiveCwd();
|
|
@@ -6225,13 +6596,17 @@
|
|
|
6225
6596
|
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
6226
6597
|
userMsgs.push(userTurn);
|
|
6227
6598
|
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
6599
|
+
// Write optimistic user turn into session.messages so WS updates
|
|
6600
|
+
// that arrive before the HTTP response don't erase it.
|
|
6228
6601
|
updateSessionSnapshot({
|
|
6229
6602
|
id: session.id,
|
|
6230
6603
|
status: "running",
|
|
6604
|
+
messages: userMsgs,
|
|
6231
6605
|
structuredState: optimisticStructuredState,
|
|
6232
6606
|
});
|
|
6233
6607
|
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
6234
6608
|
status: "running",
|
|
6609
|
+
messages: userMsgs,
|
|
6235
6610
|
structuredState: optimisticStructuredState,
|
|
6236
6611
|
}), userMsgs);
|
|
6237
6612
|
updateInputHint("思考中…");
|
|
@@ -6244,6 +6619,11 @@
|
|
|
6244
6619
|
}
|
|
6245
6620
|
setDraftValue("");
|
|
6246
6621
|
|
|
6622
|
+
// Capture queue epoch before the POST so we can detect whether
|
|
6623
|
+
// a newer WS update has already refreshed the queue by the time
|
|
6624
|
+
// the HTTP response arrives.
|
|
6625
|
+
var epochBeforePost = state.queueEpoch;
|
|
6626
|
+
|
|
6247
6627
|
return fetch("/api/structured-sessions/" + state.selectedId + "/messages", {
|
|
6248
6628
|
method: "POST",
|
|
6249
6629
|
headers: { "Content-Type": "application/json" },
|
|
@@ -6256,13 +6636,21 @@
|
|
|
6256
6636
|
throw new Error(snapshot.error);
|
|
6257
6637
|
}
|
|
6258
6638
|
if (snapshot && snapshot.id) {
|
|
6639
|
+
// If a WS update has already bumped the queue epoch, the HTTP
|
|
6640
|
+
// response's queuedMessages is stale — drop it to avoid
|
|
6641
|
+
// re-introducing already-dequeued items.
|
|
6642
|
+
if (state.queueEpoch > epochBeforePost && snapshot.queuedMessages) {
|
|
6643
|
+
delete snapshot.queuedMessages;
|
|
6644
|
+
}
|
|
6259
6645
|
updateSessionSnapshot(snapshot);
|
|
6260
6646
|
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
6261
6647
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
6262
6648
|
renderChat(true);
|
|
6263
6649
|
if (isQueueing) {
|
|
6264
6650
|
var queuedCount = getStructuredQueuedInputs(refreshedSession).length;
|
|
6265
|
-
|
|
6651
|
+
if (queuedCount > 0) {
|
|
6652
|
+
showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
6653
|
+
}
|
|
6266
6654
|
} else {
|
|
6267
6655
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
6268
6656
|
}
|
|
@@ -6314,7 +6702,25 @@
|
|
|
6314
6702
|
}
|
|
6315
6703
|
var queued = getStructuredQueuedInputs(session);
|
|
6316
6704
|
if (queued && queued.length > 0) {
|
|
6705
|
+
// Collect recent user message texts to deduplicate against queued items.
|
|
6706
|
+
// A queued message that already appears as a real user turn should not
|
|
6707
|
+
// be rendered a second time with the "排队中" badge.
|
|
6708
|
+
var existingUserTexts = {};
|
|
6709
|
+
for (var ei = base.length - 1; ei >= 0 && Object.keys(existingUserTexts).length < queued.length + 5; ei--) {
|
|
6710
|
+
var em = base[ei];
|
|
6711
|
+
if (em && em.role === "user" && Array.isArray(em.content)) {
|
|
6712
|
+
for (var ej = 0; ej < em.content.length; ej++) {
|
|
6713
|
+
if (em.content[ej] && em.content[ej].type === "text" && em.content[ej].text) {
|
|
6714
|
+
existingUserTexts[em.content[ej].text] = (existingUserTexts[em.content[ej].text] || 0) + 1;
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6317
6719
|
for (var qi = 0; qi < queued.length; qi++) {
|
|
6720
|
+
if (existingUserTexts[queued[qi]]) {
|
|
6721
|
+
existingUserTexts[queued[qi]]--;
|
|
6722
|
+
continue; // Skip — this queued text is already shown as a real message
|
|
6723
|
+
}
|
|
6318
6724
|
base.push({ role: "user", content: [{ type: "text", text: queued[qi], __queued: true }] });
|
|
6319
6725
|
}
|
|
6320
6726
|
}
|
|
@@ -7429,24 +7835,7 @@
|
|
|
7429
7835
|
|
|
7430
7836
|
function handleInputBoxBlur() {
|
|
7431
7837
|
resetInputPanelViewportSpacing();
|
|
7432
|
-
// Restore app container height when keyboard closes.
|
|
7433
|
-
// Use a short delay because on iOS the visualViewport may not
|
|
7434
|
-
// have updated yet at the moment blur fires.
|
|
7435
7838
|
setTimeout(function() {
|
|
7436
|
-
var appContainer = document.querySelector('.app-container');
|
|
7437
|
-
if (appContainer) {
|
|
7438
|
-
// Only clear if keyboard is actually closed now
|
|
7439
|
-
var vv = window.visualViewport;
|
|
7440
|
-
if (vv) {
|
|
7441
|
-
var offsetBottom = window.innerHeight - vv.height - vv.offsetTop;
|
|
7442
|
-
if (offsetBottom <= 50) {
|
|
7443
|
-
appContainer.style.height = '';
|
|
7444
|
-
}
|
|
7445
|
-
} else {
|
|
7446
|
-
appContainer.style.height = '';
|
|
7447
|
-
}
|
|
7448
|
-
}
|
|
7449
|
-
// Scroll the window back to top to fix any residual offset
|
|
7450
7839
|
window.scrollTo(0, 0);
|
|
7451
7840
|
}, 100);
|
|
7452
7841
|
}
|
|
@@ -8169,20 +8558,6 @@
|
|
|
8169
8558
|
var isKeyboardOpen = offsetBottom > 50;
|
|
8170
8559
|
var heightChanged = Math.abs(vv.height - lastHeight) > 8;
|
|
8171
8560
|
|
|
8172
|
-
// Dynamically resize the app container to match visible viewport.
|
|
8173
|
-
// This is needed because 100dvh does NOT shrink when the keyboard
|
|
8174
|
-
// appears in PWA standalone mode, and on some browsers the layout
|
|
8175
|
-
// viewport doesn't update on keyboard dismiss without this.
|
|
8176
|
-
var appContainer = document.querySelector('.app-container');
|
|
8177
|
-
if (appContainer) {
|
|
8178
|
-
if (isKeyboardOpen) {
|
|
8179
|
-
appContainer.style.height = vv.height + 'px';
|
|
8180
|
-
} else if (keyboardOpen) {
|
|
8181
|
-
// Keyboard just closed — clear forced height
|
|
8182
|
-
appContainer.style.height = '';
|
|
8183
|
-
}
|
|
8184
|
-
}
|
|
8185
|
-
|
|
8186
8561
|
if (isKeyboardOpen && (!keyboardOpen || heightChanged) && shouldAdjustForKeyboard(vv, inputBox)) {
|
|
8187
8562
|
syncInputBoxScroll(inputBox);
|
|
8188
8563
|
}
|
|
@@ -8507,8 +8882,9 @@
|
|
|
8507
8882
|
if (msg.data.messages) {
|
|
8508
8883
|
snapshot.messages = msg.data.messages;
|
|
8509
8884
|
}
|
|
8510
|
-
if (msg.data
|
|
8511
|
-
snapshot.queuedMessages = msg.data.queuedMessages;
|
|
8885
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
|
|
8886
|
+
snapshot.queuedMessages = msg.data.queuedMessages || [];
|
|
8887
|
+
state.queueEpoch++;
|
|
8512
8888
|
}
|
|
8513
8889
|
if (msg.data.structuredState) {
|
|
8514
8890
|
snapshot.structuredState = msg.data.structuredState;
|
|
@@ -8696,6 +9072,10 @@
|
|
|
8696
9072
|
});
|
|
8697
9073
|
}
|
|
8698
9074
|
}
|
|
9075
|
+
if (Object.prototype.hasOwnProperty.call(msg.data, 'queuedMessages')) {
|
|
9076
|
+
statusUpdate.queuedMessages = msg.data.queuedMessages || [];
|
|
9077
|
+
state.queueEpoch++;
|
|
9078
|
+
}
|
|
8699
9079
|
if (Object.prototype.hasOwnProperty.call(msg.data, 'permissionBlocked')) {
|
|
8700
9080
|
statusUpdate.permissionBlocked = !!msg.data.permissionBlocked;
|
|
8701
9081
|
}
|
|
@@ -8749,12 +9129,13 @@
|
|
|
8749
9129
|
}
|
|
8750
9130
|
// Re-render chat when structured session inFlight state changes
|
|
8751
9131
|
if (statusUpdate.structuredState) {
|
|
8752
|
-
|
|
8753
|
-
//
|
|
9132
|
+
// Flush queued structured messages synchronously before render
|
|
9133
|
+
// so the chat view uses up-to-date queue state.
|
|
8754
9134
|
if (!statusUpdate.structuredState.inFlight) {
|
|
8755
9135
|
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
8756
|
-
|
|
9136
|
+
flushStructuredInputQueue();
|
|
8757
9137
|
}
|
|
9138
|
+
scheduleChatRender();
|
|
8758
9139
|
}
|
|
8759
9140
|
}
|
|
8760
9141
|
}
|
|
@@ -8762,23 +9143,14 @@
|
|
|
8762
9143
|
case 'notification':
|
|
8763
9144
|
if (msg.data) {
|
|
8764
9145
|
if (msg.data.kind === "update") {
|
|
8765
|
-
|
|
8766
|
-
title: "\u53d1\u73b0\u65b0\u7248\u672c",
|
|
8767
|
-
body: "\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
|
|
8768
|
-
type: "info",
|
|
8769
|
-
icon: "\u2191",
|
|
8770
|
-
duration: 0,
|
|
8771
|
-
actionLabel: "\u53bb\u66f4\u65b0",
|
|
8772
|
-
action: function() {
|
|
8773
|
-
var settingsBtn = document.getElementById("open-settings-btn") || document.querySelector("[data-action='settings']");
|
|
8774
|
-
if (settingsBtn) settingsBtn.click();
|
|
8775
|
-
}
|
|
8776
|
-
});
|
|
9146
|
+
showUpdateBubble(msg.data.current || "-", msg.data.latest || "-");
|
|
8777
9147
|
sendBrowserNotification(
|
|
8778
9148
|
"Wand \u53d1\u73b0\u65b0\u7248\u672c",
|
|
8779
9149
|
"\u5f53\u524d " + (msg.data.current || "-") + " \u2192 \u6700\u65b0 " + (msg.data.latest || "-"),
|
|
8780
9150
|
{ tag: "wand-update" }
|
|
8781
9151
|
);
|
|
9152
|
+
} else if (msg.data.kind === "restart") {
|
|
9153
|
+
showRestartOverlay();
|
|
8782
9154
|
}
|
|
8783
9155
|
}
|
|
8784
9156
|
break;
|
|
@@ -9455,7 +9827,7 @@
|
|
|
9455
9827
|
updateChatJumpToBottomButton();
|
|
9456
9828
|
return;
|
|
9457
9829
|
}
|
|
9458
|
-
var chatMsgs = container && container.classList && container.classList.contains("chat-messages")
|
|
9830
|
+
var chatMsgs = (container && container.classList && container.classList.contains("chat-messages"))
|
|
9459
9831
|
? container
|
|
9460
9832
|
: getChatScrollElement();
|
|
9461
9833
|
if (!chatMsgs || !chatMsgs.isConnected) return;
|
|
@@ -11068,33 +11440,116 @@
|
|
|
11068
11440
|
return renderDiffTool(block, toolResult, toolName);
|
|
11069
11441
|
}
|
|
11070
11442
|
|
|
11071
|
-
// ── AskUserQuestion tool — special card
|
|
11443
|
+
// ── AskUserQuestion tool — special card with batch submit
|
|
11072
11444
|
if (toolName === "AskUserQuestion" && block.input && block.input.questions) {
|
|
11073
11445
|
var questions = block.input.questions;
|
|
11074
11446
|
if (questions && questions.length > 0) {
|
|
11447
|
+
var isAnswered = !!toolResult;
|
|
11448
|
+
var sel = state.askUserSelections[toolId] || {};
|
|
11449
|
+
var isSubmitted = !!sel.submitted;
|
|
11450
|
+
var answerText = isAnswered ? extractToolResultText(toolResult.content) : "";
|
|
11451
|
+
var answerLines = answerText ? answerText.trim().split("\n") : [];
|
|
11452
|
+
|
|
11453
|
+
// Build header summary
|
|
11454
|
+
var headerLabel = "";
|
|
11455
|
+
for (var hi = 0; hi < questions.length; hi++) {
|
|
11456
|
+
if (questions[hi].header) { headerLabel = questions[hi].header; break; }
|
|
11457
|
+
}
|
|
11458
|
+
var headerSummary = headerLabel ? '<span class="tool-use-summary">' + escapeHtml(headerLabel) + '</span>' : "";
|
|
11459
|
+
|
|
11075
11460
|
var questionsHtml = "";
|
|
11076
11461
|
questions.forEach(function(question, qIdx) {
|
|
11462
|
+
var isMulti = !!question.multiSelect;
|
|
11077
11463
|
var questionText = question.question ? '<div class="ask-user-title">' + escapeHtml(question.question) + '</div>' : "";
|
|
11078
11464
|
var optionsHtml = "";
|
|
11079
11465
|
if (question.options && question.options.length > 0) {
|
|
11080
|
-
optionsHtml = '<div class="ask-user-options">';
|
|
11466
|
+
optionsHtml = '<div class="ask-user-options" data-multi-select="' + isMulti + '">';
|
|
11081
11467
|
question.options.forEach(function(opt, idx) {
|
|
11082
11468
|
var label = opt.label ? escapeHtml(opt.label) : "选项 " + (idx + 1);
|
|
11083
|
-
|
|
11084
|
-
|
|
11085
|
-
|
|
11469
|
+
var descHtml = opt.description ? '<div class="ask-user-option-desc">' + escapeHtml(opt.description) + '</div>' : "";
|
|
11470
|
+
|
|
11471
|
+
if (isAnswered) {
|
|
11472
|
+
// Read-only: check if this option was the chosen answer
|
|
11473
|
+
var answerLine = answerLines[qIdx] || answerLines[0] || "";
|
|
11474
|
+
var chosenLabels = answerLine.split(",").map(function(s) { return s.trim(); });
|
|
11475
|
+
var isChosen = chosenLabels.indexOf(opt.label || "") !== -1;
|
|
11476
|
+
optionsHtml += '<div class="ask-user-option ask-user-option-readonly' + (isChosen ? ' ask-user-option-chosen' : '') + '">' +
|
|
11477
|
+
'<span class="ask-user-indicator"></span>' +
|
|
11478
|
+
'<div class="ask-user-option-content">' +
|
|
11479
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
11480
|
+
descHtml +
|
|
11481
|
+
'</div>' +
|
|
11482
|
+
'</div>';
|
|
11483
|
+
} else {
|
|
11484
|
+
// Interactive: selection state from askUserSelections
|
|
11485
|
+
var isSelected = (sel[qIdx] || []).indexOf(idx) !== -1;
|
|
11486
|
+
var disabledAttr = isSubmitted ? ' disabled' : '';
|
|
11487
|
+
optionsHtml += '<button class="ask-user-option' + (isSelected ? ' selected' : '') + '"' +
|
|
11488
|
+
' data-option-index="' + idx + '"' +
|
|
11489
|
+
' data-question-index="' + qIdx + '"' +
|
|
11490
|
+
' data-option-label="' + escapeHtml(opt.label || "选项 " + (idx + 1)) + '"' +
|
|
11491
|
+
' onclick="__askSelect(\'' + escapeHtml(toolId) + '\',' + qIdx + ',' + idx + ',' + isMulti + ')"' +
|
|
11492
|
+
disabledAttr + '>' +
|
|
11493
|
+
'<span class="ask-user-indicator"></span>' +
|
|
11494
|
+
'<div class="ask-user-option-content">' +
|
|
11495
|
+
'<div class="ask-user-option-label">' + label + '</div>' +
|
|
11496
|
+
descHtml +
|
|
11497
|
+
'</div>' +
|
|
11498
|
+
'</button>';
|
|
11499
|
+
}
|
|
11086
11500
|
});
|
|
11087
11501
|
optionsHtml += '</div>';
|
|
11088
11502
|
}
|
|
11089
|
-
questionsHtml += '<div class="ask-user-question-group">' + questionText + optionsHtml + '</div>';
|
|
11503
|
+
questionsHtml += '<div class="ask-user-question-group" data-question-index="' + qIdx + '">' + questionText + optionsHtml + '</div>';
|
|
11090
11504
|
});
|
|
11091
|
-
|
|
11505
|
+
|
|
11506
|
+
// Submit button (only for interactive state)
|
|
11507
|
+
var actionsHtml = "";
|
|
11508
|
+
if (!isAnswered) {
|
|
11509
|
+
var allAnsweredCheck = true;
|
|
11510
|
+
for (var qi = 0; qi < questions.length; qi++) {
|
|
11511
|
+
if (!sel[qi] || sel[qi].length === 0) { allAnsweredCheck = false; break; }
|
|
11512
|
+
}
|
|
11513
|
+
var submitDisabled = (!allAnsweredCheck || isSubmitted) ? " disabled" : "";
|
|
11514
|
+
var submitClass = isSubmitted ? " ask-user-submitted" : "";
|
|
11515
|
+
var submitText = isSubmitted ? "已提交..." : "确认提交";
|
|
11516
|
+
actionsHtml = '<div class="ask-user-actions">' +
|
|
11517
|
+
'<button class="ask-user-submit' + submitClass + '" data-tool-use-id="' + escapeHtml(toolId) + '"' +
|
|
11518
|
+
' onclick="__askSubmit(\'' + escapeHtml(toolId) + '\')"' + submitDisabled + '>' +
|
|
11519
|
+
submitText +
|
|
11520
|
+
'</button>' +
|
|
11521
|
+
'</div>';
|
|
11522
|
+
}
|
|
11523
|
+
|
|
11524
|
+
// Answered summary for header
|
|
11525
|
+
var answeredSummary = "";
|
|
11526
|
+
if (isAnswered && answerText) {
|
|
11527
|
+
var shortAnswer = answerText.trim().replace(/\n/g, ", ");
|
|
11528
|
+
if (shortAnswer.length > 40) shortAnswer = shortAnswer.slice(0, 37) + "...";
|
|
11529
|
+
answeredSummary = '<span class="tool-use-file">' + escapeHtml(shortAnswer) + '</span>';
|
|
11530
|
+
}
|
|
11531
|
+
|
|
11532
|
+
// Expand state: default expanded when unanswered, collapsed when answered
|
|
11533
|
+
var askExpandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11534
|
+
var askPersisted = getPersistedExpandState(askExpandKey);
|
|
11535
|
+
var askShouldExpand = askPersisted === null ? !isAnswered : askPersisted;
|
|
11536
|
+
var askCollapsed = askShouldExpand ? "" : " collapsed";
|
|
11537
|
+
var answeredClass = isAnswered ? " ask-user-answered" : "";
|
|
11538
|
+
|
|
11539
|
+
return '<div class="tool-use-card ask-user' + answeredClass + askCollapsed + '"' +
|
|
11540
|
+
' data-tool-use-id="' + escapeHtml(toolId) + '"' +
|
|
11541
|
+
' data-expand-kind="tool-card"' +
|
|
11542
|
+
' data-expand-key="' + escapeHtml(askExpandKey) + '">' +
|
|
11092
11543
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
11093
|
-
'<span class="tool-use-icon"
|
|
11544
|
+
'<span class="tool-use-icon">' + (isAnswered ? '✓' : '?') + '</span>' +
|
|
11094
11545
|
'<span class="tool-use-name">提问</span>' +
|
|
11546
|
+
headerSummary +
|
|
11547
|
+
answeredSummary +
|
|
11548
|
+
'<span class="tool-use-toggle">▼</span>' +
|
|
11095
11549
|
'</div>' +
|
|
11096
11550
|
'<div class="tool-use-body ask-user-body">' +
|
|
11097
11551
|
questionsHtml +
|
|
11552
|
+
actionsHtml +
|
|
11098
11553
|
'</div>' +
|
|
11099
11554
|
'</div>';
|
|
11100
11555
|
}
|
|
@@ -11565,8 +12020,8 @@
|
|
|
11565
12020
|
|
|
11566
12021
|
var notificationStack = [];
|
|
11567
12022
|
var notificationIdCounter = 0;
|
|
11568
|
-
var NOTIFICATION_GAP =
|
|
11569
|
-
var NOTIFICATION_TOP =
|
|
12023
|
+
var NOTIFICATION_GAP = 6;
|
|
12024
|
+
var NOTIFICATION_TOP = 16;
|
|
11570
12025
|
|
|
11571
12026
|
/**
|
|
11572
12027
|
* Show an in-app notification bubble at bottom-right.
|
|
@@ -11581,6 +12036,12 @@
|
|
|
11581
12036
|
* @returns {{ dismiss: function }} handle
|
|
11582
12037
|
*/
|
|
11583
12038
|
function showNotificationBubble(opts) {
|
|
12039
|
+
// Play sound for important notifications — independent of bubble setting
|
|
12040
|
+
if (opts.actionLabel || opts.playSound) playNotificationSound();
|
|
12041
|
+
|
|
12042
|
+
// Respect user preference (skip if bubbles disabled)
|
|
12043
|
+
if (!state.notifBubble) return { dismiss: function() {} };
|
|
12044
|
+
|
|
11584
12045
|
var id = ++notificationIdCounter;
|
|
11585
12046
|
var type = opts.type || "info";
|
|
11586
12047
|
var icon = opts.icon || (type === "warning" ? "!" : type === "success" ? "\u2713" : "i");
|
|
@@ -11691,6 +12152,221 @@
|
|
|
11691
12152
|
}
|
|
11692
12153
|
}
|
|
11693
12154
|
|
|
12155
|
+
/**
|
|
12156
|
+
* Play a soft, rounded notification chime using Web Audio API.
|
|
12157
|
+
* Two ascending sine tones with smooth gain envelope — gentle on the ears.
|
|
12158
|
+
*/
|
|
12159
|
+
function playNotificationSound() {
|
|
12160
|
+
if (!state.notifSound) return;
|
|
12161
|
+
_doPlaySound();
|
|
12162
|
+
}
|
|
12163
|
+
|
|
12164
|
+
/**
|
|
12165
|
+
* Try to play the notification sound regardless of user preference.
|
|
12166
|
+
* Returns true if playback was initiated successfully.
|
|
12167
|
+
* Used by the test function to always attempt playback.
|
|
12168
|
+
*/
|
|
12169
|
+
function tryPlayNotificationSound() {
|
|
12170
|
+
return _doPlaySound();
|
|
12171
|
+
}
|
|
12172
|
+
|
|
12173
|
+
function _doPlaySound() {
|
|
12174
|
+
try {
|
|
12175
|
+
var AudioCtx = window.AudioContext || window.webkitAudioContext;
|
|
12176
|
+
if (!AudioCtx) return false;
|
|
12177
|
+
var ctx = new AudioCtx();
|
|
12178
|
+
|
|
12179
|
+
// Some browsers suspend AudioContext until user gesture — resume it
|
|
12180
|
+
if (ctx.state === "suspended") ctx.resume();
|
|
12181
|
+
|
|
12182
|
+
function tone(freq, start, dur) {
|
|
12183
|
+
var osc = ctx.createOscillator();
|
|
12184
|
+
var gain = ctx.createGain();
|
|
12185
|
+
osc.type = "sine";
|
|
12186
|
+
osc.frequency.value = freq;
|
|
12187
|
+
gain.gain.setValueAtTime(0, ctx.currentTime + start);
|
|
12188
|
+
gain.gain.linearRampToValueAtTime(0.18, ctx.currentTime + start + 0.04);
|
|
12189
|
+
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + start + dur);
|
|
12190
|
+
osc.connect(gain);
|
|
12191
|
+
gain.connect(ctx.destination);
|
|
12192
|
+
osc.start(ctx.currentTime + start);
|
|
12193
|
+
osc.stop(ctx.currentTime + start + dur);
|
|
12194
|
+
}
|
|
12195
|
+
|
|
12196
|
+
// Two-tone ascending chime: C5 → E5, soft and brief
|
|
12197
|
+
tone(523, 0, 0.25);
|
|
12198
|
+
tone(659, 0.12, 0.3);
|
|
12199
|
+
|
|
12200
|
+
// Clean up context after playback
|
|
12201
|
+
setTimeout(function() { ctx.close(); }, 600);
|
|
12202
|
+
return true;
|
|
12203
|
+
} catch (_e) {
|
|
12204
|
+
// Web Audio not available or blocked
|
|
12205
|
+
return false;
|
|
12206
|
+
}
|
|
12207
|
+
}
|
|
12208
|
+
|
|
12209
|
+
/**
|
|
12210
|
+
* Show an interactive update bubble that allows updating and restarting
|
|
12211
|
+
* directly from the notification, without navigating to settings.
|
|
12212
|
+
*/
|
|
12213
|
+
function showUpdateBubble(currentVer, latestVer) {
|
|
12214
|
+
// Prevent duplicate bubbles
|
|
12215
|
+
if (state._updateBubbleShown) return;
|
|
12216
|
+
state._updateBubbleShown = true;
|
|
12217
|
+
|
|
12218
|
+
playNotificationSound();
|
|
12219
|
+
|
|
12220
|
+
var id = ++notificationIdCounter;
|
|
12221
|
+
var bubble = document.createElement("div");
|
|
12222
|
+
bubble.className = "notification-bubble";
|
|
12223
|
+
bubble.setAttribute("data-nid", id);
|
|
12224
|
+
|
|
12225
|
+
bubble.innerHTML =
|
|
12226
|
+
'<div class="notification-bubble-header">' +
|
|
12227
|
+
'<span class="notification-bubble-icon info">\u2191</span>' +
|
|
12228
|
+
'<span class="notification-bubble-title">\u53d1\u73b0\u65b0\u7248\u672c</span>' +
|
|
12229
|
+
'<button class="notification-bubble-close" title="\u5173\u95ed">\u00d7</button>' +
|
|
12230
|
+
'</div>' +
|
|
12231
|
+
'<div class="notification-bubble-body">' +
|
|
12232
|
+
escapeHtml(currentVer) + ' \u2192 ' + escapeHtml(latestVer) +
|
|
12233
|
+
'</div>' +
|
|
12234
|
+
'<div class="notification-bubble-actions">' +
|
|
12235
|
+
'<button class="primary" id="update-bubble-action">\u7acb\u5373\u66f4\u65b0</button>' +
|
|
12236
|
+
'</div>';
|
|
12237
|
+
|
|
12238
|
+
document.body.appendChild(bubble);
|
|
12239
|
+
|
|
12240
|
+
var entry = { id: id, el: bubble };
|
|
12241
|
+
notificationStack.push(entry);
|
|
12242
|
+
repositionNotifications();
|
|
12243
|
+
|
|
12244
|
+
var closeBtn = bubble.querySelector(".notification-bubble-close");
|
|
12245
|
+
if (closeBtn) closeBtn.onclick = function() {
|
|
12246
|
+
dismissNotification(id);
|
|
12247
|
+
state._updateBubbleShown = false;
|
|
12248
|
+
};
|
|
12249
|
+
|
|
12250
|
+
var actionBtn = bubble.querySelector("#update-bubble-action");
|
|
12251
|
+
var bodyEl = bubble.querySelector(".notification-bubble-body");
|
|
12252
|
+
|
|
12253
|
+
if (actionBtn) actionBtn.onclick = function() {
|
|
12254
|
+
// Phase 1: Performing update
|
|
12255
|
+
actionBtn.disabled = true;
|
|
12256
|
+
actionBtn.textContent = "\u66f4\u65b0\u4e2d\u2026";
|
|
12257
|
+
if (bodyEl) bodyEl.textContent = "\u6b63\u5728\u4e0b\u8f7d\u5e76\u5b89\u88c5\u65b0\u7248\u672c\u2026";
|
|
12258
|
+
|
|
12259
|
+
fetch("/api/update", {
|
|
12260
|
+
method: "POST",
|
|
12261
|
+
headers: { "Content-Type": "application/json" },
|
|
12262
|
+
credentials: "same-origin"
|
|
12263
|
+
})
|
|
12264
|
+
.then(function(res) { return res.json(); })
|
|
12265
|
+
.then(function(data) {
|
|
12266
|
+
if (data.error) {
|
|
12267
|
+
// Update failed
|
|
12268
|
+
if (bodyEl) {
|
|
12269
|
+
bodyEl.textContent = data.error;
|
|
12270
|
+
bodyEl.style.color = "var(--error)";
|
|
12271
|
+
}
|
|
12272
|
+
actionBtn.disabled = false;
|
|
12273
|
+
actionBtn.textContent = "\u91cd\u8bd5";
|
|
12274
|
+
return;
|
|
12275
|
+
}
|
|
12276
|
+
// Phase 2: Update succeeded, show restart button
|
|
12277
|
+
if (bodyEl) {
|
|
12278
|
+
bodyEl.textContent = data.message || "\u66f4\u65b0\u5b8c\u6210";
|
|
12279
|
+
bodyEl.style.color = "var(--success)";
|
|
12280
|
+
}
|
|
12281
|
+
actionBtn.textContent = "\u91cd\u542f\u751f\u6548";
|
|
12282
|
+
actionBtn.disabled = false;
|
|
12283
|
+
actionBtn.className = "primary success";
|
|
12284
|
+
actionBtn.onclick = function() {
|
|
12285
|
+
performRestart(actionBtn, bodyEl);
|
|
12286
|
+
};
|
|
12287
|
+
})
|
|
12288
|
+
.catch(function() {
|
|
12289
|
+
if (bodyEl) {
|
|
12290
|
+
bodyEl.textContent = "\u66f4\u65b0\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5\u7f51\u7edc\u8fde\u63a5\u3002";
|
|
12291
|
+
bodyEl.style.color = "var(--error)";
|
|
12292
|
+
}
|
|
12293
|
+
actionBtn.disabled = false;
|
|
12294
|
+
actionBtn.textContent = "\u91cd\u8bd5";
|
|
12295
|
+
});
|
|
12296
|
+
};
|
|
12297
|
+
}
|
|
12298
|
+
|
|
12299
|
+
/**
|
|
12300
|
+
* Call POST /api/restart and show the restart overlay.
|
|
12301
|
+
*/
|
|
12302
|
+
function performRestart(btn, msgEl) {
|
|
12303
|
+
if (btn) {
|
|
12304
|
+
btn.disabled = true;
|
|
12305
|
+
btn.textContent = "\u6b63\u5728\u91cd\u542f\u2026";
|
|
12306
|
+
}
|
|
12307
|
+
if (msgEl) {
|
|
12308
|
+
msgEl.textContent = "\u670d\u52a1\u6b63\u5728\u91cd\u542f\u2026";
|
|
12309
|
+
msgEl.style.color = "var(--text-secondary)";
|
|
12310
|
+
}
|
|
12311
|
+
|
|
12312
|
+
fetch("/api/restart", {
|
|
12313
|
+
method: "POST",
|
|
12314
|
+
headers: { "Content-Type": "application/json" },
|
|
12315
|
+
credentials: "same-origin"
|
|
12316
|
+
})
|
|
12317
|
+
.then(function(res) { return res.json(); })
|
|
12318
|
+
.then(function() {
|
|
12319
|
+
showRestartOverlay();
|
|
12320
|
+
})
|
|
12321
|
+
.catch(function() {
|
|
12322
|
+
// Network error likely means server already shut down — show overlay anyway
|
|
12323
|
+
showRestartOverlay();
|
|
12324
|
+
});
|
|
12325
|
+
}
|
|
12326
|
+
|
|
12327
|
+
/**
|
|
12328
|
+
* Full-screen overlay shown during server restart.
|
|
12329
|
+
* Polls /api/config until the server comes back, then reloads the page.
|
|
12330
|
+
*/
|
|
12331
|
+
function showRestartOverlay() {
|
|
12332
|
+
// Avoid duplicates
|
|
12333
|
+
if (document.getElementById("restart-overlay")) return;
|
|
12334
|
+
|
|
12335
|
+
var overlay = document.createElement("div");
|
|
12336
|
+
overlay.id = "restart-overlay";
|
|
12337
|
+
overlay.className = "restart-overlay";
|
|
12338
|
+
overlay.innerHTML =
|
|
12339
|
+
'<div class="restart-overlay-content">' +
|
|
12340
|
+
'<div class="restart-spinner"></div>' +
|
|
12341
|
+
'<div class="restart-title">\u670d\u52a1\u6b63\u5728\u91cd\u542f</div>' +
|
|
12342
|
+
'<div class="restart-subtitle">\u7a0d\u540e\u5c06\u81ea\u52a8\u5237\u65b0\u9875\u9762\u2026</div>' +
|
|
12343
|
+
'</div>';
|
|
12344
|
+
document.body.appendChild(overlay);
|
|
12345
|
+
|
|
12346
|
+
var attempts = 0;
|
|
12347
|
+
var maxAttempts = 20; // 20 * 2s = 40s
|
|
12348
|
+
var timer = setInterval(function() {
|
|
12349
|
+
attempts++;
|
|
12350
|
+
fetch("/api/config", { credentials: "same-origin" })
|
|
12351
|
+
.then(function(res) {
|
|
12352
|
+
if (res.ok) {
|
|
12353
|
+
clearInterval(timer);
|
|
12354
|
+
location.reload();
|
|
12355
|
+
}
|
|
12356
|
+
})
|
|
12357
|
+
.catch(function() {
|
|
12358
|
+
// Server not ready yet
|
|
12359
|
+
});
|
|
12360
|
+
if (attempts >= maxAttempts) {
|
|
12361
|
+
clearInterval(timer);
|
|
12362
|
+
var subtitle = overlay.querySelector(".restart-subtitle");
|
|
12363
|
+
if (subtitle) {
|
|
12364
|
+
subtitle.innerHTML = '\u91cd\u542f\u8d85\u65f6\uff0c\u8bf7 <a href="javascript:location.reload()" style="color:var(--accent);text-decoration:underline">\u624b\u52a8\u5237\u65b0</a> \u9875\u9762\u3002';
|
|
12365
|
+
}
|
|
12366
|
+
}
|
|
12367
|
+
}, 2000);
|
|
12368
|
+
}
|
|
12369
|
+
|
|
11694
12370
|
function escapeHtml(value) {
|
|
11695
12371
|
return String(value)
|
|
11696
12372
|
.replace(/&/g, "&")
|