@co0ontty/wand 1.10.0 → 1.14.3
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 +799 -66
- package/dist/web-ui/content/styles.css +247 -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,56 @@
|
|
|
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="switch-card-list">' +
|
|
1518
|
+
'<label class="switch-card" for="cfg-card-edit">' +
|
|
1519
|
+
'<div class="switch-card-header">' +
|
|
1520
|
+
'<span class="switch-card-title">编辑卡片 (Edit/Write)</span>' +
|
|
1521
|
+
'<input id="cfg-card-edit" type="checkbox" class="switch-toggle" />' +
|
|
1522
|
+
'<span class="switch-slider"></span>' +
|
|
1523
|
+
'</div>' +
|
|
1524
|
+
'<div class="switch-card-desc">文件编辑和写入操作的 diff 视图</div>' +
|
|
1525
|
+
'</label>' +
|
|
1526
|
+
'<label class="switch-card" for="cfg-card-inline">' +
|
|
1527
|
+
'<div class="switch-card-header">' +
|
|
1528
|
+
'<span class="switch-card-title">内联工具 (Read/Glob/Grep)</span>' +
|
|
1529
|
+
'<input id="cfg-card-inline" type="checkbox" class="switch-toggle" />' +
|
|
1530
|
+
'<span class="switch-slider"></span>' +
|
|
1531
|
+
'</div>' +
|
|
1532
|
+
'<div class="switch-card-desc">文件读取、搜索等工具的结果</div>' +
|
|
1533
|
+
'</label>' +
|
|
1534
|
+
'<label class="switch-card" for="cfg-card-terminal">' +
|
|
1535
|
+
'<div class="switch-card-header">' +
|
|
1536
|
+
'<span class="switch-card-title">终端输出 (Bash)</span>' +
|
|
1537
|
+
'<input id="cfg-card-terminal" type="checkbox" class="switch-toggle" />' +
|
|
1538
|
+
'<span class="switch-slider"></span>' +
|
|
1539
|
+
'</div>' +
|
|
1540
|
+
'<div class="switch-card-desc">命令行执行结果</div>' +
|
|
1541
|
+
'</label>' +
|
|
1542
|
+
'<label class="switch-card" for="cfg-card-thinking">' +
|
|
1543
|
+
'<div class="switch-card-header">' +
|
|
1544
|
+
'<span class="switch-card-title">思考过程 (Thinking)</span>' +
|
|
1545
|
+
'<input id="cfg-card-thinking" type="checkbox" class="switch-toggle" />' +
|
|
1546
|
+
'<span class="switch-slider"></span>' +
|
|
1547
|
+
'</div>' +
|
|
1548
|
+
'<div class="switch-card-desc">Claude 的思考过程块</div>' +
|
|
1549
|
+
'</label>' +
|
|
1550
|
+
'<label class="switch-card" for="cfg-card-toolgroup">' +
|
|
1551
|
+
'<div class="switch-card-header">' +
|
|
1552
|
+
'<span class="switch-card-title">工具组</span>' +
|
|
1553
|
+
'<input id="cfg-card-toolgroup" type="checkbox" class="switch-toggle" />' +
|
|
1554
|
+
'<span class="switch-slider"></span>' +
|
|
1555
|
+
'</div>' +
|
|
1556
|
+
'<div class="switch-card-desc">连续同类工具调用的折叠组</div>' +
|
|
1557
|
+
'</label>' +
|
|
1558
|
+
'</div>' +
|
|
1559
|
+
'<button id="save-display-button" class="btn btn-primary btn-block" style="margin-top:16px">保存显示设置</button>' +
|
|
1560
|
+
'<p id="display-message" class="hint hidden"></p>' +
|
|
1561
|
+
'</div>' +
|
|
1432
1562
|
'</div>' +
|
|
1433
1563
|
'</div>' +
|
|
1434
1564
|
'</section>';
|
|
@@ -1856,7 +1986,10 @@
|
|
|
1856
1986
|
} catch (e) {}
|
|
1857
1987
|
applyTerminalScale();
|
|
1858
1988
|
updateScaleLabel();
|
|
1859
|
-
|
|
1989
|
+
// Force refit: font size changed but container dimensions didn't,
|
|
1990
|
+
// so ensureTerminalFit (which resets viewport tracking) is needed
|
|
1991
|
+
// instead of scheduleTerminalResize (which skips when size unchanged).
|
|
1992
|
+
ensureTerminalFit();
|
|
1860
1993
|
}
|
|
1861
1994
|
|
|
1862
1995
|
function applyTerminalScale() {
|
|
@@ -2295,6 +2428,35 @@
|
|
|
2295
2428
|
'</div>';
|
|
2296
2429
|
}
|
|
2297
2430
|
|
|
2431
|
+
function timeAgo(isoString) {
|
|
2432
|
+
if (!isoString) return "";
|
|
2433
|
+
var now = Date.now();
|
|
2434
|
+
var then = new Date(isoString).getTime();
|
|
2435
|
+
var diff = Math.max(0, now - then);
|
|
2436
|
+
var seconds = Math.floor(diff / 1000);
|
|
2437
|
+
if (seconds < 60) return "刚刚";
|
|
2438
|
+
var minutes = Math.floor(seconds / 60);
|
|
2439
|
+
if (minutes < 60) return minutes + "分钟前";
|
|
2440
|
+
var hours = Math.floor(minutes / 60);
|
|
2441
|
+
if (hours < 24) return hours + "小时前";
|
|
2442
|
+
var days = Math.floor(hours / 24);
|
|
2443
|
+
if (days < 30) return days + "天前";
|
|
2444
|
+
return Math.floor(days / 30) + "个月前";
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function elapsedTime(isoString) {
|
|
2448
|
+
if (!isoString) return "";
|
|
2449
|
+
var now = Date.now();
|
|
2450
|
+
var then = new Date(isoString).getTime();
|
|
2451
|
+
var diff = Math.max(0, now - then);
|
|
2452
|
+
var seconds = Math.floor(diff / 1000);
|
|
2453
|
+
var minutes = Math.floor(seconds / 60);
|
|
2454
|
+
var hours = Math.floor(minutes / 60);
|
|
2455
|
+
if (hours > 0) return hours + "h" + (minutes % 60 > 0 ? (minutes % 60) + "m" : "");
|
|
2456
|
+
if (minutes > 0) return minutes + "m";
|
|
2457
|
+
return seconds + "s";
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2298
2460
|
function getSessionStatusLabel(session) {
|
|
2299
2461
|
if (!session) return "";
|
|
2300
2462
|
if (session.archived) return "已归档";
|
|
@@ -2317,29 +2479,55 @@
|
|
|
2317
2479
|
return session.status || "";
|
|
2318
2480
|
}
|
|
2319
2481
|
|
|
2482
|
+
/** Get a human-readable activity description for a running session */
|
|
2483
|
+
function getSessionActivityDesc(session) {
|
|
2484
|
+
if (!session) return "";
|
|
2485
|
+
if (session.permissionBlocked) return "等待你的授权";
|
|
2486
|
+
if (session.status !== "running") return "";
|
|
2487
|
+
// Check WebSocket-delivered currentTask first
|
|
2488
|
+
if (session.id === state.selectedId && state.currentTask && state.currentTask.title) {
|
|
2489
|
+
return state.currentTask.title;
|
|
2490
|
+
}
|
|
2491
|
+
// Fall back to snapshot-delivered currentTaskTitle
|
|
2492
|
+
if (session.currentTaskTitle) return session.currentTaskTitle;
|
|
2493
|
+
return "";
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2496
|
+
/** Get the last meaningful assistant text from messages for notification/display */
|
|
2497
|
+
function getLastAssistantSummary(session) {
|
|
2498
|
+
var msgs = session && session.messages;
|
|
2499
|
+
if (!msgs || msgs.length === 0) return "";
|
|
2500
|
+
for (var i = msgs.length - 1; i >= 0; i--) {
|
|
2501
|
+
var msg = msgs[i];
|
|
2502
|
+
if (msg.role !== "assistant") continue;
|
|
2503
|
+
var blocks = msg.content || [];
|
|
2504
|
+
for (var j = 0; j < blocks.length; j++) {
|
|
2505
|
+
if (blocks[j].type === "text" && blocks[j].text && blocks[j].text.trim()) {
|
|
2506
|
+
var text = blocks[j].text.trim();
|
|
2507
|
+
// Strip markdown formatting for compact display
|
|
2508
|
+
text = text.replace(/^#+\s+/gm, "").replace(/\*\*/g, "").replace(/`/g, "");
|
|
2509
|
+
var firstLine = text.split("\n")[0].trim();
|
|
2510
|
+
return firstLine.slice(0, 100);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return "";
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2320
2517
|
function renderSessionItem(session) {
|
|
2321
2518
|
var activeClass = session.id === state.selectedId ? " active" : "";
|
|
2322
2519
|
var selectedClass = state.sessionsManageMode && state.selectedSessionIds[session.id] ? " selected" : "";
|
|
2323
2520
|
var metaStatus = getSessionStatusLabel(session);
|
|
2324
2521
|
var metaStatusClass = getSessionStatusClass(session);
|
|
2325
|
-
var modeName = session.mode === "full-access" ? "全权限" : session.mode === "default" ? "默认" : session.mode === "native" ? "原生" : session.mode === "auto-edit" ? "自动编辑" : session.mode;
|
|
2326
2522
|
var resumeButton = "";
|
|
2327
|
-
var sessionIdDisplay = "";
|
|
2328
|
-
var recoveryHint = "";
|
|
2329
2523
|
var checkbox = renderManageCheckbox("sessions", session.id, "选择会话 " + session.command);
|
|
2330
2524
|
|
|
2331
2525
|
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
2526
|
if (session.status !== "running" && !state.sessionsManageMode && !isStructuredSession(session)) {
|
|
2335
2527
|
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
2528
|
}
|
|
2337
2529
|
}
|
|
2338
2530
|
|
|
2339
|
-
if (session.autoRecovered) {
|
|
2340
|
-
recoveryHint = '<span class="session-id" title="自动恢复的会话">自动恢复</span>';
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
2531
|
var canOpenMerge = !state.sessionsManageMode && session.worktreeEnabled && session.worktree && session.worktree.branch && session.worktree.path;
|
|
2344
2532
|
var needsCleanup = session.worktreeMergeStatus === "merged" && session.worktreeMergeInfo && session.worktreeMergeInfo.cleanupDone === false;
|
|
2345
2533
|
var mergeDisabled = session.status === "running" || session.worktreeMergeStatus === "merging";
|
|
@@ -2350,23 +2538,50 @@
|
|
|
2350
2538
|
? '<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
2539
|
: "";
|
|
2352
2540
|
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
2541
|
var actionsHtml = '<span class="session-actions">' + resumeButton + mergeButton + deleteButton + '</span>';
|
|
2355
2542
|
|
|
2543
|
+
// Title: summary or command
|
|
2544
|
+
var titleHtml = session.summary
|
|
2545
|
+
? '<div class="session-title">' + escapeHtml(session.summary) + '</div>'
|
|
2546
|
+
: '<div class="session-command">' + escapeHtml(session.resumedFromSessionId ? session.command.replace(/\s+--resume\s+\S+/, '') : session.command) + '</div>';
|
|
2547
|
+
|
|
2548
|
+
// Activity description for running sessions
|
|
2549
|
+
var activityDesc = getSessionActivityDesc(session);
|
|
2550
|
+
var activityHtml = "";
|
|
2551
|
+
if (session.status === "running" && activityDesc) {
|
|
2552
|
+
activityHtml = '<div class="session-activity">' + escapeHtml(activityDesc) + '</div>';
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// Time display
|
|
2556
|
+
var timeDisplay = "";
|
|
2557
|
+
if (session.status === "running") {
|
|
2558
|
+
timeDisplay = '<span class="session-time" title="已运行 ' + escapeHtml(elapsedTime(session.startedAt)) + '">' + escapeHtml(elapsedTime(session.startedAt)) + '</span>';
|
|
2559
|
+
} else if (session.endedAt) {
|
|
2560
|
+
timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.endedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.endedAt)) + '</span>';
|
|
2561
|
+
} else if (session.startedAt) {
|
|
2562
|
+
timeDisplay = '<span class="session-time" title="' + escapeHtml(new Date(session.startedAt).toLocaleString()) + '">' + escapeHtml(timeAgo(session.startedAt)) + '</span>';
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
// Badges: worktree only (removed PTY/Structured and mode badges for cleaner look)
|
|
2566
|
+
var badgesHtml = renderWorktreeBadge(session);
|
|
2567
|
+
|
|
2568
|
+
// Recovery hint
|
|
2569
|
+
var recoveryHtml = session.autoRecovered ? '<span class="session-recovery-hint">自动恢复</span>' : '';
|
|
2570
|
+
|
|
2356
2571
|
return '<div class="session-item' + activeClass + selectedClass + '" data-session-id="' + session.id + '" role="button" tabindex="0">' +
|
|
2357
2572
|
'<div class="session-item-content">' +
|
|
2358
2573
|
'<div class="session-item-row">' +
|
|
2359
2574
|
checkbox +
|
|
2360
2575
|
'<div class="session-main">' +
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2576
|
+
'<div class="session-title-row">' +
|
|
2577
|
+
titleHtml +
|
|
2578
|
+
timeDisplay +
|
|
2579
|
+
'</div>' +
|
|
2580
|
+
activityHtml +
|
|
2364
2581
|
'<div class="session-meta">' +
|
|
2365
|
-
modeBadge +
|
|
2366
|
-
'<span>' + escapeHtml(modeName) + '</span>' +
|
|
2367
2582
|
'<span class="session-status ' + metaStatusClass + '">' + escapeHtml(metaStatus) + '</span>' +
|
|
2368
|
-
|
|
2369
|
-
|
|
2583
|
+
badgesHtml +
|
|
2584
|
+
recoveryHtml +
|
|
2370
2585
|
'</div>' +
|
|
2371
2586
|
'</div>' +
|
|
2372
2587
|
actionsHtml +
|
|
@@ -2525,6 +2740,8 @@
|
|
|
2525
2740
|
'<p class="field-hint">留空则使用上方目录,支持路径自动补全。</p>' +
|
|
2526
2741
|
'<div id="recent-paths-bubbles" class="recent-paths-bubbles"></div>' +
|
|
2527
2742
|
'</div>' +
|
|
2743
|
+
'</div>' +
|
|
2744
|
+
'<div class="modal-footer">' +
|
|
2528
2745
|
'<button id="run-button" class="btn btn-primary btn-block">启动会话</button>' +
|
|
2529
2746
|
'<p id="modal-error" class="error-message hidden"></p>' +
|
|
2530
2747
|
'</div>' +
|
|
@@ -2533,11 +2750,53 @@
|
|
|
2533
2750
|
}
|
|
2534
2751
|
|
|
2535
2752
|
// Global toggle function for tool card headers — called via onclick attribute
|
|
2753
|
+
// Lazy-load tool content for truncated results
|
|
2754
|
+
function __fetchToolContent(toolUseId, callback) {
|
|
2755
|
+
if (!state.selectedId || !toolUseId) return;
|
|
2756
|
+
var cacheKey = state.selectedId + ":" + toolUseId;
|
|
2757
|
+
if (state.toolContentCache[cacheKey]) {
|
|
2758
|
+
callback(null, state.toolContentCache[cacheKey]);
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
fetch("/api/sessions/" + encodeURIComponent(state.selectedId) + "/tool-content/" + encodeURIComponent(toolUseId), { credentials: "same-origin" })
|
|
2762
|
+
.then(function(res) { return res.json(); })
|
|
2763
|
+
.then(function(data) {
|
|
2764
|
+
if (data.error) {
|
|
2765
|
+
callback(data.error, null);
|
|
2766
|
+
} else {
|
|
2767
|
+
state.toolContentCache[cacheKey] = data;
|
|
2768
|
+
callback(null, data);
|
|
2769
|
+
}
|
|
2770
|
+
})
|
|
2771
|
+
.catch(function() {
|
|
2772
|
+
callback("加载失败", null);
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2536
2776
|
window.__tcToggle = function(e, headerEl) {
|
|
2537
2777
|
var card = headerEl.closest(".tool-use-card");
|
|
2538
2778
|
if (card) {
|
|
2779
|
+
var wasCollapsed = card.classList.contains("collapsed");
|
|
2539
2780
|
card.classList.toggle("collapsed");
|
|
2540
2781
|
persistElementExpandState(card, "tool-card");
|
|
2782
|
+
// Lazy-load truncated content on expand
|
|
2783
|
+
if (wasCollapsed && card.dataset.truncated === "true" && card.dataset.loaded !== "true") {
|
|
2784
|
+
var toolUseId = card.dataset.toolUseId;
|
|
2785
|
+
var resultDiv = card.querySelector(".tool-use-result");
|
|
2786
|
+
if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2787
|
+
card.dataset.loaded = "loading";
|
|
2788
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2789
|
+
if (err) {
|
|
2790
|
+
if (resultDiv) resultDiv.innerHTML = '<div class="tool-content-error" onclick="__tcToggle(null, card.querySelector(\'.tool-use-header\'))">加载失败,点击重试</div>';
|
|
2791
|
+
card.dataset.loaded = "";
|
|
2792
|
+
} else {
|
|
2793
|
+
card.dataset.truncated = "false";
|
|
2794
|
+
card.dataset.loaded = "true";
|
|
2795
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2796
|
+
if (resultDiv) resultDiv.innerHTML = '<pre class="tool-use-result-content">' + escapeHtml(content) + '</pre>';
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2541
2800
|
}
|
|
2542
2801
|
if (e) { e.preventDefault(); e.stopPropagation(); }
|
|
2543
2802
|
};
|
|
@@ -2576,6 +2835,24 @@
|
|
|
2576
2835
|
statusSpan.textContent = "✓";
|
|
2577
2836
|
}
|
|
2578
2837
|
}
|
|
2838
|
+
// Lazy-load truncated content on expand
|
|
2839
|
+
if (expanded && el.dataset.truncated === "true" && el.dataset.loaded !== "true") {
|
|
2840
|
+
var toolUseId = el.dataset.toolUseId;
|
|
2841
|
+
if (body) body.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2842
|
+
el.dataset.loaded = "loading";
|
|
2843
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2844
|
+
if (err) {
|
|
2845
|
+
if (body) body.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
|
|
2846
|
+
el.dataset.loaded = "";
|
|
2847
|
+
} else {
|
|
2848
|
+
el.dataset.truncated = "false";
|
|
2849
|
+
el.dataset.loaded = "true";
|
|
2850
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2851
|
+
el.dataset.result = content;
|
|
2852
|
+
if (body) body.innerHTML = '<div class="inline-tool-result">' + formatInlineResult(content, "") + '</div>';
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2579
2856
|
persistElementExpandState(el, "inline-tool");
|
|
2580
2857
|
};
|
|
2581
2858
|
// Toggle function for terminal tool blocks
|
|
@@ -2590,6 +2867,32 @@
|
|
|
2590
2867
|
var toggleIcon = el.querySelector(".term-toggle-icon");
|
|
2591
2868
|
if (toggleIcon) toggleIcon.textContent = isHidden ? "▼" : "▶";
|
|
2592
2869
|
persistElementExpandState(container, "terminal");
|
|
2870
|
+
// Lazy-load truncated content on expand
|
|
2871
|
+
if (isHidden && container.dataset.truncated === "true" && container.dataset.loaded !== "true") {
|
|
2872
|
+
var toolUseId = container.dataset.toolUseId;
|
|
2873
|
+
var termOutput = body.querySelector(".term-output");
|
|
2874
|
+
if (termOutput) termOutput.innerHTML = '<div class="tool-content-loading">加载中…</div>';
|
|
2875
|
+
container.dataset.loaded = "loading";
|
|
2876
|
+
__fetchToolContent(toolUseId, function(err, data) {
|
|
2877
|
+
if (err) {
|
|
2878
|
+
if (termOutput) termOutput.innerHTML = '<div class="tool-content-error">加载失败,点击重试</div>';
|
|
2879
|
+
container.dataset.loaded = "";
|
|
2880
|
+
} else {
|
|
2881
|
+
container.dataset.truncated = "false";
|
|
2882
|
+
container.dataset.loaded = "true";
|
|
2883
|
+
var content = typeof data.content === "string" ? data.content : JSON.stringify(data.content);
|
|
2884
|
+
if (termOutput) {
|
|
2885
|
+
var lines = content.split("\n");
|
|
2886
|
+
var html = "";
|
|
2887
|
+
for (var i = 0; i < lines.length; i++) {
|
|
2888
|
+
if (!lines[i] && i === lines.length - 1) continue;
|
|
2889
|
+
html += '<div class="term-line">' + escapeHtml(lines[i]) + '</div>';
|
|
2890
|
+
}
|
|
2891
|
+
termOutput.innerHTML = html;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2593
2896
|
}
|
|
2594
2897
|
};
|
|
2595
2898
|
// Update streaming thinking content (called from WebSocket handler)
|
|
@@ -2860,6 +3163,30 @@
|
|
|
2860
3163
|
}
|
|
2861
3164
|
var saveConfigBtn = document.getElementById("save-config-button");
|
|
2862
3165
|
if (saveConfigBtn) saveConfigBtn.addEventListener("click", saveConfigSettings);
|
|
3166
|
+
var saveDisplayBtn = document.getElementById("save-display-button");
|
|
3167
|
+
if (saveDisplayBtn) saveDisplayBtn.addEventListener("click", saveDisplaySettings);
|
|
3168
|
+
// App icon picker (APK only)
|
|
3169
|
+
var appIconPicker = document.getElementById("app-icon-picker");
|
|
3170
|
+
if (appIconPicker) {
|
|
3171
|
+
var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
|
|
3172
|
+
for (var ai = 0; ai < appIconOpts.length; ai++) {
|
|
3173
|
+
appIconOpts[ai].addEventListener("click", function() {
|
|
3174
|
+
var iconName = this.getAttribute("data-icon");
|
|
3175
|
+
if (!iconName || typeof WandNative === "undefined" || typeof WandNative.setAppIcon !== "function") return;
|
|
3176
|
+
try {
|
|
3177
|
+
WandNative.setAppIcon(iconName);
|
|
3178
|
+
_updateAppIconSelection(iconName);
|
|
3179
|
+
var msgEl = document.getElementById("app-icon-message");
|
|
3180
|
+
if (msgEl) {
|
|
3181
|
+
msgEl.textContent = "图标已切换,返回桌面后生效";
|
|
3182
|
+
msgEl.style.color = "var(--success)";
|
|
3183
|
+
msgEl.classList.remove("hidden");
|
|
3184
|
+
setTimeout(function() { msgEl.classList.add("hidden"); }, 3000);
|
|
3185
|
+
}
|
|
3186
|
+
} catch (_e) {}
|
|
3187
|
+
});
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
2863
3190
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
2864
3191
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
2865
3192
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
@@ -2868,6 +3195,11 @@
|
|
|
2868
3195
|
if (doUpdateBtn) doUpdateBtn.addEventListener("click", performUpdate);
|
|
2869
3196
|
var doRestartBtn = document.getElementById("do-restart-button");
|
|
2870
3197
|
if (doRestartBtn) doRestartBtn.addEventListener("click", performSettingsRestart);
|
|
3198
|
+
var copyConnectCodeBtn = document.getElementById("copy-connect-code-button");
|
|
3199
|
+
if (copyConnectCodeBtn) copyConnectCodeBtn.addEventListener("click", function() {
|
|
3200
|
+
var text = document.getElementById("android-connect-code");
|
|
3201
|
+
if (text) copyToClipboard(text.textContent, copyConnectCodeBtn);
|
|
3202
|
+
});
|
|
2871
3203
|
// Notification preferences
|
|
2872
3204
|
var notifSoundEl = document.getElementById("cfg-notif-sound");
|
|
2873
3205
|
if (notifSoundEl) {
|
|
@@ -2890,7 +3222,13 @@
|
|
|
2890
3222
|
// Browser notification section
|
|
2891
3223
|
var notifRequestBtn = document.getElementById("notification-request-btn");
|
|
2892
3224
|
if (notifRequestBtn) notifRequestBtn.addEventListener("click", function() {
|
|
2893
|
-
if (
|
|
3225
|
+
if (_hasNativeBridge) {
|
|
3226
|
+
window._onNativePermissionResult = function() {
|
|
3227
|
+
updateNotificationStatus();
|
|
3228
|
+
delete window._onNativePermissionResult;
|
|
3229
|
+
};
|
|
3230
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
3231
|
+
} else if (typeof Notification !== "undefined") {
|
|
2894
3232
|
Notification.requestPermission().then(function() { updateNotificationStatus(); });
|
|
2895
3233
|
}
|
|
2896
3234
|
});
|
|
@@ -4664,6 +5002,15 @@
|
|
|
4664
5002
|
});
|
|
4665
5003
|
}
|
|
4666
5004
|
|
|
5005
|
+
var _sessionListUpdateTimer = null;
|
|
5006
|
+
function scheduleSessionListUpdate() {
|
|
5007
|
+
if (_sessionListUpdateTimer) return;
|
|
5008
|
+
_sessionListUpdateTimer = setTimeout(function() {
|
|
5009
|
+
_sessionListUpdateTimer = null;
|
|
5010
|
+
updateSessionsList();
|
|
5011
|
+
}, 200);
|
|
5012
|
+
}
|
|
5013
|
+
|
|
4667
5014
|
function updateSessionsList() {
|
|
4668
5015
|
var listEl = document.getElementById("sessions-list");
|
|
4669
5016
|
var countEl = document.getElementById("session-count");
|
|
@@ -4792,6 +5139,8 @@
|
|
|
4792
5139
|
}
|
|
4793
5140
|
state.selectedId = id;
|
|
4794
5141
|
persistSelectedId();
|
|
5142
|
+
// Clear tool content cache on session switch
|
|
5143
|
+
state.toolContentCache = {};
|
|
4795
5144
|
// Clear queued inputs from the previous session to prevent cross-session leaks
|
|
4796
5145
|
state.messageQueue = [];
|
|
4797
5146
|
state.pendingMessages = [];
|
|
@@ -5119,6 +5468,10 @@
|
|
|
5119
5468
|
if (soundEl) soundEl.checked = state.notifSound;
|
|
5120
5469
|
if (bubbleEl) bubbleEl.checked = state.notifBubble;
|
|
5121
5470
|
updateNotificationStatus();
|
|
5471
|
+
// Load current app icon selection (APK only)
|
|
5472
|
+
if (typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function") {
|
|
5473
|
+
try { _updateAppIconSelection(WandNative.getAppIcon() || "shorthair"); } catch (_e) {}
|
|
5474
|
+
}
|
|
5122
5475
|
}
|
|
5123
5476
|
}
|
|
5124
5477
|
|
|
@@ -5210,6 +5563,46 @@
|
|
|
5210
5563
|
}
|
|
5211
5564
|
}
|
|
5212
5565
|
|
|
5566
|
+
function copyToClipboard(text, triggerBtn) {
|
|
5567
|
+
if (!text) return;
|
|
5568
|
+
navigator.clipboard.writeText(text).then(function() {
|
|
5569
|
+
if (triggerBtn) {
|
|
5570
|
+
var orig = triggerBtn.textContent;
|
|
5571
|
+
triggerBtn.textContent = "已复制";
|
|
5572
|
+
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
5573
|
+
}
|
|
5574
|
+
}).catch(function() {
|
|
5575
|
+
// Fallback for non-secure contexts
|
|
5576
|
+
var ta = document.createElement("textarea");
|
|
5577
|
+
ta.value = text;
|
|
5578
|
+
ta.style.position = "fixed";
|
|
5579
|
+
ta.style.opacity = "0";
|
|
5580
|
+
document.body.appendChild(ta);
|
|
5581
|
+
ta.select();
|
|
5582
|
+
document.execCommand("copy");
|
|
5583
|
+
document.body.removeChild(ta);
|
|
5584
|
+
if (triggerBtn) {
|
|
5585
|
+
var orig = triggerBtn.textContent;
|
|
5586
|
+
triggerBtn.textContent = "已复制";
|
|
5587
|
+
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
5588
|
+
}
|
|
5589
|
+
});
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
function formatBytes(value) {
|
|
5593
|
+
if (typeof value !== "number" || !isFinite(value) || value < 0) return "-";
|
|
5594
|
+
if (value < 1024) return value + " B";
|
|
5595
|
+
var units = ["KB", "MB", "GB", "TB"];
|
|
5596
|
+
var size = value / 1024;
|
|
5597
|
+
var unitIndex = 0;
|
|
5598
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
5599
|
+
size = size / 1024;
|
|
5600
|
+
unitIndex += 1;
|
|
5601
|
+
}
|
|
5602
|
+
var display = size >= 10 ? size.toFixed(0) : size.toFixed(1);
|
|
5603
|
+
return display + " " + units[unitIndex];
|
|
5604
|
+
}
|
|
5605
|
+
|
|
5213
5606
|
function loadSettingsData() {
|
|
5214
5607
|
fetch("/api/settings", { credentials: "same-origin" })
|
|
5215
5608
|
.then(function(res) { return res.json(); })
|
|
@@ -5236,6 +5629,97 @@
|
|
|
5236
5629
|
}
|
|
5237
5630
|
}
|
|
5238
5631
|
|
|
5632
|
+
// ── Android APK version display ──
|
|
5633
|
+
var apkSection = document.getElementById("android-apk-section");
|
|
5634
|
+
var apkCurrentRow = document.getElementById("android-apk-current-row");
|
|
5635
|
+
var apkCurrentEl = document.getElementById("settings-android-apk-current");
|
|
5636
|
+
var apkGithubRow = document.getElementById("android-apk-github-row");
|
|
5637
|
+
var apkGithubEl = document.getElementById("settings-android-apk-github");
|
|
5638
|
+
var apkGithubBtn = document.getElementById("download-github-apk-btn");
|
|
5639
|
+
var apkLocalRow = document.getElementById("android-apk-local-row");
|
|
5640
|
+
var apkLocalEl = document.getElementById("settings-android-apk-local");
|
|
5641
|
+
var apkLocalBtn = document.getElementById("download-local-apk-btn");
|
|
5642
|
+
var apkMessageEl = document.getElementById("android-apk-message");
|
|
5643
|
+
var androidApk = data.androidApk || {};
|
|
5644
|
+
var isInApk = !!_apkVersion;
|
|
5645
|
+
|
|
5646
|
+
if (isInApk) {
|
|
5647
|
+
// ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
|
|
5648
|
+
if (apkCurrentRow && apkCurrentEl) {
|
|
5649
|
+
apkCurrentEl.textContent = "v" + _apkVersion;
|
|
5650
|
+
apkCurrentRow.classList.remove("hidden");
|
|
5651
|
+
}
|
|
5652
|
+
// 线上版本
|
|
5653
|
+
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
5654
|
+
var ghLabel = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
|
|
5655
|
+
if (typeof androidApk.github.size === "number") ghLabel += " · " + formatBytes(androidApk.github.size);
|
|
5656
|
+
apkGithubEl.textContent = ghLabel;
|
|
5657
|
+
apkGithubRow.classList.remove("hidden");
|
|
5658
|
+
if (apkGithubBtn) {
|
|
5659
|
+
apkGithubBtn.textContent = "下载安装";
|
|
5660
|
+
apkGithubBtn.classList.remove("hidden");
|
|
5661
|
+
apkGithubBtn.onclick = function() {
|
|
5662
|
+
try {
|
|
5663
|
+
WandNative.downloadUpdate(androidApk.github.downloadUrl, androidApk.github.fileName || "wand-update.apk", "github");
|
|
5664
|
+
} catch (e) {
|
|
5665
|
+
alert("调用下载失败: " + e.message);
|
|
5666
|
+
}
|
|
5667
|
+
};
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
5670
|
+
// 本地版本
|
|
5671
|
+
if (androidApk.local && apkLocalRow && apkLocalEl) {
|
|
5672
|
+
var lcLabel = androidApk.local.version ? ("v" + androidApk.local.version) : androidApk.local.fileName;
|
|
5673
|
+
if (typeof androidApk.local.size === "number") lcLabel += " · " + formatBytes(androidApk.local.size);
|
|
5674
|
+
apkLocalEl.textContent = lcLabel;
|
|
5675
|
+
apkLocalRow.classList.remove("hidden");
|
|
5676
|
+
if (apkLocalBtn) {
|
|
5677
|
+
apkLocalBtn.textContent = "下载安装";
|
|
5678
|
+
apkLocalBtn.classList.remove("hidden");
|
|
5679
|
+
apkLocalBtn.onclick = function() {
|
|
5680
|
+
try {
|
|
5681
|
+
WandNative.downloadUpdate(androidApk.local.downloadUrl, androidApk.local.fileName || "wand-update.apk", "local");
|
|
5682
|
+
} catch (e) {
|
|
5683
|
+
alert("调用下载失败: " + e.message);
|
|
5684
|
+
}
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
// 都没有时
|
|
5689
|
+
if (!androidApk.github && !androidApk.local && apkMessageEl) {
|
|
5690
|
+
apkMessageEl.textContent = "暂无可用更新";
|
|
5691
|
+
apkMessageEl.classList.remove("hidden");
|
|
5692
|
+
}
|
|
5693
|
+
} else {
|
|
5694
|
+
// ── 浏览器模式:只显示线上版本 + 下载按钮 ──
|
|
5695
|
+
if (androidApk.github && apkGithubRow && apkGithubEl) {
|
|
5696
|
+
var ghLabel2 = androidApk.github.version ? ("v" + androidApk.github.version) : androidApk.github.fileName;
|
|
5697
|
+
if (typeof androidApk.github.size === "number") ghLabel2 += " · " + formatBytes(androidApk.github.size);
|
|
5698
|
+
apkGithubEl.textContent = ghLabel2;
|
|
5699
|
+
apkGithubRow.classList.remove("hidden");
|
|
5700
|
+
if (apkGithubBtn) {
|
|
5701
|
+
apkGithubBtn.textContent = "下载";
|
|
5702
|
+
apkGithubBtn.classList.remove("hidden");
|
|
5703
|
+
apkGithubBtn.onclick = function() {
|
|
5704
|
+
window.open(androidApk.github.downloadUrl, "_blank");
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
} else if (apkMessageEl) {
|
|
5708
|
+
apkMessageEl.textContent = "暂未提供";
|
|
5709
|
+
apkMessageEl.classList.remove("hidden");
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
|
|
5713
|
+
// App connect code (encrypted)
|
|
5714
|
+
var connectCodeEl = document.getElementById("android-connect-code");
|
|
5715
|
+
if (connectCodeEl) {
|
|
5716
|
+
connectCodeEl.textContent = "加载中...";
|
|
5717
|
+
fetch("/api/app-connect-code").then(function(r) { return r.json(); }).then(function(d) {
|
|
5718
|
+
if (d.code) connectCodeEl.textContent = d.code;
|
|
5719
|
+
else connectCodeEl.textContent = "生成失败";
|
|
5720
|
+
}).catch(function() { connectCodeEl.textContent = "获取失败"; });
|
|
5721
|
+
}
|
|
5722
|
+
|
|
5239
5723
|
// Config fields
|
|
5240
5724
|
var cfg = data.config || {};
|
|
5241
5725
|
var hostEl = document.getElementById("cfg-host");
|
|
@@ -5274,6 +5758,19 @@
|
|
|
5274
5758
|
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
5759
|
presetsList.innerHTML = html;
|
|
5276
5760
|
}
|
|
5761
|
+
|
|
5762
|
+
// Card expand defaults
|
|
5763
|
+
var cd = cfg.cardDefaults || {};
|
|
5764
|
+
var cdEditEl = document.getElementById("cfg-card-edit");
|
|
5765
|
+
var cdInlineEl = document.getElementById("cfg-card-inline");
|
|
5766
|
+
var cdTerminalEl = document.getElementById("cfg-card-terminal");
|
|
5767
|
+
var cdThinkingEl = document.getElementById("cfg-card-thinking");
|
|
5768
|
+
var cdToolgroupEl = document.getElementById("cfg-card-toolgroup");
|
|
5769
|
+
if (cdEditEl) cdEditEl.checked = cd.editCards === true;
|
|
5770
|
+
if (cdInlineEl) cdInlineEl.checked = cd.inlineTools === true;
|
|
5771
|
+
if (cdTerminalEl) cdTerminalEl.checked = cd.terminal === true;
|
|
5772
|
+
if (cdThinkingEl) cdThinkingEl.checked = cd.thinking === true;
|
|
5773
|
+
if (cdToolgroupEl) cdToolgroupEl.checked = cd.toolGroup === true;
|
|
5277
5774
|
})
|
|
5278
5775
|
.catch(function() {});
|
|
5279
5776
|
}
|
|
@@ -5320,6 +5817,52 @@
|
|
|
5320
5817
|
});
|
|
5321
5818
|
}
|
|
5322
5819
|
|
|
5820
|
+
function saveDisplaySettings() {
|
|
5821
|
+
var msgEl = document.getElementById("display-message");
|
|
5822
|
+
if (msgEl) { msgEl.classList.add("hidden"); msgEl.textContent = ""; }
|
|
5823
|
+
|
|
5824
|
+
var body = {
|
|
5825
|
+
cardDefaults: {
|
|
5826
|
+
editCards: !!(document.getElementById("cfg-card-edit") || {}).checked,
|
|
5827
|
+
inlineTools: !!(document.getElementById("cfg-card-inline") || {}).checked,
|
|
5828
|
+
terminal: !!(document.getElementById("cfg-card-terminal") || {}).checked,
|
|
5829
|
+
thinking: !!(document.getElementById("cfg-card-thinking") || {}).checked,
|
|
5830
|
+
toolGroup: !!(document.getElementById("cfg-card-toolgroup") || {}).checked,
|
|
5831
|
+
}
|
|
5832
|
+
};
|
|
5833
|
+
|
|
5834
|
+
fetch("/api/settings/config", {
|
|
5835
|
+
method: "POST",
|
|
5836
|
+
headers: { "Content-Type": "application/json" },
|
|
5837
|
+
credentials: "same-origin",
|
|
5838
|
+
body: JSON.stringify(body)
|
|
5839
|
+
})
|
|
5840
|
+
.then(function(res) { return res.json(); })
|
|
5841
|
+
.then(function(data) {
|
|
5842
|
+
if (msgEl) {
|
|
5843
|
+
if (data.error) {
|
|
5844
|
+
msgEl.textContent = data.error;
|
|
5845
|
+
msgEl.style.color = "var(--error)";
|
|
5846
|
+
} else {
|
|
5847
|
+
msgEl.textContent = "显示设置已保存";
|
|
5848
|
+
msgEl.style.color = "var(--success)";
|
|
5849
|
+
}
|
|
5850
|
+
msgEl.classList.remove("hidden");
|
|
5851
|
+
}
|
|
5852
|
+
// Update local config so card defaults take effect immediately
|
|
5853
|
+
if (!data.error && state.config) {
|
|
5854
|
+
state.config.cardDefaults = body.cardDefaults;
|
|
5855
|
+
}
|
|
5856
|
+
})
|
|
5857
|
+
.catch(function() {
|
|
5858
|
+
if (msgEl) {
|
|
5859
|
+
msgEl.textContent = "保存失败。";
|
|
5860
|
+
msgEl.style.color = "var(--error)";
|
|
5861
|
+
msgEl.classList.remove("hidden");
|
|
5862
|
+
}
|
|
5863
|
+
});
|
|
5864
|
+
}
|
|
5865
|
+
|
|
5323
5866
|
function uploadCertificates() {
|
|
5324
5867
|
var keyFile = document.getElementById("cert-key-file");
|
|
5325
5868
|
var certFile = document.getElementById("cert-cert-file");
|
|
@@ -5459,6 +6002,16 @@
|
|
|
5459
6002
|
|
|
5460
6003
|
// ── Notification Settings Helpers ──
|
|
5461
6004
|
|
|
6005
|
+
function _updateAppIconSelection(activeIcon) {
|
|
6006
|
+
var opts = document.querySelectorAll(".app-icon-option");
|
|
6007
|
+
for (var i = 0; i < opts.length; i++) {
|
|
6008
|
+
var preview = opts[i].querySelector(".app-icon-preview");
|
|
6009
|
+
if (preview) {
|
|
6010
|
+
preview.style.borderColor = opts[i].getAttribute("data-icon") === activeIcon ? "var(--accent)" : "transparent";
|
|
6011
|
+
}
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
|
|
5462
6015
|
function updateNotificationStatus() {
|
|
5463
6016
|
var statusEl = document.getElementById("notification-permission-status");
|
|
5464
6017
|
var requestBtn = document.getElementById("notification-request-btn");
|
|
@@ -5466,15 +6019,20 @@
|
|
|
5466
6019
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
5467
6020
|
if (!statusEl) return;
|
|
5468
6021
|
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
if (
|
|
5474
|
-
|
|
6022
|
+
// Determine permission state: native bridge or browser API
|
|
6023
|
+
var perm = _getNativePermission();
|
|
6024
|
+
if (perm === null) {
|
|
6025
|
+
// No native bridge — fall back to browser Notification API
|
|
6026
|
+
if (typeof Notification === "undefined") {
|
|
6027
|
+
statusEl.textContent = "\u4e0d\u652f\u6301";
|
|
6028
|
+
statusEl.style.color = "var(--fg-muted)";
|
|
6029
|
+
if (requestBtn) requestBtn.classList.add("hidden");
|
|
6030
|
+
if (resetBtn) resetBtn.classList.add("hidden");
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
perm = Notification.permission;
|
|
5475
6034
|
}
|
|
5476
6035
|
|
|
5477
|
-
var perm = Notification.permission;
|
|
5478
6036
|
if (perm === "granted") {
|
|
5479
6037
|
statusEl.textContent = "\u5df2\u6388\u6743 \u2713";
|
|
5480
6038
|
statusEl.style.color = "var(--success)";
|
|
@@ -5495,6 +6053,28 @@
|
|
|
5495
6053
|
|
|
5496
6054
|
function resetNotificationPermission() {
|
|
5497
6055
|
var testMsgEl = document.getElementById("notification-test-message");
|
|
6056
|
+
|
|
6057
|
+
// Native bridge path — trigger Android system permission dialog
|
|
6058
|
+
if (_hasNativeBridge) {
|
|
6059
|
+
// Listen for permission result callback from native
|
|
6060
|
+
window._onNativePermissionResult = function(result) {
|
|
6061
|
+
updateNotificationStatus();
|
|
6062
|
+
if (testMsgEl) {
|
|
6063
|
+
if (result === "granted") {
|
|
6064
|
+
testMsgEl.textContent = "\u2713 \u5df2\u6388\u6743";
|
|
6065
|
+
testMsgEl.style.color = "var(--success)";
|
|
6066
|
+
} else {
|
|
6067
|
+
testMsgEl.textContent = "\u2717 \u672a\u6388\u6743\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f Wand \u7684\u901a\u77e5\u6743\u9650";
|
|
6068
|
+
testMsgEl.style.color = "var(--danger)";
|
|
6069
|
+
}
|
|
6070
|
+
testMsgEl.classList.remove("hidden");
|
|
6071
|
+
}
|
|
6072
|
+
delete window._onNativePermissionResult;
|
|
6073
|
+
};
|
|
6074
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
6075
|
+
return;
|
|
6076
|
+
}
|
|
6077
|
+
|
|
5498
6078
|
if (typeof Notification === "undefined") return;
|
|
5499
6079
|
|
|
5500
6080
|
// Always call requestPermission — this triggers the browser's native
|
|
@@ -5550,9 +6130,44 @@
|
|
|
5550
6130
|
});
|
|
5551
6131
|
results.push(bubbleEnabled ? "\u2713 \u5e94\u7528\u5185\u6c14\u6ce1" : "\u2013 \u5e94\u7528\u5185\u6c14\u6ce1\uff08\u5df2\u5173\u95ed\uff09");
|
|
5552
6132
|
|
|
5553
|
-
// 3. Test browser
|
|
6133
|
+
// 3. Test system notification (native bridge or browser API)
|
|
6134
|
+
if (_hasNativeBridge) {
|
|
6135
|
+
var nativePerm = _getNativePermission();
|
|
6136
|
+
if (nativePerm === "granted") {
|
|
6137
|
+
try {
|
|
6138
|
+
WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
|
|
6139
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
|
|
6140
|
+
} catch (_e) {
|
|
6141
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
|
|
6142
|
+
}
|
|
6143
|
+
} else if (nativePerm === "denied") {
|
|
6144
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff0c\u8bf7\u5728\u7cfb\u7edf\u8bbe\u7f6e\u4e2d\u5f00\u542f\uff09");
|
|
6145
|
+
} else {
|
|
6146
|
+
// "default" — request permission, then report
|
|
6147
|
+
window._onNativePermissionResult = function(result) {
|
|
6148
|
+
updateNotificationStatus();
|
|
6149
|
+
if (result === "granted") {
|
|
6150
|
+
try {
|
|
6151
|
+
WandNative.sendNotification("Wand \u6d4b\u8bd5\u901a\u77e5", "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002", "wand-test");
|
|
6152
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
6153
|
+
} catch (_e2) {
|
|
6154
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff09");
|
|
6155
|
+
}
|
|
6156
|
+
} else {
|
|
6157
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
6158
|
+
}
|
|
6159
|
+
showTestResults(testMsgEl, results);
|
|
6160
|
+
delete window._onNativePermissionResult;
|
|
6161
|
+
};
|
|
6162
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
6163
|
+
return; // async — results shown in callback
|
|
6164
|
+
}
|
|
6165
|
+
showTestResults(testMsgEl, results);
|
|
6166
|
+
return;
|
|
6167
|
+
}
|
|
6168
|
+
|
|
5554
6169
|
if (typeof Notification === "undefined") {
|
|
5555
|
-
results.push("\u2013 \
|
|
6170
|
+
results.push("\u2013 \u7cfb\u7edf\u901a\u77e5\uff08\u4e0d\u652f\u6301\uff09");
|
|
5556
6171
|
showTestResults(testMsgEl, results);
|
|
5557
6172
|
return;
|
|
5558
6173
|
}
|
|
@@ -5561,27 +6176,27 @@
|
|
|
5561
6176
|
if (perm === "granted") {
|
|
5562
6177
|
try {
|
|
5563
6178
|
var n = new Notification("Wand \u6d4b\u8bd5\u901a\u77e5", {
|
|
5564
|
-
body: "\
|
|
6179
|
+
body: "\u7cfb\u7edf\u901a\u77e5\u5df2\u6b63\u5e38\u5de5\u4f5c\u3002",
|
|
5565
6180
|
icon: "/favicon.ico",
|
|
5566
6181
|
tag: "wand-test",
|
|
5567
6182
|
});
|
|
5568
6183
|
setTimeout(function() { n.close(); }, 5000);
|
|
5569
|
-
results.push("\u2713 \
|
|
6184
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5");
|
|
5570
6185
|
} catch (_e) {
|
|
5571
|
-
results.push("\u2717 \
|
|
6186
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u53d1\u9001\u5931\u8d25\uff0c\u53ef\u80fd\u9700\u8981 HTTPS\uff09");
|
|
5572
6187
|
}
|
|
5573
6188
|
showTestResults(testMsgEl, results);
|
|
5574
6189
|
} else if (perm === "denied") {
|
|
5575
|
-
results.push("\u2717 \
|
|
6190
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u62d2\u7edd\uff09");
|
|
5576
6191
|
showTestResults(testMsgEl, results);
|
|
5577
6192
|
} else {
|
|
5578
6193
|
// "default" — try requesting
|
|
5579
6194
|
Notification.requestPermission().then(function(result) {
|
|
5580
6195
|
updateNotificationStatus();
|
|
5581
6196
|
if (result === "granted") {
|
|
5582
|
-
results.push("\u2713 \
|
|
6197
|
+
results.push("\u2713 \u7cfb\u7edf\u901a\u77e5\uff08\u5df2\u6388\u6743\uff09");
|
|
5583
6198
|
} else {
|
|
5584
|
-
results.push("\u2717 \
|
|
6199
|
+
results.push("\u2717 \u7cfb\u7edf\u901a\u77e5\uff08\u672a\u6388\u6743\uff09");
|
|
5585
6200
|
}
|
|
5586
6201
|
showTestResults(testMsgEl, results);
|
|
5587
6202
|
});
|
|
@@ -7837,6 +8452,13 @@
|
|
|
7837
8452
|
resetInputPanelViewportSpacing();
|
|
7838
8453
|
setTimeout(function() {
|
|
7839
8454
|
window.scrollTo(0, 0);
|
|
8455
|
+
// On mobile, force terminal refit + scroll after keyboard dismissal.
|
|
8456
|
+
// The container height restores but xterm needs an explicit refit to
|
|
8457
|
+
// fill the expanded space, and the scroll position needs resetting.
|
|
8458
|
+
if (isTouchDevice()) {
|
|
8459
|
+
ensureTerminalFit();
|
|
8460
|
+
maybeScrollTerminalToBottom("force");
|
|
8461
|
+
}
|
|
7840
8462
|
}, 100);
|
|
7841
8463
|
}
|
|
7842
8464
|
|
|
@@ -8562,6 +9184,15 @@
|
|
|
8562
9184
|
syncInputBoxScroll(inputBox);
|
|
8563
9185
|
}
|
|
8564
9186
|
|
|
9187
|
+
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
9188
|
+
// after a delay so the keyboard dismiss animation and layout settle.
|
|
9189
|
+
if (keyboardOpen && !isKeyboardOpen) {
|
|
9190
|
+
setTimeout(function() {
|
|
9191
|
+
ensureTerminalFit();
|
|
9192
|
+
maybeScrollTerminalToBottom("force");
|
|
9193
|
+
}, 200);
|
|
9194
|
+
}
|
|
9195
|
+
|
|
8565
9196
|
keyboardOpen = isKeyboardOpen;
|
|
8566
9197
|
lastHeight = vv.height;
|
|
8567
9198
|
}
|
|
@@ -8666,6 +9297,11 @@
|
|
|
8666
9297
|
}
|
|
8667
9298
|
state.resizeHandler = function() { scheduleTerminalResize(true); };
|
|
8668
9299
|
window.addEventListener("resize", state.resizeHandler);
|
|
9300
|
+
// Also listen to visualViewport resize for pinch-zoom / browser zoom
|
|
9301
|
+
if (window.visualViewport) {
|
|
9302
|
+
state.visualViewportHandler = function() { scheduleTerminalResize(true); };
|
|
9303
|
+
window.visualViewport.addEventListener("resize", state.visualViewportHandler);
|
|
9304
|
+
}
|
|
8669
9305
|
requestAnimationFrame(function() { scheduleTerminalResize(true); });
|
|
8670
9306
|
}
|
|
8671
9307
|
|
|
@@ -8682,6 +9318,10 @@
|
|
|
8682
9318
|
window.removeEventListener("resize", state.resizeHandler);
|
|
8683
9319
|
state.resizeHandler = null;
|
|
8684
9320
|
}
|
|
9321
|
+
if (state.visualViewportHandler && window.visualViewport) {
|
|
9322
|
+
window.visualViewport.removeEventListener("resize", state.visualViewportHandler);
|
|
9323
|
+
state.visualViewportHandler = null;
|
|
9324
|
+
}
|
|
8685
9325
|
[["mousemove", "resizeMouseMove"], ["mouseup", "resizeMouseUp"],
|
|
8686
9326
|
["touchmove", "resizeTouchMove"], ["touchend", "resizeTouchEnd"]
|
|
8687
9327
|
].forEach(function(pair) {
|
|
@@ -8814,6 +9454,12 @@
|
|
|
8814
9454
|
state.pollTimer = setInterval(refreshAll, 1600);
|
|
8815
9455
|
}
|
|
8816
9456
|
|
|
9457
|
+
// Periodically refresh session time displays (30s)
|
|
9458
|
+
setInterval(function() {
|
|
9459
|
+
var timeEls = document.querySelectorAll(".session-time");
|
|
9460
|
+
if (timeEls.length > 0) scheduleSessionListUpdate();
|
|
9461
|
+
}, 30000);
|
|
9462
|
+
|
|
8817
9463
|
function initWebSocket() {
|
|
8818
9464
|
if (!window.WebSocket) return false;
|
|
8819
9465
|
|
|
@@ -8952,14 +9598,26 @@
|
|
|
8952
9598
|
// Trigger status bar completion animation
|
|
8953
9599
|
scheduleChatRender(true);
|
|
8954
9600
|
}
|
|
8955
|
-
// Notify user when a session completes
|
|
9601
|
+
// Notify user when a session completes — show what was accomplished
|
|
8956
9602
|
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
9603
|
var endedExitCode = msg.data && msg.data.exitCode;
|
|
8959
9604
|
var endedIsError = endedExitCode !== null && endedExitCode !== undefined && endedExitCode !== 0;
|
|
9605
|
+
// Build meaningful notification body
|
|
9606
|
+
var endedTaskSummary = endedSession ? (endedSession.summary || "") : "";
|
|
9607
|
+
var endedLastReply = endedSession ? getLastAssistantSummary(endedSession) : "";
|
|
9608
|
+
var endedNotifTitle = endedIsError ? "任务异常结束" : "任务已完成";
|
|
9609
|
+
var endedNotifBody = "";
|
|
9610
|
+
if (endedTaskSummary) {
|
|
9611
|
+
endedNotifBody = endedTaskSummary;
|
|
9612
|
+
if (endedLastReply && !endedIsError) {
|
|
9613
|
+
endedNotifBody += "\n" + endedLastReply;
|
|
9614
|
+
}
|
|
9615
|
+
} else {
|
|
9616
|
+
endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
|
|
9617
|
+
}
|
|
8960
9618
|
sendBrowserNotification(
|
|
8961
|
-
|
|
8962
|
-
|
|
9619
|
+
endedNotifTitle,
|
|
9620
|
+
endedNotifBody,
|
|
8963
9621
|
{
|
|
8964
9622
|
tag: "wand-ended-" + msg.sessionId,
|
|
8965
9623
|
onClick: function() {
|
|
@@ -8969,8 +9627,8 @@
|
|
|
8969
9627
|
);
|
|
8970
9628
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
8971
9629
|
showNotificationBubble({
|
|
8972
|
-
title:
|
|
8973
|
-
body:
|
|
9630
|
+
title: endedNotifTitle,
|
|
9631
|
+
body: endedNotifBody,
|
|
8974
9632
|
type: endedIsError ? "warning" : "success",
|
|
8975
9633
|
icon: endedIsError ? "!" : "\u2713",
|
|
8976
9634
|
duration: 6000,
|
|
@@ -9052,6 +9710,8 @@
|
|
|
9052
9710
|
state.currentTask = msg.data || null;
|
|
9053
9711
|
updateTaskDisplay();
|
|
9054
9712
|
}
|
|
9713
|
+
// Update session list to reflect current activity (debounced)
|
|
9714
|
+
scheduleSessionListUpdate();
|
|
9055
9715
|
break;
|
|
9056
9716
|
case 'status':
|
|
9057
9717
|
if (msg.sessionId && msg.data) {
|
|
@@ -9087,10 +9747,18 @@
|
|
|
9087
9747
|
};
|
|
9088
9748
|
// Browser notification for permission waiting (background tab)
|
|
9089
9749
|
var permSession = state.sessions.find(function(s) { return s.id === msg.sessionId; });
|
|
9090
|
-
var
|
|
9750
|
+
var permTaskName = permSession ? (permSession.summary || permSession.command || msg.sessionId) : msg.sessionId;
|
|
9751
|
+
var permDetail = msg.data.permissionRequest.prompt || "需要权限审批";
|
|
9752
|
+
var permTarget = msg.data.permissionRequest.target;
|
|
9753
|
+
var permBody = permTaskName;
|
|
9754
|
+
if (permTarget) {
|
|
9755
|
+
permBody += "\n" + permDetail + " · " + permTarget;
|
|
9756
|
+
} else {
|
|
9757
|
+
permBody += "\n" + permDetail;
|
|
9758
|
+
}
|
|
9091
9759
|
sendBrowserNotification(
|
|
9092
|
-
"
|
|
9093
|
-
|
|
9760
|
+
"需要你的授权",
|
|
9761
|
+
permBody,
|
|
9094
9762
|
{
|
|
9095
9763
|
tag: "wand-perm-" + msg.sessionId,
|
|
9096
9764
|
onClick: function() {
|
|
@@ -9103,12 +9771,12 @@
|
|
|
9103
9771
|
// In-app bubble if not currently viewing this session
|
|
9104
9772
|
if (msg.sessionId !== state.selectedId) {
|
|
9105
9773
|
showNotificationBubble({
|
|
9106
|
-
title: "
|
|
9107
|
-
body:
|
|
9774
|
+
title: "需要你的授权",
|
|
9775
|
+
body: permBody,
|
|
9108
9776
|
type: "warning",
|
|
9109
9777
|
icon: "!",
|
|
9110
9778
|
duration: 0,
|
|
9111
|
-
actionLabel: "
|
|
9779
|
+
actionLabel: "去处理",
|
|
9112
9780
|
action: function() {
|
|
9113
9781
|
selectSession(msg.sessionId);
|
|
9114
9782
|
}
|
|
@@ -9542,9 +10210,9 @@
|
|
|
9542
10210
|
return;
|
|
9543
10211
|
}
|
|
9544
10212
|
|
|
9545
|
-
var
|
|
10213
|
+
var allMessages = state.currentMessages;
|
|
9546
10214
|
|
|
9547
|
-
if (
|
|
10215
|
+
if (allMessages.length === 0) {
|
|
9548
10216
|
if (state.lastRenderedEmpty !== "empty") {
|
|
9549
10217
|
renderChatEmptyState(chatOutput, '<div class="empty-state"><strong>对话已开始</strong><br>在下方输入框发送消息,Claude 会自动回复。</div>');
|
|
9550
10218
|
state.lastRenderedEmpty = "empty";
|
|
@@ -9553,6 +10221,16 @@
|
|
|
9553
10221
|
return;
|
|
9554
10222
|
}
|
|
9555
10223
|
|
|
10224
|
+
// Lazy loading: only render the most recent chatRenderedCount messages.
|
|
10225
|
+
// Auto-expand when new messages arrive during active streaming to avoid hiding them.
|
|
10226
|
+
var totalMsgCount = allMessages.length;
|
|
10227
|
+
if (totalMsgCount > state.chatRenderedCount && state.chatAutoFollow) {
|
|
10228
|
+
state.chatRenderedCount = totalMsgCount;
|
|
10229
|
+
}
|
|
10230
|
+
var visibleOffset = Math.max(0, totalMsgCount - state.chatRenderedCount);
|
|
10231
|
+
var messages = visibleOffset > 0 ? allMessages.slice(visibleOffset) : allMessages;
|
|
10232
|
+
var hasOlderMessages = visibleOffset > 0;
|
|
10233
|
+
|
|
9556
10234
|
// Check if messages actually changed
|
|
9557
10235
|
var msgCount = messages.length;
|
|
9558
10236
|
var outputHash = selectedSession.output ? selectedSession.output.length : 0;
|
|
@@ -9613,16 +10291,17 @@
|
|
|
9613
10291
|
// Build HTML with system info cards interleaved
|
|
9614
10292
|
var html = '';
|
|
9615
10293
|
var reversedMessages = messages.slice().reverse();
|
|
9616
|
-
var
|
|
10294
|
+
var visibleCount = messages.length;
|
|
9617
10295
|
|
|
9618
10296
|
for (var i = 0; i < reversedMessages.length; i++) {
|
|
9619
10297
|
var msg = reversedMessages[i];
|
|
9620
|
-
var
|
|
10298
|
+
var localIndex = visibleCount - 1 - i; // Index within visible slice
|
|
10299
|
+
var originalIndex = localIndex + visibleOffset; // Index in full messages array
|
|
9621
10300
|
|
|
9622
10301
|
// Find system info for this message position
|
|
9623
10302
|
var sysInfo = null;
|
|
9624
10303
|
for (var j = 0; j < systemInfo.length; j++) {
|
|
9625
|
-
if (systemInfo[j].beforeMessage ===
|
|
10304
|
+
if (systemInfo[j].beforeMessage === localIndex) {
|
|
9626
10305
|
sysInfo = systemInfo[j];
|
|
9627
10306
|
break;
|
|
9628
10307
|
}
|
|
@@ -9642,6 +10321,13 @@
|
|
|
9642
10321
|
html += renderChatMessage(msg, roundUsageByIndex[originalIndex] || null, originalIndex);
|
|
9643
10322
|
}
|
|
9644
10323
|
|
|
10324
|
+
// Add sentinel for loading older messages (DOM end = visual top in column-reverse)
|
|
10325
|
+
if (hasOlderMessages) {
|
|
10326
|
+
html += '<div class="chat-load-more" id="chat-load-more-sentinel">' +
|
|
10327
|
+
'<button class="chat-load-more-btn" type="button">加载更早的 ' + Math.min(state.chatPageSize, visibleOffset) + ' 条消息</button>' +
|
|
10328
|
+
'</div>';
|
|
10329
|
+
}
|
|
10330
|
+
|
|
9645
10331
|
chatMessages.innerHTML = html;
|
|
9646
10332
|
attachAllCopyHandlers(chatMessages);
|
|
9647
10333
|
bindChatScrollListener();
|
|
@@ -9667,6 +10353,7 @@
|
|
|
9667
10353
|
// Scroll to bottom (newest message) - column-reverse: scrollTop=0 is visual bottom
|
|
9668
10354
|
requestAnimationFrame(function() {
|
|
9669
10355
|
smartScrollToBottom(chatMessages);
|
|
10356
|
+
observeLoadMoreSentinel();
|
|
9670
10357
|
});
|
|
9671
10358
|
}
|
|
9672
10359
|
|
|
@@ -9687,15 +10374,15 @@
|
|
|
9687
10374
|
});
|
|
9688
10375
|
}
|
|
9689
10376
|
|
|
9690
|
-
// Pre-compute per-round cumulative usage.
|
|
10377
|
+
// Pre-compute per-round cumulative usage using original (full array) indices.
|
|
9691
10378
|
// A "round" starts at a user message and includes all subsequent assistant turns
|
|
9692
10379
|
// until the next user message. Only the last assistant in each round shows the total.
|
|
9693
10380
|
var roundUsageByIndex = {};
|
|
9694
10381
|
(function() {
|
|
9695
10382
|
var acc = { inputTokens: 0, outputTokens: 0, cacheReadInputTokens: 0, totalCostUsd: 0 };
|
|
9696
10383
|
var lastAssistantIdx = -1;
|
|
9697
|
-
for (var mi = 0; mi <
|
|
9698
|
-
var m =
|
|
10384
|
+
for (var mi = 0; mi < allMessages.length; mi++) {
|
|
10385
|
+
var m = allMessages[mi];
|
|
9699
10386
|
if (m.role === "user") {
|
|
9700
10387
|
if (lastAssistantIdx >= 0 && (acc.inputTokens > 0 || acc.outputTokens > 0 || acc.totalCostUsd > 0)) {
|
|
9701
10388
|
roundUsageByIndex[lastAssistantIdx] = {
|
|
@@ -9739,7 +10426,7 @@
|
|
|
9739
10426
|
var insertedEls = [];
|
|
9740
10427
|
for (var i = 0; i < newMessages.length; i++) {
|
|
9741
10428
|
var div = document.createElement("div");
|
|
9742
|
-
var nmOrigIdx = existingCount + (newMessages.length - 1 - i);
|
|
10429
|
+
var nmOrigIdx = visibleOffset + existingCount + (newMessages.length - 1 - i);
|
|
9743
10430
|
div.innerHTML = renderChatMessage(newMessages[i], roundUsageByIndex[nmOrigIdx] || null, nmOrigIdx);
|
|
9744
10431
|
var el = div.firstElementChild;
|
|
9745
10432
|
if (el) {
|
|
@@ -9771,7 +10458,7 @@
|
|
|
9771
10458
|
for (var mi = 0; mi < MAX_STREAMING_SCAN; mi++) {
|
|
9772
10459
|
var currentEl = existingEls[mi];
|
|
9773
10460
|
var tmpWrap = document.createElement("div");
|
|
9774
|
-
var srOrigIdx = reversedMessages.length - 1 - mi;
|
|
10461
|
+
var srOrigIdx = visibleOffset + reversedMessages.length - 1 - mi;
|
|
9775
10462
|
tmpWrap.innerHTML = renderChatMessage(reversedMessages[mi], roundUsageByIndex[srOrigIdx] || null, srOrigIdx);
|
|
9776
10463
|
var replacementEl = tmpWrap.firstElementChild;
|
|
9777
10464
|
if (!replacementEl) continue;
|
|
@@ -9817,7 +10504,7 @@
|
|
|
9817
10504
|
renderStructuredStatusBar(chatMessages, selectedSession);
|
|
9818
10505
|
|
|
9819
10506
|
// Update todo progress bar from latest messages
|
|
9820
|
-
updateTodoProgress(
|
|
10507
|
+
updateTodoProgress(allMessages);
|
|
9821
10508
|
}
|
|
9822
10509
|
|
|
9823
10510
|
// Smart scroll: only auto-scroll if user is near bottom
|
|
@@ -10890,7 +11577,8 @@
|
|
|
10890
11577
|
// Thinking card (deep thought) — from PTY parsing
|
|
10891
11578
|
if (msg.role === "thinking") {
|
|
10892
11579
|
var thinkingKey = buildExpandKey("thinking", [getMessageKey(msg, messageIndex), "pty"]);
|
|
10893
|
-
var
|
|
11580
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11581
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
10894
11582
|
return '<div class="chat-message thinking">' +
|
|
10895
11583
|
'<div class="thinking-inline thinking-pty ' + (thinkingExpanded ? 'expanded' : 'collapsed') + '" data-expand-kind="thinking" data-expand-key="' + escapeHtml(thinkingKey) + '" data-thinking="" onclick="__thinkingToggle(this)">' +
|
|
10896
11584
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
@@ -11028,7 +11716,7 @@
|
|
|
11028
11716
|
var summaryText = parts.join(" · ");
|
|
11029
11717
|
var groupKey = buildExpandKey("tool-group", [messageKey, items[0] && items[0].index, items.length]);
|
|
11030
11718
|
var persistedExpanded = getPersistedExpandState(groupKey);
|
|
11031
|
-
var shouldExpand = persistedExpanded === null ?
|
|
11719
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.toolGroup) : persistedExpanded;
|
|
11032
11720
|
|
|
11033
11721
|
// Render each item's inline-tool card
|
|
11034
11722
|
var innerHtml = "";
|
|
@@ -11139,7 +11827,8 @@
|
|
|
11139
11827
|
'</div>';
|
|
11140
11828
|
}
|
|
11141
11829
|
var thinkingKey = buildExpandKey("thinking", [messageKey, index]);
|
|
11142
|
-
var
|
|
11830
|
+
var thinkingPersisted = getPersistedExpandState(thinkingKey);
|
|
11831
|
+
var thinkingExpanded = thinkingPersisted === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.thinking) : thinkingPersisted;
|
|
11143
11832
|
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
11833
|
'<span class="thinking-inline-icon">⦿</span>' +
|
|
11145
11834
|
'<span class="thinking-inline-preview">' + escapeHtml(thinkingExpanded ? thinkingText : preview) + '</span>' +
|
|
@@ -11236,7 +11925,7 @@
|
|
|
11236
11925
|
var fullResult = resultContent;
|
|
11237
11926
|
|
|
11238
11927
|
var expandedHtml = "";
|
|
11239
|
-
var shouldExpand = persistedExpanded === null ?
|
|
11928
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.inlineTools) : persistedExpanded;
|
|
11240
11929
|
if (hasResult) {
|
|
11241
11930
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';">' +
|
|
11242
11931
|
'<div class="inline-tool-result">' + formatInlineResult(resultContent, toolName) + '</div>' +
|
|
@@ -11248,16 +11937,23 @@
|
|
|
11248
11937
|
expandedHtml = '<div class="inline-tool-expanded" style="display: ' + (shouldExpand ? 'block' : 'none') + ';"><div class="inline-tool-loading">等待响应…</div></div>';
|
|
11249
11938
|
}
|
|
11250
11939
|
|
|
11940
|
+
var isTruncated = toolResult && toolResult._truncated === true;
|
|
11941
|
+
|
|
11251
11942
|
var extraInfoHtml = meta ? '<span class="inline-tool-meta">' + escapeHtml(meta) + '</span>' : '';
|
|
11252
11943
|
var extraClass = isError ? 'inline-tool-error-inline' : '';
|
|
11253
11944
|
if (shouldExpand) extraClass += ' inline-tool-open';
|
|
11254
11945
|
|
|
11946
|
+
var truncatedAttrs = isTruncated
|
|
11947
|
+
? 'data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '" '
|
|
11948
|
+
: '';
|
|
11949
|
+
|
|
11255
11950
|
return '<div class="inline-tool ' + extraClass + '" ' +
|
|
11256
11951
|
'data-expand-kind="inline-tool" ' +
|
|
11257
11952
|
'data-expand-key="' + escapeHtml(expandKey) + '" ' +
|
|
11258
11953
|
'data-result="' + escapeHtml(fullResult) + '" ' +
|
|
11259
11954
|
'data-preview="' + previewDataAttr + '" ' +
|
|
11260
11955
|
'data-status="' + (isError ? 'error' : (hasResult ? 'done' : 'pending')) + '" ' +
|
|
11956
|
+
truncatedAttrs +
|
|
11261
11957
|
'onclick="__inlineToolToggle(this)">' +
|
|
11262
11958
|
'<div class="inline-tool-row">' +
|
|
11263
11959
|
'<span class="inline-tool-status">' + statusIcon + '</span>' +
|
|
@@ -11314,9 +12010,14 @@
|
|
|
11314
12010
|
|
|
11315
12011
|
// Show command preview in header (truncate long commands)
|
|
11316
12012
|
var cmdPreview = command.length > 80 ? command.slice(0, 77) + "…" : command;
|
|
11317
|
-
var shouldExpand = persistedExpanded === null ?
|
|
12013
|
+
var shouldExpand = persistedExpanded === null ? !!(state.config && state.config.cardDefaults && state.config.cardDefaults.terminal) : persistedExpanded;
|
|
11318
12014
|
|
|
11319
|
-
|
|
12015
|
+
var termTruncated = toolResult && toolResult._truncated === true;
|
|
12016
|
+
var termTruncAttrs = termTruncated
|
|
12017
|
+
? ' data-truncated="true" data-tool-use-id="' + escapeHtml(block.id || "") + '"'
|
|
12018
|
+
: '';
|
|
12019
|
+
|
|
12020
|
+
return '<div class="inline-terminal" data-expand-kind="terminal" data-expand-key="' + escapeHtml(expandKey) + '" data-expanded="' + (shouldExpand ? 'true' : 'false') + '"' + termTruncAttrs + '>' +
|
|
11320
12021
|
'<div class="term-header" onclick="__terminalExpand(this)">' +
|
|
11321
12022
|
statusDot +
|
|
11322
12023
|
'<span class="term-cmd-preview"><span class="term-prompt">$</span> ' + escapeHtml(cmdPreview) + '</span>' +
|
|
@@ -11596,10 +12297,12 @@
|
|
|
11596
12297
|
|
|
11597
12298
|
var expandKey = buildExpandKey("tool-card", [messageKey, toolId]);
|
|
11598
12299
|
var persistedExpanded = getPersistedExpandState(expandKey);
|
|
11599
|
-
var
|
|
12300
|
+
var cardDefaultExpand = !!(state.config && state.config.cardDefaults && state.config.cardDefaults.editCards);
|
|
12301
|
+
var shouldExpand = persistedExpanded === null ? (statusClass === "loading" || cardDefaultExpand) : persistedExpanded;
|
|
12302
|
+
var tcTruncated = toolResult && toolResult._truncated === true;
|
|
11600
12303
|
var collapsedClass = shouldExpand ? "" : " collapsed";
|
|
11601
12304
|
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) + '">' +
|
|
12305
|
+
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
12306
|
'<div class="tool-use-header" data-tool-toggle onclick="__tcToggle(event,this)">' +
|
|
11604
12307
|
'<span class="tool-use-icon">' + headerIcon + '</span>' +
|
|
11605
12308
|
'<span class="tool-use-name">' + escapeHtml(titleText) + '</span>' +
|
|
@@ -12059,7 +12762,7 @@
|
|
|
12059
12762
|
'</div>';
|
|
12060
12763
|
|
|
12061
12764
|
var bodyHtml = opts.body
|
|
12062
|
-
? '<div class="notification-bubble-body">' + escapeHtml(opts.body) + '</div>'
|
|
12765
|
+
? '<div class="notification-bubble-body">' + escapeHtml(opts.body).replace(/\n/g, '<br>') + '</div>'
|
|
12063
12766
|
: '';
|
|
12064
12767
|
|
|
12065
12768
|
var actionsHtml = opts.actionLabel
|
|
@@ -12125,13 +12828,43 @@
|
|
|
12125
12828
|
|
|
12126
12829
|
// ── Browser Notification API ──
|
|
12127
12830
|
|
|
12831
|
+
// Detect Android APK native bridge
|
|
12832
|
+
var _hasNativeBridge = typeof WandNative !== "undefined" && typeof WandNative.sendNotification === "function";
|
|
12833
|
+
// Detect if running inside APK and extract installed version from User-Agent
|
|
12834
|
+
var _apkVersionMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
|
|
12835
|
+
var _apkVersion = _apkVersionMatch ? _apkVersionMatch[1] : null;
|
|
12836
|
+
|
|
12837
|
+
function _getNativePermission() {
|
|
12838
|
+
if (_hasNativeBridge && typeof WandNative.getPermission === "function") {
|
|
12839
|
+
try { return WandNative.getPermission(); } catch (_e) {}
|
|
12840
|
+
}
|
|
12841
|
+
return null;
|
|
12842
|
+
}
|
|
12843
|
+
|
|
12128
12844
|
function requestNotificationPermission() {
|
|
12845
|
+
if (_hasNativeBridge) {
|
|
12846
|
+
var perm = _getNativePermission();
|
|
12847
|
+
if (perm === "default" || perm === "denied") {
|
|
12848
|
+
try { WandNative.requestPermission(); } catch (_e) {}
|
|
12849
|
+
}
|
|
12850
|
+
return;
|
|
12851
|
+
}
|
|
12129
12852
|
if (typeof Notification !== "undefined" && Notification.permission === "default") {
|
|
12130
12853
|
Notification.requestPermission();
|
|
12131
12854
|
}
|
|
12132
12855
|
}
|
|
12133
12856
|
|
|
12134
12857
|
function sendBrowserNotification(title, body, opts) {
|
|
12858
|
+
// Native Android bridge path
|
|
12859
|
+
if (_hasNativeBridge) {
|
|
12860
|
+
var perm = _getNativePermission();
|
|
12861
|
+
if (perm !== "granted") return;
|
|
12862
|
+
try {
|
|
12863
|
+
WandNative.sendNotification(title || "Wand", body || "", (opts && opts.tag) || "");
|
|
12864
|
+
} catch (_e) {}
|
|
12865
|
+
return;
|
|
12866
|
+
}
|
|
12867
|
+
// Browser Notification API path
|
|
12135
12868
|
if (typeof Notification === "undefined" || Notification.permission !== "granted") return;
|
|
12136
12869
|
if (!document.hidden) return; // Only notify when tab is in background
|
|
12137
12870
|
try {
|