@co0ontty/wand 1.10.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 -0
- 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/server-session-routes.js +29 -1
- package/dist/server.js +276 -11
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +10 -0
- package/dist/types.d.ts +28 -0
- package/dist/web-ui/content/scripts.js +782 -67
- package/dist/web-ui/content/styles.css +160 -27
- package/dist/ws-broadcast.d.ts +3 -2
- package/dist/ws-broadcast.js +8 -2
- package/package.json +1 -1
|
@@ -137,6 +137,7 @@
|
|
|
137
137
|
notifBubble: (function() {
|
|
138
138
|
try { var v = localStorage.getItem("wand-notif-bubble"); return v === null ? true : v === "true"; } catch (e) { return true; }
|
|
139
139
|
})(),
|
|
140
|
+
toolContentCache: {},
|
|
140
141
|
currentView: "terminal",
|
|
141
142
|
terminalScale: (function() {
|
|
142
143
|
try {
|
|
@@ -175,6 +176,8 @@
|
|
|
175
176
|
lastRenderedMsgCount: 0,
|
|
176
177
|
lastRenderedEmpty: null,
|
|
177
178
|
renderPending: false,
|
|
179
|
+
chatPageSize: 20,
|
|
180
|
+
chatRenderedCount: 20,
|
|
178
181
|
currentTask: null, // Current task title from Claude
|
|
179
182
|
terminalInteractive: false,
|
|
180
183
|
miniKeyboardVisible: false,
|
|
@@ -402,6 +405,35 @@
|
|
|
402
405
|
updateChatJumpToBottomButton();
|
|
403
406
|
}
|
|
404
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
|
+
|
|
405
437
|
// Helper function to persist selected session ID to localStorage
|
|
406
438
|
function persistSelectedId() {
|
|
407
439
|
try {
|
|
@@ -704,6 +736,7 @@
|
|
|
704
736
|
state.lastRenderedMsgCount = 0;
|
|
705
737
|
state.lastRenderedEmpty = null;
|
|
706
738
|
state.renderPending = false;
|
|
739
|
+
state.chatRenderedCount = state.chatPageSize;
|
|
707
740
|
state.askUserSelections = {};
|
|
708
741
|
if (state.chatScrollElement && state.chatScrollHandler) {
|
|
709
742
|
state.chatScrollElement.removeEventListener("scroll", state.chatScrollHandler);
|
|
@@ -1283,6 +1316,7 @@
|
|
|
1283
1316
|
'<button class="settings-tab" data-tab="notifications">\u901a\u77e5</button>' +
|
|
1284
1317
|
'<button class="settings-tab" data-tab="security">\u5b89\u5168</button>' +
|
|
1285
1318
|
'<button class="settings-tab" data-tab="presets">\u547d\u4ee4\u9884\u8bbe</button>' +
|
|
1319
|
+
'<button class="settings-tab" data-tab="display">\u663e\u793a</button>' +
|
|
1286
1320
|
'</div>' +
|
|
1287
1321
|
|
|
1288
1322
|
// About tab
|
|
@@ -1305,6 +1339,31 @@
|
|
|
1305
1339
|
'</div>' +
|
|
1306
1340
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1307
1341
|
'</div>' +
|
|
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>' +
|
|
1308
1367
|
'</div>' +
|
|
1309
1368
|
|
|
1310
1369
|
// Notifications tab
|
|
@@ -1389,6 +1448,27 @@
|
|
|
1389
1448
|
'<label class="field-label" for="cfg-shell">Shell</label>' +
|
|
1390
1449
|
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
1391
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
|
+
: '') +
|
|
1392
1472
|
'<button id="save-config-button" class="btn btn-primary btn-block">保存配置</button>' +
|
|
1393
1473
|
'<p id="config-message" class="hint hidden"></p>' +
|
|
1394
1474
|
'</div>' +
|
|
@@ -1429,6 +1509,39 @@
|
|
|
1429
1509
|
'<div class="settings-panel" id="settings-tab-presets">' +
|
|
1430
1510
|
'<div id="presets-list" class="presets-list"></div>' +
|
|
1431
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>' +
|
|
1544
|
+
'</div>' +
|
|
1432
1545
|
'</div>' +
|
|
1433
1546
|
'</div>' +
|
|
1434
1547
|
'</section>';
|
|
@@ -1856,7 +1969,10 @@
|
|
|
1856
1969
|
} catch (e) {}
|
|
1857
1970
|
applyTerminalScale();
|
|
1858
1971
|
updateScaleLabel();
|
|
1859
|
-
|
|
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();
|
|
1860
1976
|
}
|
|
1861
1977
|
|
|
1862
1978
|
function applyTerminalScale() {
|
|
@@ -2295,6 +2411,35 @@
|
|
|
2295
2411
|
'</div>';
|
|
2296
2412
|
}
|
|
2297
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
|
+
|
|
2298
2443
|
function getSessionStatusLabel(session) {
|
|
2299
2444
|
if (!session) return "";
|
|
2300
2445
|
if (session.archived) return "已归档";
|
|
@@ -2317,29 +2462,55 @@
|
|
|
2317
2462
|
return session.status || "";
|
|
2318
2463
|
}
|
|
2319
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
|
+
|
|
2320
2500
|
function renderSessionItem(session) {
|
|
2321
2501
|
var activeClass = session.id === state.selectedId ? " active" : "";
|
|
2322
2502
|
var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
|
|
2323
2503
|
var metaStatus = getSessionStatusLabel(session);
|
|
2324
2504
|
var metaStatusClass = getSessionStatusClass(session);
|
|
2325
|
-
var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
|
|
2326
2505
|
var resumeButton = "";
|
|
2327
|
-
var sessionIdDisplay = "";
|
|
2328
|
-
var recoveryHint = "";
|
|
2329
2506
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
2330
2507
|
|
|
2331
2508
|
if (session.provider === "claude" && session.claudeSessionId) {
|
|
2332
|
-
var shortId = session.claudeSessionId.slice(0, 8);
|
|
2333
|
-
sessionIdDisplay = '<span class="session-id" title="' + escapeHtml(session.claudeSessionId) + '">' + escapeHtml(shortId) + '</span>';
|
|
2334
2509
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
2335
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>';
|
|
2336
2511
|
}
|
|
2337
2512
|
}
|
|
2338
2513
|
|
|
2339
|
-
if (session.autoRecovered) {
|
|
2340
|
-
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
2514
|
var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
2344
2515
|
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
2345
2516
|
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
@@ -2350,23 +2521,50 @@
|
|
|
2350
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>'
|
|
2351
2522
|
: "";
|
|
2352
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>';
|
|
2353
|
-
var modeBadge = renderSessionKindBadge(session);
|
|
2354
2524
|
var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
|
|
2355
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
|
+
|
|
2356
2554
|
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
2357
2555
|
'<div class="session-item-content">' +
|
|
2358
2556
|
'<div class="session-item-row">' +
|
|
2359
2557
|
checkbox +
|
|
2360
2558
|
'<div class="session-main">' +
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2559
|
+
'<div class="session-title-row">' +
|
|
2560
|
+
titleHtml +
|
|
2561
|
+
timeDisplay +
|
|
2562
|
+
'</div>' +
|
|
2563
|
+
activityHtml +
|
|
2364
2564
|
'<div class="session-meta">' +
|
|
2365
|
-
modeBadge +
|
|
2366
|
-
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
2367
2565
|
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
2368
|
-
|
|
2369
|
-
|
|
2566
|
+
badgesHtml +
|
|
2567
|
+
recoveryHtml +
|
|
2370
2568
|
'</div>' +
|
|
2371
2569
|
'</div>' +
|
|
2372
2570
|
actionsHtml +
|
|
@@ -2525,6 +2723,8 @@
|
|
|
2525
2723
|
'<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
|
|
2526
2724
|
'<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
|
|
2527
2725
|
'</div>' +
|
|
2726
|
+
'</div>' +
|
|
2727
|
+
'<div class="modal-footer">' +
|
|
2528
2728
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
2529
2729
|
'<p id="modal-error" class="error-message hidden"></p>' +
|
|
2530
2730
|
'</div>' +
|
|
@@ -2533,11 +2733,53 @@
|
|
|
2533
2733
|
}
|
|
2534
2734
|
|
|
2535
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
|
+
|
|
2536
2759
|
window.__tcToggle = function(e, headerEl) {
|
|
2537
2760
|
var card = headerEl.closest(".tool-use-card");
|
|
2538
2761
|
if (card) {
|
|
2762
|
+
var wasCollapsed = card.classList.contains("collapsed");
|
|
2539
2763
|
card.classList.toggle("collapsed");
|
|
2540
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
|
+
}
|
|
2541
2783
|
}
|
|
2542
2784
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
2543
2785
|
};
|
|
@@ -2576,6 +2818,24 @@
|
|
|
2576
2818
|
statusSpan.textContent = "✓";
|
|
2577
2819
|
}
|
|
2578
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
|
+
}
|
|
2579
2839
|
persistElementExpandState(el, "inline-tool");
|
|
2580
2840
|
};
|
|
2581
2841
|
// Toggle function for terminal tool blocks
|
|
@@ -2590,7 +2850,32 @@
|
|
|
2590
2850
|
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
2591
2851
|
if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
|
|
2592
2852
|
persistElementExpandState(container, "terminal");
|
|
2593
|
-
|
|
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
|
+
}
|
|
2594
2879
|
};
|
|
2595
2880
|
// Update streaming thinking content (called from WebSocket handler)
|
|
2596
2881
|
function updateStreamingThinking(text) {
|
|
@@ -2860,6 +3145,30 @@
|
|
|
2860
3145
|
}
|
|
2861
3146
|
var saveConfigBtn = document.getElementById("save-config-button");
|
|
2862
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
|
+
}
|
|
2863
3172
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
2864
3173
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
2865
3174
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
@@ -2868,6 +3177,11 @@
|
|
|
2868
3177
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
2869
3178
|
var doRestartBtn = document.getElementById("do-restart-button");
|
|
2870
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
|
+
});
|
|
2871
3185
|
// Notification preferences
|
|
2872
3186
|
var notifSoundEl = document.getElementById("cfg-notif-sound");
|
|
2873
3187
|
if (notifSoundEl) {
|
|
@@ -2890,7 +3204,13 @@
|
|
|
2890
3204
|
// Browser notification section
|
|
2891
3205
|
var notifRequestBtn = document.getElementById("notification-request-btn");
|
|
2892
3206
|
if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
|
|
2893
|
-
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") {
|
|
2894
3214
|
Notification.requestPermission().then(function() { updateNotificationStatus(); });
|
|
2895
3215
|
}
|
|
2896
3216
|
});
|
|
@@ -4664,6 +4984,15 @@
|
|
|
4664
4984
|
});
|
|
4665
4985
|
}
|
|
4666
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
|
+
|
|
4667
4996
|
function updateSessionsList() {
|
|
4668
4997
|
var listEl = document.getElementById("sessions-list");
|
|
4669
4998
|
var countEl = document.getElementById("session-count");
|
|
@@ -4792,6 +5121,8 @@
|
|
|
4792
5121
|
}
|
|
4793
5122
|
state.selectedId = id;
|
|
4794
5123
|
persistSelectedId();
|
|
5124
|
+
// Clear tool content cache on session switch
|
|
5125
|
+
state.toolContentCache = {};
|
|
4795
5126
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
4796
5127
|
state.messageQueue = [];
|
|
4797
5128
|
state.pendingMessages = [];
|
|
@@ -5119,6 +5450,10 @@
|
|
|
5119
5450
|
if (soundEl) soundEl.checked = state.notifSound;
|
|
5120
5451
|
if (bubbleEl) bubbleEl.checked = state.notifBubble;
|
|
5121
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
|
+
}
|
|
5122
5457
|
}
|
|
5123
5458
|
}
|
|
5124
5459
|
|
|
@@ -5210,6 +5545,46 @@
|
|
|
5210
5545
|
}
|
|
5211
5546
|
}
|
|
5212
5547
|
|
|
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
|
+
});
|
|
5572
|
+
}
|
|
5573
|
+
|
|
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;
|
|
5583
|
+
}
|
|
5584
|
+
var display = size >= 10 ? size.toFixed(0) : size.toFixed(1);
|
|
5585
|
+
return display + " " + units[unitIndex];
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5213
5588
|
function loadSettingsData() {
|
|
5214
5589
|
fetch("/api/settings", { credentials: "same-origin" })
|
|
5215
5590
|
.then(function(res) { return res.json(); })
|
|
@@ -5236,6 +5611,97 @@
|
|
|
5236
5611
|
}
|
|
5237
5612
|
}
|
|
5238
5613
|
|
|
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
|
+
}
|
|
5694
|
+
|
|
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
|
+
|
|
5239
5705
|
// Config fields
|
|
5240
5706
|
var cfg = data.config || {};
|
|
5241
5707
|
var hostEl = document.getElementById("cfg-host");
|
|
@@ -5274,6 +5740,19 @@
|
|
|
5274
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>';
|
|
5275
5741
|
presetsList.innerHTML = html;
|
|
5276
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;
|
|
5277
5756
|
})
|
|
5278
5757
|
.catch(function() {});
|
|
5279
5758
|
}
|
|
@@ -5320,6 +5799,52 @@
|
|
|
5320
5799
|
});
|
|
5321
5800
|
}
|
|
5322
5801
|
|
|
5802
|
+
function saveDisplaySettings() {
|
|
5803
|
+
var msgEl = document.getElementById("display-message");
|
|
5804
|
+
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
5805
|
+
|
|
5806
|
+
var body = {
|
|
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,
|
|
5813
|
+
}
|
|
5814
|
+
};
|
|
5815
|
+
|
|
5816
|
+
fetch("/api/settings/config", {
|
|
5817
|
+
method: "POST",
|
|
5818
|
+
headers: { "Content-Type": "application/json" },
|
|
5819
|
+
credentials: "same-origin",
|
|
5820
|
+
body: JSON.stringify(body)
|
|
5821
|
+
})
|
|
5822
|
+
.then(function(res) { return res.json(); })
|
|
5823
|
+
.then(function(data) {
|
|
5824
|
+
if (msgEl) {
|
|
5825
|
+
if (data.error) {
|
|
5826
|
+
msgEl.textContent = data.error;
|
|
5827
|
+
msgEl.style.color = "var(--error)";
|
|
5828
|
+
} else {
|
|
5829
|
+
msgEl.textContent = "显示设置已保存";
|
|
5830
|
+
msgEl.style.color = "var(--success)";
|
|
5831
|
+
}
|
|
5832
|
+
msgEl.classList.remove("hidden");
|
|
5833
|
+
}
|
|
5834
|
+
// Update local config so card defaults take effect immediately
|
|
5835
|
+
if (!data.error && state.config) {
|
|
5836
|
+
state.config.cardDefaults = body.cardDefaults;
|
|
5837
|
+
}
|
|
5838
|
+
})
|
|
5839
|
+
.catch(function() {
|
|
5840
|
+
if (msgEl) {
|
|
5841
|
+
msgEl.textContent = "保存失败。";
|
|
5842
|
+
msgEl.style.color = "var(--error)";
|
|
5843
|
+
msgEl.classList.remove("hidden");
|
|
5844
|
+
}
|
|
5845
|
+
});
|
|
5846
|
+
}
|
|
5847
|
+
|
|
5323
5848
|
function uploadCertificates() {
|
|
5324
5849
|
var keyFile = document.getElementById("cert-key-file");
|
|
5325
5850
|
var certFile = document.getElementById("cert-cert-file");
|
|
@@ -5459,6 +5984,16 @@
|
|
|
5459
5984
|
|
|
5460
5985
|
// ── Notification Settings Helpers ──
|
|
5461
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
|
+
|
|
5462
5997
|
function updateNotificationStatus() {
|
|
5463
5998
|
var statusEl = document.getElementById("notification-permission-status");
|
|
5464
5999
|
var requestBtn = document.getElementById("notification-request-btn");
|
|
@@ -5466,15 +6001,20 @@
|
|
|
5466
6001
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
5467
6002
|
if (!statusEl) return;
|
|
5468
6003
|
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
if (
|
|
5474
|
-
|
|
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;
|
|
5475
6016
|
}
|
|
5476
6017
|
|
|
5477
|
-
var perm = Notification.permission;
|
|
5478
6018
|
if (perm === "granted") {
|
|
5479
6019
|
statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
|
|
5480
6020
|
statusEl.style.color = "var(--success)";
|
|
@@ -5495,6 +6035,28 @@
|
|
|
5495
6035
|
|
|
5496
6036
|
function resetNotificationPermission() {
|
|
5497
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;
|
|
6058
|
+
}
|
|
6059
|
+
|
|
5498
6060
|
if (typeof Notification === "undefined") return;
|
|
5499
6061
|
|
|
5500
6062
|
// Always call requestPermission — this triggers the browser's native
|
|
@@ -5550,9 +6112,44 @@
|
|
|
5550
6112
|
});
|
|
5551
6113
|
results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
|
|
5552
6114
|
|
|
5553
|
-
// 3. Test browser
|
|
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
|
|
6146
|
+
}
|
|
6147
|
+
showTestResults(testMsgEl, results);
|
|
6148
|
+
return;
|
|
6149
|
+
}
|
|
6150
|
+
|
|
5554
6151
|
if (typeof Notification === "undefined") {
|
|
5555
|
-
results.push("\u2013 \
|
|
6152
|
+
results.push("\u2013 \u7cfb\u7edf\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
|
|
5556
6153
|
showTestResults(testMsgEl, results);
|
|
5557
6154
|
return;
|
|
5558
6155
|
}
|
|
@@ -5561,27 +6158,27 @@
|
|
|
5561
6158
|
if (perm === "granted") {
|
|
5562
6159
|
try {
|
|
5563
6160
|
var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
|
|
5564
|
-
body: "\
|
|
6161
|
+
body: "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
|
|
5565
6162
|
icon: "/favicon.ico",
|
|
5566
6163
|
tag: "wand-test",
|
|
5567
6164
|
});
|
|
5568
6165
|
setTimeout(function() { n.close(); }, 5000);
|
|
5569
|
-
results.push("\u2713 \
|
|
6166
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
|
|
5570
6167
|
} catch (_e) {
|
|
5571
|
-
results.push("\u2717 \
|
|
6168
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
|
|
5572
6169
|
}
|
|
5573
6170
|
showTestResults(testMsgEl, results);
|
|
5574
6171
|
} else if (perm === "denied") {
|
|
5575
|
-
results.push("\u2717 \
|
|
6172
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
|
|
5576
6173
|
showTestResults(testMsgEl, results);
|
|
5577
6174
|
} else {
|
|
5578
6175
|
// "default" — try requesting
|
|
5579
6176
|
Notification.requestPermission().then(function(result) {
|
|
5580
6177
|
updateNotificationStatus();
|
|
5581
6178
|
if (result === "granted") {
|
|
5582
|
-
results.push("\u2713 \
|
|
6179
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
5583
6180
|
} else {
|
|
5584
|
-
results.push("\u2717 \
|
|
6181
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
5585
6182
|
}
|
|
5586
6183
|
showTestResults(testMsgEl, results);
|
|
5587
6184
|
});
|
|
@@ -7837,6 +8434,13 @@
|
|
|
7837
8434
|
resetInputPanelViewportSpacing();
|
|
7838
8435
|
setTimeout(function() {
|
|
7839
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
|
+
}
|
|
7840
8444
|
}, 100);
|
|
7841
8445
|
}
|
|
7842
8446
|
|
|
@@ -8562,6 +9166,15 @@
|
|
|
8562
9166
|
syncInputBoxScroll(inputBox);
|
|
8563
9167
|
}
|
|
8564
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
|
+
|
|
8565
9178
|
keyboardOpen = isKeyboardOpen;
|
|
8566
9179
|
lastHeight = vv.height;
|
|
8567
9180
|
}
|
|
@@ -8666,6 +9279,11 @@
|
|
|
8666
9279
|
}
|
|
8667
9280
|
state.resizeHandler = function() { scheduleTerminalResize(true); };
|
|
8668
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
|
+
}
|
|
8669
9287
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
8670
9288
|
}
|
|
8671
9289
|
|
|
@@ -8682,6 +9300,10 @@
|
|
|
8682
9300
|
window.removeEventListener("resize", state.resizeHandler);
|
|
8683
9301
|
state.resizeHandler = null;
|
|
8684
9302
|
}
|
|
9303
|
+
if (state.visualViewportHandler && window.visualViewport) {
|
|
9304
|
+
window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
|
|
9305
|
+
state.visualViewportHandler = null;
|
|
9306
|
+
}
|
|
8685
9307
|
[["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
|
|
8686
9308
|
["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
|
|
8687
9309
|
].forEach(function(pair) {
|
|
@@ -8814,6 +9436,12 @@
|
|
|
8814
9436
|
state.pollTimer = setInterval(refreshAll, 1600);
|
|
8815
9437
|
}
|
|
8816
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
|
+
|
|
8817
9445
|
function initWebSocket() {
|
|
8818
9446
|
if (!window.WebSocket) return false;
|
|
8819
9447
|
|
|
@@ -8952,14 +9580,26 @@
|
|
|
8952
9580
|
// Trigger status bar completion animation
|
|
8953
9581
|
scheduleChatRender(true);
|
|
8954
9582
|
}
|
|
8955
|
-
// Notify user when a session completes
|
|
9583
|
+
// Notify user when a session completes — show what was accomplished
|
|
8956
9584
|
var endedSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
8957
|
-
var endedName = endedSession ? (endedSession.label || endedSession.command || msg.sessionId) : msg.sessionId;
|
|
8958
9585
|
var endedExitCode = msg.data && msg.data.exitCode;
|
|
8959
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
|
+
}
|
|
8960
9600
|
sendBrowserNotification(
|
|
8961
|
-
|
|
8962
|
-
|
|
9601
|
+
endedNotifTitle,
|
|
9602
|
+
endedNotifBody,
|
|
8963
9603
|
{
|
|
8964
9604
|
tag: "wand-ended-" + msg.sessionId,
|
|
8965
9605
|
onClick: function() {
|
|
@@ -8969,8 +9609,8 @@
|
|
|
8969
9609
|
);
|
|
8970
9610
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
8971
9611
|
showNotificationBubble({
|
|
8972
|
-
title:
|
|
8973
|
-
body:
|
|
9612
|
+
title: endedNotifTitle,
|
|
9613
|
+
body: endedNotifBody,
|
|
8974
9614
|
type: endedIsError ? "warning" : "success",
|
|
8975
9615
|
icon: endedIsError ? "!" : "\u2713",
|
|
8976
9616
|
duration: 6000,
|
|
@@ -9052,6 +9692,8 @@
|
|
|
9052
9692
|
state.currentTask = msg.data || null;
|
|
9053
9693
|
updateTaskDisplay();
|
|
9054
9694
|
}
|
|
9695
|
+
// Update session list to reflect current activity (debounced)
|
|
9696
|
+
scheduleSessionListUpdate();
|
|
9055
9697
|
break;
|
|
9056
9698
|
case 'status':
|
|
9057
9699
|
if (msg.sessionId && msg.data) {
|
|
@@ -9087,10 +9729,18 @@
|
|
|
9087
9729
|
};
|
|
9088
9730
|
// Browser notification for permission waiting (background tab)
|
|
9089
9731
|
var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
9090
|
-
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
|
+
}
|
|
9091
9741
|
sendBrowserNotification(
|
|
9092
|
-
"
|
|
9093
|
-
|
|
9742
|
+
"需要你的授权",
|
|
9743
|
+
permBody,
|
|
9094
9744
|
{
|
|
9095
9745
|
tag: "wand-perm-" + msg.sessionId,
|
|
9096
9746
|
onClick: function() {
|
|
@@ -9103,12 +9753,12 @@
|
|
|
9103
9753
|
// In-app bubble if not currently viewing this session
|
|
9104
9754
|
if (msg.sessionId !== state.selectedId) {
|
|
9105
9755
|
showNotificationBubble({
|
|
9106
|
-
title: "
|
|
9107
|
-
body:
|
|
9756
|
+
title: "需要你的授权",
|
|
9757
|
+
body: permBody,
|
|
9108
9758
|
type: "warning",
|
|
9109
9759
|
icon: "!",
|
|
9110
9760
|
duration: 0,
|
|
9111
|
-
actionLabel: "
|
|
9761
|
+
actionLabel: "去处理",
|
|
9112
9762
|
action: function() {
|
|
9113
9763
|
selectSession(msg.sessionId);
|
|
9114
9764
|
}
|
|
@@ -9542,9 +10192,9 @@
|
|
|
9542
10192
|
return;
|
|
9543
10193
|
}
|
|
9544
10194
|
|
|
9545
|
-
var
|
|
10195
|
+
var allMessages = state.currentMessages;
|
|
9546
10196
|
|
|
9547
|
-
if (
|
|
10197
|
+
if (allMessages.length === 0) {
|
|
9548
10198
|
if (state.lastRenderedEmpty !== "empty") {
|
|
9549
10199
|
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
|
|
9550
10200
|
state.lastRenderedEmpty = "empty";
|
|
@@ -9553,6 +10203,16 @@
|
|
|
9553
10203
|
return;
|
|
9554
10204
|
}
|
|
9555
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
|
+
|
|
9556
10216
|
// Check if messages actually changed
|
|
9557
10217
|
var msgCount = messages.length;
|
|
9558
10218
|
var outputHash = selectedSession.output ? selectedSession.output.length : 0;
|
|
@@ -9613,16 +10273,17 @@
|
|
|
9613
10273
|
// Build HTML with system info cards interleaved
|
|
9614
10274
|
var html = '';
|
|
9615
10275
|
var reversedMessages = messages.slice().reverse();
|
|
9616
|
-
var
|
|
10276
|
+
var visibleCount = messages.length;
|
|
9617
10277
|
|
|
9618
10278
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
9619
10279
|
var msg = reversedMessages[i];
|
|
9620
|
-
var
|
|
10280
|
+
var localIndex = visibleCount - 1 - i; // Index within visible slice
|
|
10281
|
+
var originalIndex = localIndex + visibleOffset; // Index in full messages array
|
|
9621
10282
|
|
|
9622
10283
|
// Find system info for this message position
|
|
9623
10284
|
var sysInfo = null;
|
|
9624
10285
|
for (var j = 0; j < systemInfo.length; j++) {
|
|
9625
|
-
if (systemInfo[j].beforeMessage ===
|
|
10286
|
+
if (systemInfo[j].beforeMessage === localIndex) {
|
|
9626
10287
|
sysInfo = systemInfo[j];
|
|
9627
10288
|
break;
|
|
9628
10289
|
}
|
|
@@ -9642,6 +10303,13 @@
|
|
|
9642
10303
|
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
9643
10304
|
}
|
|
9644
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
|
+
|
|
9645
10313
|
chatMessages.innerHTML = html;
|
|
9646
10314
|
attachAllCopyHandlers(chatMessages);
|
|
9647
10315
|
bindChatScrollListener();
|
|
@@ -9667,6 +10335,7 @@
|
|
|
9667
10335
|
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
9668
10336
|
requestAnimationFrame(function() {
|
|
9669
10337
|
smartScrollToBottom(chatMessages);
|
|
10338
|
+
observeLoadMoreSentinel();
|
|
9670
10339
|
});
|
|
9671
10340
|
}
|
|
9672
10341
|
|
|
@@ -9687,15 +10356,15 @@
|
|
|
9687
10356
|
});
|
|
9688
10357
|
}
|
|
9689
10358
|
|
|
9690
|
-
// Pre-compute per-round cumulative usage.
|
|
10359
|
+
// Pre-compute per-round cumulative usage using original (full array) indices.
|
|
9691
10360
|
// A "round" starts at a user message and includes all subsequent assistant turns
|
|
9692
10361
|
// until the next user message. Only the last assistant in each round shows the total.
|
|
9693
10362
|
var roundUsageByIndex = {};
|
|
9694
10363
|
(function() {
|
|
9695
10364
|
var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
9696
10365
|
var lastAssistantIdx = -1;
|
|
9697
|
-
for (var mi = 0; mi <
|
|
9698
|
-
var m =
|
|
10366
|
+
for (var mi = 0; mi < allMessages.length; mi++) {
|
|
10367
|
+
var m = allMessages[mi];
|
|
9699
10368
|
if (m.role === "user") {
|
|
9700
10369
|
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
9701
10370
|
roundUsageByIndex[lastAssistantIdx] = {
|
|
@@ -9739,7 +10408,7 @@
|
|
|
9739
10408
|
var insertedEls = [];
|
|
9740
10409
|
for (var i = 0; i < newMessages.length; i++) {
|
|
9741
10410
|
var div = document.createElement("div");
|
|
9742
|
-
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
10411
|
+
var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
|
|
9743
10412
|
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
9744
10413
|
var el = div.firstElementChild;
|
|
9745
10414
|
if (el) {
|
|
@@ -9771,7 +10440,7 @@
|
|
|
9771
10440
|
for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
|
|
9772
10441
|
var currentEl = existingEls[mi];
|
|
9773
10442
|
var tmpWrap = document.createElement("div");
|
|
9774
|
-
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
10443
|
+
var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
|
|
9775
10444
|
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
9776
10445
|
var replacementEl = tmpWrap.firstElementChild;
|
|
9777
10446
|
if (!replacementEl) continue;
|
|
@@ -9817,7 +10486,7 @@
|
|
|
9817
10486
|
renderStructuredStatusBar(chatMessages, selectedSession);
|
|
9818
10487
|
|
|
9819
10488
|
// Update todo progress bar from latest messages
|
|
9820
|
-
updateTodoProgress(
|
|
10489
|
+
updateTodoProgress(allMessages);
|
|
9821
10490
|
}
|
|
9822
10491
|
|
|
9823
10492
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
@@ -10890,7 +11559,8 @@
|
|
|
10890
11559
|
// Thinking card (deep thought) — from PTY parsing
|
|
10891
11560
|
if (msg.role === "thinking") {
|
|
10892
11561
|
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
10893
|
-
var
|
|
11562
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11563
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
10894
11564
|
return '<div class="chat-message thinking">' +
|
|
10895
11565
|
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
10896
11566
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
@@ -11028,7 +11698,7 @@
|
|
|
11028
11698
|
var summaryText = parts.join(" · ");
|
|
11029
11699
|
var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
|
|
11030
11700
|
var persistedExpanded = getPersistedExpandState(groupKey);
|
|
11031
|
-
var shouldExpand = persistedExpanded === null ?
|
|
11701
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.toolGroup) : persistedExpanded;
|
|
11032
11702
|
|
|
11033
11703
|
// Render each item's inline-tool card
|
|
11034
11704
|
var innerHtml = "";
|
|
@@ -11139,7 +11809,8 @@
|
|
|
11139
11809
|
'</div>';
|
|
11140
11810
|
}
|
|
11141
11811
|
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
11142
|
-
var
|
|
11812
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11813
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
11143
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)">' +
|
|
11144
11815
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
11145
11816
|
'<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
|
|
@@ -11236,7 +11907,7 @@
|
|
|
11236
11907
|
var fullResult = resultContent;
|
|
11237
11908
|
|
|
11238
11909
|
var expandedHtml = "";
|
|
11239
|
-
var shouldExpand = persistedExpanded === null ?
|
|
11910
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.inlineTools) : persistedExpanded;
|
|
11240
11911
|
if (hasResult) {
|
|
11241
11912
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
11242
11913
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
@@ -11248,16 +11919,23 @@
|
|
|
11248
11919
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
11249
11920
|
}
|
|
11250
11921
|
|
|
11922
|
+
var isTruncated = toolResult && toolResult._truncated === true;
|
|
11923
|
+
|
|
11251
11924
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
11252
11925
|
var extraClass = isError ? 'inline-tool-error-inline' : '';
|
|
11253
11926
|
if (shouldExpand) extraClass += ' inline-tool-open';
|
|
11254
11927
|
|
|
11928
|
+
var truncatedAttrs = isTruncated
|
|
11929
|
+
? 'data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '" '
|
|
11930
|
+
: '';
|
|
11931
|
+
|
|
11255
11932
|
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
11256
11933
|
'data-expand-kind="inline-tool" ' +
|
|
11257
11934
|
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
11258
11935
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
11259
11936
|
'data-preview="' + previewDataAttr + '" ' +
|
|
11260
11937
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
11938
|
+
truncatedAttrs +
|
|
11261
11939
|
'onclick="__inlineToolToggle(this)">' +
|
|
11262
11940
|
'<div class="inline-tool-row">' +
|
|
11263
11941
|
'<span class="inline-tool-status">' + statusIcon + '</span>' +
|
|
@@ -11314,9 +11992,14 @@
|
|
|
11314
11992
|
|
|
11315
11993
|
// Show command preview in header (truncate long commands)
|
|
11316
11994
|
var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
|
|
11317
|
-
var shouldExpand = persistedExpanded === null ?
|
|
11995
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.terminal) : persistedExpanded;
|
|
11318
11996
|
|
|
11319
|
-
|
|
11997
|
+
var termTruncated = toolResult && toolResult._truncated === true;
|
|
11998
|
+
var termTruncAttrs = termTruncated
|
|
11999
|
+
? ' data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '"'
|
|
12000
|
+
: '';
|
|
12001
|
+
|
|
12002
|
+
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '"' + termTruncAttrs + '>' +
|
|
11320
12003
|
'<div class="term-header" onclick="__terminalExpand(this)">' +
|
|
11321
12004
|
statusDot +
|
|
11322
12005
|
'<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
|
|
@@ -11596,10 +12279,12 @@
|
|
|
11596
12279
|
|
|
11597
12280
|
var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11598
12281
|
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
11599
|
-
var
|
|
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;
|
|
11600
12285
|
var collapsedClass = shouldExpand ? "" : " collapsed";
|
|
11601
12286
|
var toggleHtml = '<span class="tool-use-toggle">▼</span>';
|
|
11602
|
-
return '<div class="tool-use-card ' + statusClass + collapsedClass + '" data-expand-kind="tool-card" data-expand-key="' + escapeHtml(expandKey) + '" data-tool-use-id="' + escapeHtml(toolId) + '">' +
|
|
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"' : '') + '>' +
|
|
11603
12288
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
11604
12289
|
'<span class="tool-use-icon">' + headerIcon + '</span>' +
|
|
11605
12290
|
'<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
|
|
@@ -12059,7 +12744,7 @@
|
|
|
12059
12744
|
'</div>';
|
|
12060
12745
|
|
|
12061
12746
|
var bodyHtml = opts.body
|
|
12062
|
-
? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
|
|
12747
|
+
? '<div class="notification-bubble-body">' + escapeHtml(opts.body).replace(/\n/g, '<br>') + '</div>'
|
|
12063
12748
|
: '';
|
|
12064
12749
|
|
|
12065
12750
|
var actionsHtml = opts.actionLabel
|
|
@@ -12125,13 +12810,43 @@
|
|
|
12125
12810
|
|
|
12126
12811
|
// ── Browser Notification API ──
|
|
12127
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
|
+
|
|
12128
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
|
+
}
|
|
12129
12834
|
if (typeof Notification !== "undefined" && Notification.permission === "default") {
|
|
12130
12835
|
Notification.requestPermission();
|
|
12131
12836
|
}
|
|
12132
12837
|
}
|
|
12133
12838
|
|
|
12134
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
|
|
12135
12850
|
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
12136
12851
|
if (!document.hidden) return; // Only notify when tab is in background
|
|
12137
12852
|
try {
|