@co0ontty/wand 1.17.6 → 1.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/process-manager.d.ts +4 -2
- package/dist/process-manager.js +57 -12
- package/dist/server-session-routes.js +2 -4
- package/dist/web-ui/content/scripts.js +427 -121
- package/dist/web-ui/content/styles.css +546 -67
- package/package.json +1 -1
|
@@ -337,8 +337,11 @@
|
|
|
337
337
|
var enabled = !!state.chatAutoFollow;
|
|
338
338
|
button.classList.toggle("active", enabled);
|
|
339
339
|
button.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
340
|
-
button.setAttribute("title", enabled ? "
|
|
341
|
-
button.
|
|
340
|
+
button.setAttribute("title", enabled ? "追踪底部:开启(点击暂停)" : "追踪底部:已暂停(点击开启)");
|
|
341
|
+
button.setAttribute("aria-label", enabled ? "追踪底部:开启" : "追踪底部:已暂停");
|
|
342
|
+
button.innerHTML = enabled
|
|
343
|
+
? '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 2.5l4.5 4.5 4.5-4.5"/><path d="M3.5 8.5l4.5 4.5 4.5-4.5"/></svg>'
|
|
344
|
+
: '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 3v10"/><path d="M10.5 3v10"/></svg>';
|
|
342
345
|
}
|
|
343
346
|
|
|
344
347
|
function updateChatJumpToBottomButton() {
|
|
@@ -842,6 +845,9 @@
|
|
|
842
845
|
if (!state.ws || (state.ws.readyState !== WebSocket.OPEN && state.ws.readyState !== WebSocket.CONNECTING)) {
|
|
843
846
|
initWebSocket();
|
|
844
847
|
}
|
|
848
|
+
if (state.claudeHistoryLoaded) {
|
|
849
|
+
loadClaudeHistory();
|
|
850
|
+
}
|
|
845
851
|
return loadSessions({ skipSelectedOutputReload: true }).then(function() {
|
|
846
852
|
if (state.selectedId) {
|
|
847
853
|
return loadOutput(state.selectedId);
|
|
@@ -1201,13 +1207,13 @@
|
|
|
1201
1207
|
'<span class="terminal-scale-overlay-divider"></span>' +
|
|
1202
1208
|
'<button id="page-refresh-btn" class="terminal-scale-overlay-btn" type="button" title="刷新页面">↻</button>' +
|
|
1203
1209
|
'</div>' +
|
|
1204
|
-
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部"
|
|
1210
|
+
'<button id="terminal-jump-bottom" class="terminal-jump-bottom' + (state.showTerminalJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部" aria-label="回到底部"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8l4.5 4.5L12.5 8"/></svg></button>' +
|
|
1205
1211
|
'</div>' +
|
|
1206
1212
|
'<div id="chat-output" class="chat-container hidden">' +
|
|
1207
1213
|
'<div class="chat-overlay-controls">' +
|
|
1208
|
-
'<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '"
|
|
1214
|
+
'<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '" aria-label="' + (state.chatAutoFollow ? '追踪底部:开启' : '追踪底部:已暂停') + '" title="' + (state.chatAutoFollow ? '追踪底部:开启(点击暂停)' : '追踪底部:已暂停(点击开启)') + '">' + (state.chatAutoFollow ? '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.5 2.5l4.5 4.5 4.5-4.5"/><path d="M3.5 8.5l4.5 4.5 4.5-4.5"/></svg>' : '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5.5 3v10"/><path d="M10.5 3v10"/></svg>') + '</button>' +
|
|
1209
1215
|
'</div>' +
|
|
1210
|
-
'<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底"
|
|
1216
|
+
'<button id="chat-jump-bottom" class="chat-jump-bottom' + (state.showChatJumpToBottom ? ' visible' : '') + '" type="button" title="回到底部并继续追底" aria-label="回到底部"><svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3.5v9M3.5 8l4.5 4.5L12.5 8"/></svg></button>' +
|
|
1211
1217
|
'</div>' +
|
|
1212
1218
|
'<div id="blank-chat" class="blank-chat' + (state.selectedId ? " hidden" : "") + '">' +
|
|
1213
1219
|
'<div class="blank-chat-inner">' +
|
|
@@ -1409,58 +1415,81 @@
|
|
|
1409
1415
|
'<div class="settings-about-row"><span class="settings-label">Node.js 要求</span><span class="settings-value" id="settings-node-req">-</span></div>' +
|
|
1410
1416
|
'<div class="settings-about-row"><span class="settings-label">仓库地址</span><span class="settings-value" id="settings-repo-url"><a href="#" target="_blank" rel="noopener">-</a></span></div>' +
|
|
1411
1417
|
'</div>' +
|
|
1412
|
-
'<div class="settings-update-section">' +
|
|
1418
|
+
'<div class="settings-update-section" id="web-update-section">' +
|
|
1419
|
+
'<div class="settings-section-head">' +
|
|
1420
|
+
'<span class="settings-section-icon">🌐</span>' +
|
|
1421
|
+
'<div class="settings-section-head-text">' +
|
|
1422
|
+
'<h4 class="settings-section-heading">Web 端</h4>' +
|
|
1423
|
+
'<p class="settings-section-sub">浏览器访问的服务版本</p>' +
|
|
1424
|
+
'</div>' +
|
|
1425
|
+
'</div>' +
|
|
1413
1426
|
'<div class="settings-about-row">' +
|
|
1414
1427
|
'<span class="settings-label">最新版本</span>' +
|
|
1415
1428
|
'<span class="settings-value" id="settings-latest-version">-</span>' +
|
|
1416
1429
|
'</div>' +
|
|
1417
1430
|
'<div class="settings-update-actions">' +
|
|
1418
|
-
'<button id="check-update-button" class="btn btn-
|
|
1431
|
+
'<button id="check-update-button" class="btn btn-secondary btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
|
|
1419
1432
|
'<button id="do-update-button" class="btn btn-primary btn-sm hidden">\u66f4\u65b0\u5230\u6700\u65b0\u7248</button>' +
|
|
1420
1433
|
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1421
1434
|
'</div>' +
|
|
1422
1435
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1423
|
-
'<div class="settings-
|
|
1424
|
-
'<
|
|
1425
|
-
|
|
1436
|
+
'<div class="settings-toggle-row">' +
|
|
1437
|
+
'<div class="settings-toggle-text">' +
|
|
1438
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
1439
|
+
'<span class="settings-toggle-desc">检测到新版本将自动下载安装并重启服务。</span>' +
|
|
1440
|
+
'</div>' +
|
|
1441
|
+
'<label class="settings-switch">' +
|
|
1426
1442
|
'<input type="checkbox" id="auto-update-web-toggle" class="switch-toggle">' +
|
|
1427
1443
|
'<span class="switch-slider"></span>' +
|
|
1428
1444
|
'</label>' +
|
|
1429
1445
|
'</div>' +
|
|
1430
|
-
'<p class="hint" style="margin-top:2px">开启后,检测到新版本将自动下载安装并重启服务。</p>' +
|
|
1431
1446
|
'</div>' +
|
|
1432
|
-
'<div class="settings-update-section" id="android-apk-section">' +
|
|
1447
|
+
'<div class="settings-update-section hidden" id="android-apk-section">' +
|
|
1448
|
+
'<div class="settings-section-head">' +
|
|
1449
|
+
'<span class="settings-section-icon">📱</span>' +
|
|
1450
|
+
'<div class="settings-section-head-text">' +
|
|
1451
|
+
'<h4 class="settings-section-heading">Android App</h4>' +
|
|
1452
|
+
'<p class="settings-section-sub">原生客户端版本与 APK 下载</p>' +
|
|
1453
|
+
'</div>' +
|
|
1454
|
+
'</div>' +
|
|
1433
1455
|
'<div id="android-apk-current-row" class="settings-about-row hidden">' +
|
|
1434
1456
|
'<span class="settings-label">当前版本</span>' +
|
|
1435
1457
|
'<span class="settings-value" id="settings-android-apk-current">-</span>' +
|
|
1436
1458
|
'</div>' +
|
|
1437
|
-
'<div id="android-apk-github-row" class="settings-about-row hidden">' +
|
|
1459
|
+
'<div id="android-apk-github-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
1438
1460
|
'<span class="settings-label">线上版本</span>' +
|
|
1439
|
-
'<span class="settings-value" id="settings-android-apk-github"
|
|
1440
|
-
'<button id="download-github-apk-btn" class="btn btn-
|
|
1461
|
+
'<span class="settings-value settings-value-flex" id="settings-android-apk-github">-</span>' +
|
|
1462
|
+
'<button id="download-github-apk-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
1441
1463
|
'</div>' +
|
|
1442
|
-
'<div id="android-apk-local-row" class="settings-about-row hidden">' +
|
|
1464
|
+
'<div id="android-apk-local-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
1443
1465
|
'<span class="settings-label">本地版本</span>' +
|
|
1444
|
-
'<span class="settings-value" id="settings-android-apk-local"
|
|
1445
|
-
'<button id="download-local-apk-btn" class="btn btn-
|
|
1466
|
+
'<span class="settings-value settings-value-flex" id="settings-android-apk-local">-</span>' +
|
|
1467
|
+
'<button id="download-local-apk-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
1446
1468
|
'</div>' +
|
|
1447
|
-
'<div id="android-auto-update-row" class="
|
|
1448
|
-
'<
|
|
1449
|
-
|
|
1469
|
+
'<div id="android-auto-update-row" class="settings-toggle-row hidden">' +
|
|
1470
|
+
'<div class="settings-toggle-text">' +
|
|
1471
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
1472
|
+
'<span class="settings-toggle-desc" id="android-auto-update-hint">检测到新版 APK 将自动下载安装。</span>' +
|
|
1473
|
+
'</div>' +
|
|
1474
|
+
'<label class="settings-switch">' +
|
|
1450
1475
|
'<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
|
|
1451
1476
|
'<span class="switch-slider"></span>' +
|
|
1452
1477
|
'</label>' +
|
|
1453
1478
|
'</div>' +
|
|
1454
|
-
'<p id="android-auto-update-hint" class="hint hidden" style="margin-top:2px">开启后,检测到新版 APK 将自动下载安装。</p>' +
|
|
1455
1479
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
1456
1480
|
'</div>' +
|
|
1457
1481
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
1458
|
-
'<div class="settings-section-
|
|
1482
|
+
'<div class="settings-section-head">' +
|
|
1483
|
+
'<span class="settings-section-icon">🔗</span>' +
|
|
1484
|
+
'<div class="settings-section-head-text">' +
|
|
1485
|
+
'<h4 class="settings-section-heading">App 连接码</h4>' +
|
|
1486
|
+
'<p class="settings-section-sub">粘贴到 Android App 即可自动连接,无需密码;改密码后失效。</p>' +
|
|
1487
|
+
'</div>' +
|
|
1488
|
+
'</div>' +
|
|
1459
1489
|
'<div class="settings-connect-url-box">' +
|
|
1460
|
-
'<code id="android-connect-code" class="settings-connect-url-text"
|
|
1461
|
-
'<button id="copy-connect-code-button" class="btn btn-
|
|
1490
|
+
'<code id="android-connect-code" class="settings-connect-url-text">-</code>' +
|
|
1491
|
+
'<button id="copy-connect-code-button" class="btn btn-secondary btn-sm" type="button" title="复制连接码">复制</button>' +
|
|
1462
1492
|
'</div>' +
|
|
1463
|
-
'<p class="hint">复制此连接码粘贴到 Android App 即可自动连接,无需输入密码。修改密码后连接码自动失效。</p>' +
|
|
1464
1493
|
'</div>' +
|
|
1465
1494
|
'</div>' +
|
|
1466
1495
|
|
|
@@ -1470,46 +1499,88 @@
|
|
|
1470
1499
|
'<h3 class="settings-panel-title">通知</h3>' +
|
|
1471
1500
|
'<p class="settings-panel-desc">设置提示音、系统通知和浏览器通知的行为。</p>' +
|
|
1472
1501
|
'</div>' +
|
|
1473
|
-
'<div class="settings-section
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
'
|
|
1481
|
-
'<div
|
|
1482
|
-
'<
|
|
1483
|
-
|
|
1502
|
+
'<div class="settings-notification-section">' +
|
|
1503
|
+
'<div class="settings-section-head">' +
|
|
1504
|
+
'<span class="settings-section-icon">🔔</span>' +
|
|
1505
|
+
'<div class="settings-section-head-text">' +
|
|
1506
|
+
'<h4 class="settings-section-heading">通知偏好</h4>' +
|
|
1507
|
+
'<p class="settings-section-sub">提示音与应用内通知气泡</p>' +
|
|
1508
|
+
'</div>' +
|
|
1509
|
+
'</div>' +
|
|
1510
|
+
'<div class="settings-toggle-row">' +
|
|
1511
|
+
'<div class="settings-toggle-text">' +
|
|
1512
|
+
'<label class="settings-toggle-title" for="cfg-notif-sound">播放提示音</label>' +
|
|
1513
|
+
'<span class="settings-toggle-desc">重要通知(版本更新、权限等待等)时播放柔和提示音。</span>' +
|
|
1514
|
+
'</div>' +
|
|
1515
|
+
'<label class="settings-switch">' +
|
|
1516
|
+
'<input id="cfg-notif-sound" type="checkbox" class="switch-toggle" />' +
|
|
1517
|
+
'<span class="switch-slider"></span>' +
|
|
1518
|
+
'</label>' +
|
|
1519
|
+
'</div>' +
|
|
1520
|
+
'<div class="settings-range-row" id="notif-volume-field">' +
|
|
1521
|
+
'<label class="settings-range-label" for="cfg-notif-volume">音量</label>' +
|
|
1522
|
+
'<input id="cfg-notif-volume" type="range" min="0" max="100" step="5" class="settings-range" />' +
|
|
1523
|
+
'<span id="cfg-notif-volume-val" class="settings-range-value">80%</span>' +
|
|
1524
|
+
'</div>' +
|
|
1525
|
+
'<div class="settings-toggle-row">' +
|
|
1526
|
+
'<div class="settings-toggle-text">' +
|
|
1527
|
+
'<label class="settings-toggle-title" for="cfg-notif-bubble">应用内通知气泡</label>' +
|
|
1528
|
+
'<span class="settings-toggle-desc">在页面顶部弹出浮动通知气泡。</span>' +
|
|
1529
|
+
'</div>' +
|
|
1530
|
+
'<label class="settings-switch">' +
|
|
1531
|
+
'<input id="cfg-notif-bubble" type="checkbox" class="switch-toggle" />' +
|
|
1532
|
+
'<span class="switch-slider"></span>' +
|
|
1533
|
+
'</label>' +
|
|
1484
1534
|
'</div>' +
|
|
1485
1535
|
'</div>' +
|
|
1486
|
-
'<div class="
|
|
1487
|
-
'<
|
|
1488
|
-
|
|
1536
|
+
'<div id="native-sound-section" class="settings-notification-section hidden">' +
|
|
1537
|
+
'<div class="settings-section-head">' +
|
|
1538
|
+
'<span class="settings-section-icon">🎵</span>' +
|
|
1539
|
+
'<div class="settings-section-head-text">' +
|
|
1540
|
+
'<h4 class="settings-section-heading">系统通知铃声</h4>' +
|
|
1541
|
+
'<p class="settings-section-sub">选择 Android 系统通知使用的铃声</p>' +
|
|
1542
|
+
'</div>' +
|
|
1543
|
+
'</div>' +
|
|
1544
|
+
'<div class="settings-row-with-action">' +
|
|
1545
|
+
'<select id="native-sound-select" class="field-input field-select"></select>' +
|
|
1546
|
+
'<button id="native-sound-preview" class="btn btn-secondary btn-sm" type="button">▶ 试听</button>' +
|
|
1547
|
+
'</div>' +
|
|
1489
1548
|
'</div>' +
|
|
1490
|
-
'<
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1549
|
+
'<div id="native-haptic-section" class="settings-notification-section hidden">' +
|
|
1550
|
+
'<div class="settings-section-head">' +
|
|
1551
|
+
'<span class="settings-section-icon">📳</span>' +
|
|
1552
|
+
'<div class="settings-section-head-text">' +
|
|
1553
|
+
'<h4 class="settings-section-heading">触感反馈</h4>' +
|
|
1554
|
+
'<p class="settings-section-sub">按钮操作和任务完成时提供振动反馈</p>' +
|
|
1555
|
+
'</div>' +
|
|
1556
|
+
'</div>' +
|
|
1557
|
+
'<div class="settings-toggle-row">' +
|
|
1558
|
+
'<div class="settings-toggle-text">' +
|
|
1559
|
+
'<label class="settings-toggle-title" for="cfg-haptic-enabled">启用触感反馈</label>' +
|
|
1498
1560
|
'</div>' +
|
|
1561
|
+
'<label class="settings-switch">' +
|
|
1562
|
+
'<input id="cfg-haptic-enabled" type="checkbox" class="switch-toggle" />' +
|
|
1563
|
+
'<span class="switch-slider"></span>' +
|
|
1564
|
+
'</label>' +
|
|
1499
1565
|
'</div>' +
|
|
1500
|
-
'<p class="hint" style="margin-top:0">\u9009\u62e9 Android \u7cfb\u7edf\u901a\u77e5\u4f7f\u7528\u7684\u94c3\u58f0</p>' +
|
|
1501
1566
|
'</div>' +
|
|
1502
|
-
'<div class="settings-notification-section"
|
|
1503
|
-
'<div class="settings-section-
|
|
1567
|
+
'<div class="settings-notification-section">' +
|
|
1568
|
+
'<div class="settings-section-head">' +
|
|
1569
|
+
'<span class="settings-section-icon">🌐</span>' +
|
|
1570
|
+
'<div class="settings-section-head-text">' +
|
|
1571
|
+
'<h4 class="settings-section-heading">浏览器通知</h4>' +
|
|
1572
|
+
'<p class="settings-section-sub">来自系统通知中心的弹窗</p>' +
|
|
1573
|
+
'</div>' +
|
|
1574
|
+
'</div>' +
|
|
1504
1575
|
'<div class="settings-about-row">' +
|
|
1505
|
-
'<span class="settings-label"
|
|
1576
|
+
'<span class="settings-label">授权状态</span>' +
|
|
1506
1577
|
'<span class="settings-value" id="notification-permission-status">-</span>' +
|
|
1507
1578
|
'</div>' +
|
|
1508
1579
|
'<div class="settings-update-actions">' +
|
|
1509
|
-
'<button id="notification-request-btn" class="btn btn-
|
|
1510
|
-
'<button id="notification-reset-btn" class="btn btn-
|
|
1511
|
-
'<button id="notification-test-btn" class="btn btn-
|
|
1512
|
-
'<button id="notification-test-delay-btn" class="btn btn-
|
|
1580
|
+
'<button id="notification-request-btn" class="btn btn-secondary btn-sm hidden" type="button">授权通知</button>' +
|
|
1581
|
+
'<button id="notification-reset-btn" class="btn btn-secondary btn-sm hidden" type="button">重新授权</button>' +
|
|
1582
|
+
'<button id="notification-test-btn" class="btn btn-secondary btn-sm" type="button">发送测试通知</button>' +
|
|
1583
|
+
'<button id="notification-test-delay-btn" class="btn btn-secondary btn-sm" type="button">10 秒后发送</button>' +
|
|
1513
1584
|
'</div>' +
|
|
1514
1585
|
'<p id="notification-test-message" class="hint hidden"></p>' +
|
|
1515
1586
|
'</div>' +
|
|
@@ -1567,13 +1638,13 @@
|
|
|
1567
1638
|
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
1568
1639
|
'<div class="field">' +
|
|
1569
1640
|
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
1570
|
-
'<div
|
|
1571
|
-
'<select id="cfg-default-model" class="field-input
|
|
1641
|
+
'<div class="settings-row-with-action">' +
|
|
1642
|
+
'<select id="cfg-default-model" class="field-input field-select">' +
|
|
1572
1643
|
'<option value="">跟随 Claude Code 默认</option>' +
|
|
1573
1644
|
'</select>' +
|
|
1574
|
-
'<button type="button" id="cfg-default-model-refresh" class="btn btn-
|
|
1645
|
+
'<button type="button" id="cfg-default-model-refresh" class="btn btn-secondary btn-sm" title="刷新模型列表">刷新</button>' +
|
|
1575
1646
|
'</div>' +
|
|
1576
|
-
'<p class="field-hint" id="cfg-default-model-version"
|
|
1647
|
+
'<p class="field-hint" id="cfg-default-model-version">新建会话时默认使用该模型;运行中的会话可在输入框切换。</p>' +
|
|
1577
1648
|
'</div>' +
|
|
1578
1649
|
'<div class="field">' +
|
|
1579
1650
|
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
@@ -1584,24 +1655,29 @@
|
|
|
1584
1655
|
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
1585
1656
|
'</div>' +
|
|
1586
1657
|
(typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function" ?
|
|
1587
|
-
'<div
|
|
1588
|
-
'<div class="settings-section-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
'<
|
|
1593
|
-
PIXEL_AVATAR.user +
|
|
1594
|
-
'</div>' +
|
|
1595
|
-
'<span style="font-size:0.72rem;color:var(--text-secondary)">赛博虎妞</span>' +
|
|
1658
|
+
'<div class="settings-app-icon-block">' +
|
|
1659
|
+
'<div class="settings-section-head">' +
|
|
1660
|
+
'<span class="settings-section-icon">🎨</span>' +
|
|
1661
|
+
'<div class="settings-section-head-text">' +
|
|
1662
|
+
'<h4 class="settings-section-heading">应用图标</h4>' +
|
|
1663
|
+
'<p class="settings-section-sub">选择 App 启动器图标,返回桌面后生效</p>' +
|
|
1596
1664
|
'</div>' +
|
|
1597
|
-
|
|
1598
|
-
|
|
1665
|
+
'</div>' +
|
|
1666
|
+
'<div id="app-icon-picker" class="settings-app-icon-picker">' +
|
|
1667
|
+
'<button type="button" class="settings-app-icon-option" data-icon="shorthair">' +
|
|
1668
|
+
'<span class="settings-app-icon-preview">' +
|
|
1669
|
+
PIXEL_AVATAR.user +
|
|
1670
|
+
'</span>' +
|
|
1671
|
+
'<span class="settings-app-icon-label">赛博虎妞</span>' +
|
|
1672
|
+
'</button>' +
|
|
1673
|
+
'<button type="button" class="settings-app-icon-option" data-icon="garfield">' +
|
|
1674
|
+
'<span class="settings-app-icon-preview">' +
|
|
1599
1675
|
PIXEL_AVATAR.assistant +
|
|
1600
|
-
'</
|
|
1601
|
-
'<span
|
|
1602
|
-
'</
|
|
1676
|
+
'</span>' +
|
|
1677
|
+
'<span class="settings-app-icon-label">勤劳初二</span>' +
|
|
1678
|
+
'</button>' +
|
|
1603
1679
|
'</div>' +
|
|
1604
|
-
'<p id="app-icon-message" class="hint hidden"
|
|
1680
|
+
'<p id="app-icon-message" class="hint hidden"></p>' +
|
|
1605
1681
|
'</div>'
|
|
1606
1682
|
: '') +
|
|
1607
1683
|
'<div class="settings-actions settings-actions-sticky">' +
|
|
@@ -1617,7 +1693,13 @@
|
|
|
1617
1693
|
'<p class="settings-panel-desc">管理登录密码与 SSL 证书,敏感变更请确认后再保存。</p>' +
|
|
1618
1694
|
'</div>' +
|
|
1619
1695
|
'<div class="settings-card">' +
|
|
1620
|
-
'<
|
|
1696
|
+
'<div class="settings-card-head">' +
|
|
1697
|
+
'<span class="settings-card-icon">\ud83d\udd12</span>' +
|
|
1698
|
+
'<div class="settings-card-head-text">' +
|
|
1699
|
+
'<h3 class="settings-card-title">修改密码</h3>' +
|
|
1700
|
+
'<p class="settings-card-desc">至少 6 个字符;保存后下次登录生效。</p>' +
|
|
1701
|
+
'</div>' +
|
|
1702
|
+
'</div>' +
|
|
1621
1703
|
'<div class="field">' +
|
|
1622
1704
|
'<label class="field-label" for="new-password">新密码</label>' +
|
|
1623
1705
|
'<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
|
|
@@ -1626,22 +1708,45 @@
|
|
|
1626
1708
|
'<label class="field-label" for="confirm-password">确认密码</label>' +
|
|
1627
1709
|
'<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
|
|
1628
1710
|
'</div>' +
|
|
1629
|
-
'<
|
|
1711
|
+
'<div class="settings-card-actions">' +
|
|
1712
|
+
'<button id="save-password-button" class="btn btn-primary">保存密码</button>' +
|
|
1713
|
+
'</div>' +
|
|
1630
1714
|
'<p id="settings-error" class="error-message hidden"></p>' +
|
|
1631
|
-
'<p id="settings-success" class="hint hidden"
|
|
1715
|
+
'<p id="settings-success" class="hint settings-success-message hidden"></p>' +
|
|
1632
1716
|
'</div>' +
|
|
1633
1717
|
'<div class="settings-card">' +
|
|
1634
|
-
'<
|
|
1635
|
-
|
|
1718
|
+
'<div class="settings-card-head">' +
|
|
1719
|
+
'<span class="settings-card-icon">\ud83d\udd10</span>' +
|
|
1720
|
+
'<div class="settings-card-head-text">' +
|
|
1721
|
+
'<h3 class="settings-card-title">SSL 证书</h3>' +
|
|
1722
|
+
'<p class="settings-card-desc" id="cert-status">加载中...</p>' +
|
|
1723
|
+
'</div>' +
|
|
1724
|
+
'</div>' +
|
|
1636
1725
|
'<div class="field">' +
|
|
1637
1726
|
'<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
|
|
1638
|
-
'<
|
|
1727
|
+
'<div class="file-picker">' +
|
|
1728
|
+
'<input id="cert-key-file" type="file" class="file-picker-input" accept=".key,.pem" />' +
|
|
1729
|
+
'<label for="cert-key-file" class="file-picker-trigger">' +
|
|
1730
|
+
'<svg class="file-picker-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>' +
|
|
1731
|
+
'<span class="file-picker-label">选择私钥</span>' +
|
|
1732
|
+
'</label>' +
|
|
1733
|
+
'<span class="file-picker-name" data-default="未选择文件">未选择文件</span>' +
|
|
1734
|
+
'</div>' +
|
|
1639
1735
|
'</div>' +
|
|
1640
1736
|
'<div class="field">' +
|
|
1641
1737
|
'<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
|
|
1642
|
-
'<
|
|
1738
|
+
'<div class="file-picker">' +
|
|
1739
|
+
'<input id="cert-cert-file" type="file" class="file-picker-input" accept=".crt,.pem,.cert" />' +
|
|
1740
|
+
'<label for="cert-cert-file" class="file-picker-trigger">' +
|
|
1741
|
+
'<svg class="file-picker-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>' +
|
|
1742
|
+
'<span class="file-picker-label">选择证书</span>' +
|
|
1743
|
+
'</label>' +
|
|
1744
|
+
'<span class="file-picker-name" data-default="未选择文件">未选择文件</span>' +
|
|
1745
|
+
'</div>' +
|
|
1746
|
+
'</div>' +
|
|
1747
|
+
'<div class="settings-card-actions">' +
|
|
1748
|
+
'<button id="upload-cert-button" class="btn btn-primary">上传证书</button>' +
|
|
1643
1749
|
'</div>' +
|
|
1644
|
-
'<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
|
|
1645
1750
|
'<p id="cert-message" class="hint hidden"></p>' +
|
|
1646
1751
|
'</div>' +
|
|
1647
1752
|
'</div>' +
|
|
@@ -1857,8 +1962,12 @@
|
|
|
1857
1962
|
}
|
|
1858
1963
|
|
|
1859
1964
|
function getVisibleClaudeHistorySessions() {
|
|
1965
|
+
var managedIds = new Set();
|
|
1966
|
+
state.sessions.forEach(function(s) {
|
|
1967
|
+
if (s.claudeSessionId) managedIds.add(s.claudeSessionId);
|
|
1968
|
+
});
|
|
1860
1969
|
return state.claudeHistory.filter(function(s) {
|
|
1861
|
-
return s.hasConversation && !s.managedByWand;
|
|
1970
|
+
return s.hasConversation && !s.managedByWand && !managedIds.has(s.claudeSessionId);
|
|
1862
1971
|
});
|
|
1863
1972
|
}
|
|
1864
1973
|
|
|
@@ -3348,7 +3457,7 @@
|
|
|
3348
3457
|
// App icon picker (APK only)
|
|
3349
3458
|
var appIconPicker = document.getElementById("app-icon-picker");
|
|
3350
3459
|
if (appIconPicker) {
|
|
3351
|
-
var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
|
|
3460
|
+
var appIconOpts = appIconPicker.querySelectorAll(".settings-app-icon-option");
|
|
3352
3461
|
for (var ai = 0; ai < appIconOpts.length; ai++) {
|
|
3353
3462
|
appIconOpts[ai].addEventListener("click", function() {
|
|
3354
3463
|
var iconName = this.getAttribute("data-icon");
|
|
@@ -3369,6 +3478,24 @@
|
|
|
3369
3478
|
}
|
|
3370
3479
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
3371
3480
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
3481
|
+
var filePickerInputs = document.querySelectorAll(".file-picker-input");
|
|
3482
|
+
for (var fpi = 0; fpi < filePickerInputs.length; fpi++) {
|
|
3483
|
+
(function(input) {
|
|
3484
|
+
input.addEventListener("change", function() {
|
|
3485
|
+
var picker = input.closest(".file-picker");
|
|
3486
|
+
if (!picker) return;
|
|
3487
|
+
var nameEl = picker.querySelector(".file-picker-name");
|
|
3488
|
+
if (!nameEl) return;
|
|
3489
|
+
if (input.files && input.files[0]) {
|
|
3490
|
+
nameEl.textContent = input.files[0].name;
|
|
3491
|
+
picker.classList.add("file-picker-has-file");
|
|
3492
|
+
} else {
|
|
3493
|
+
nameEl.textContent = nameEl.getAttribute("data-default") || "未选择文件";
|
|
3494
|
+
picker.classList.remove("file-picker-has-file");
|
|
3495
|
+
}
|
|
3496
|
+
});
|
|
3497
|
+
})(filePickerInputs[fpi]);
|
|
3498
|
+
}
|
|
3372
3499
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
3373
3500
|
if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
|
|
3374
3501
|
var doUpdateBtn = document.getElementById("do-update-button");
|
|
@@ -3483,6 +3610,19 @@
|
|
|
3483
3610
|
} catch (_e) {}
|
|
3484
3611
|
}
|
|
3485
3612
|
}
|
|
3613
|
+
// Native haptic toggle (APK only)
|
|
3614
|
+
if (_hasNativeBridge && typeof WandNative.isHapticEnabled === "function") {
|
|
3615
|
+
var hapticSection = document.getElementById("native-haptic-section");
|
|
3616
|
+
var hapticToggle = document.getElementById("cfg-haptic-enabled");
|
|
3617
|
+
if (hapticSection && hapticToggle) {
|
|
3618
|
+
hapticSection.classList.remove("hidden");
|
|
3619
|
+
try { hapticToggle.checked = WandNative.isHapticEnabled(); } catch (_e) {}
|
|
3620
|
+
hapticToggle.addEventListener("change", function() {
|
|
3621
|
+
try { WandNative.setHapticEnabled(hapticToggle.checked); } catch (_e) {}
|
|
3622
|
+
if (hapticToggle.checked) _vibrate("medium");
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3486
3626
|
var newSessBtn = document.getElementById("topbar-new-session-button");
|
|
3487
3627
|
if (newSessBtn) newSessBtn.addEventListener("click", openSessionModal);
|
|
3488
3628
|
var drawerNewSessBtn = document.getElementById("drawer-new-session-button");
|
|
@@ -4245,16 +4385,14 @@
|
|
|
4245
4385
|
if (!badge) return;
|
|
4246
4386
|
var fullId = badge.dataset.claudeId;
|
|
4247
4387
|
if (!fullId) return;
|
|
4248
|
-
|
|
4249
|
-
|
|
4388
|
+
var original = badge.textContent;
|
|
4389
|
+
copyToClipboard(fullId, null, function() {
|
|
4250
4390
|
badge.textContent = "\u2713 已复制";
|
|
4251
4391
|
badge.classList.add("copied");
|
|
4252
4392
|
setTimeout(function() {
|
|
4253
4393
|
badge.textContent = original;
|
|
4254
4394
|
badge.classList.remove("copied");
|
|
4255
4395
|
}, 1200);
|
|
4256
|
-
}).catch(function() {
|
|
4257
|
-
showToast("复制失败", "error");
|
|
4258
4396
|
});
|
|
4259
4397
|
}
|
|
4260
4398
|
|
|
@@ -4538,6 +4676,9 @@
|
|
|
4538
4676
|
// Remeasure against real container: the refresh button used to only
|
|
4539
4677
|
// reset+write, so a stale cols/rows (set at mount time with hidden
|
|
4540
4678
|
// container) would survive the refresh and keep wrapping output wrong.
|
|
4679
|
+
// Suppress the auto-replay branch in ensureTerminalFit — we just
|
|
4680
|
+
// replayed, no point doing it again on the next rAF tick.
|
|
4681
|
+
state.suppressFitReplay = true;
|
|
4541
4682
|
ensureTerminalFit("refresh");
|
|
4542
4683
|
return true;
|
|
4543
4684
|
}
|
|
@@ -4662,7 +4803,16 @@
|
|
|
4662
4803
|
}
|
|
4663
4804
|
});
|
|
4664
4805
|
|
|
4665
|
-
|
|
4806
|
+
// Wait for the monospace webfont (if any) before init so the very first
|
|
4807
|
+
// _measureCharSize() inside wterm uses the final glyph metrics. Otherwise
|
|
4808
|
+
// the fallback font's narrower glyphs make wterm calculate too many cols,
|
|
4809
|
+
// and subsequent chunks render with broken wrapping until the user
|
|
4810
|
+
// triggers a resize. Cap the wait so a missing font never blocks startup.
|
|
4811
|
+
var fontsReady = (document.fonts && typeof document.fonts.ready === "object")
|
|
4812
|
+
? Promise.race([document.fonts.ready, new Promise(function(r) { setTimeout(r, 800); })])
|
|
4813
|
+
: Promise.resolve();
|
|
4814
|
+
|
|
4815
|
+
fontsReady.then(function() { return term.init(); }).then(function() {
|
|
4666
4816
|
state.terminal = term;
|
|
4667
4817
|
state.terminalInitializing = false;
|
|
4668
4818
|
applyTerminalScale();
|
|
@@ -4838,6 +4988,9 @@
|
|
|
4838
4988
|
: "向 Codex 发送输入;chat 为解析后的阅读视图";
|
|
4839
4989
|
}
|
|
4840
4990
|
if (session && !isStructuredSession(session) && session.status !== "running") {
|
|
4991
|
+
if (canAutoResumeSession(session)) {
|
|
4992
|
+
return "输入消息...";
|
|
4993
|
+
}
|
|
4841
4994
|
return "会话已结束,无法继续发送";
|
|
4842
4995
|
}
|
|
4843
4996
|
return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
|
|
@@ -5389,6 +5542,7 @@
|
|
|
5389
5542
|
flushCrossSessionQueue();
|
|
5390
5543
|
}
|
|
5391
5544
|
renderCrossSessionQueue();
|
|
5545
|
+
_syncWakeLock();
|
|
5392
5546
|
});
|
|
5393
5547
|
})
|
|
5394
5548
|
.catch(function(e) {
|
|
@@ -5948,6 +6102,12 @@
|
|
|
5948
6102
|
try { localStorage.setItem("wand-notif-volume", String(nativeVol)); } catch (_e) {}
|
|
5949
6103
|
} catch (_e) {}
|
|
5950
6104
|
}
|
|
6105
|
+
if (_hasNativeBridge && typeof WandNative.isHapticEnabled === "function") {
|
|
6106
|
+
try {
|
|
6107
|
+
var hapticEl = document.getElementById("cfg-haptic-enabled");
|
|
6108
|
+
if (hapticEl) hapticEl.checked = WandNative.isHapticEnabled();
|
|
6109
|
+
} catch (_e) {}
|
|
6110
|
+
}
|
|
5951
6111
|
}
|
|
5952
6112
|
}
|
|
5953
6113
|
|
|
@@ -6094,16 +6254,23 @@
|
|
|
6094
6254
|
}
|
|
6095
6255
|
|
|
6096
6256
|
|
|
6097
|
-
function copyToClipboard(text, triggerBtn) {
|
|
6257
|
+
function copyToClipboard(text, triggerBtn, successCallback) {
|
|
6098
6258
|
if (!text) return;
|
|
6099
|
-
|
|
6259
|
+
function onSuccess() {
|
|
6260
|
+
_vibrate("light");
|
|
6261
|
+
if (successCallback) { successCallback(); return; }
|
|
6100
6262
|
if (triggerBtn) {
|
|
6101
6263
|
var orig = triggerBtn.textContent;
|
|
6102
6264
|
triggerBtn.textContent = "已复制";
|
|
6103
6265
|
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
6104
6266
|
}
|
|
6105
|
-
}
|
|
6106
|
-
|
|
6267
|
+
}
|
|
6268
|
+
if (_hasNativeBridge && typeof WandNative.copyToClipboard === "function") {
|
|
6269
|
+
try {
|
|
6270
|
+
if (WandNative.copyToClipboard(text) === "ok") { onSuccess(); return; }
|
|
6271
|
+
} catch (_e) {}
|
|
6272
|
+
}
|
|
6273
|
+
navigator.clipboard.writeText(text).then(onSuccess).catch(function() {
|
|
6107
6274
|
var ta = document.createElement("textarea");
|
|
6108
6275
|
ta.value = text;
|
|
6109
6276
|
ta.style.position = "fixed";
|
|
@@ -6112,11 +6279,7 @@
|
|
|
6112
6279
|
ta.select();
|
|
6113
6280
|
document.execCommand("copy");
|
|
6114
6281
|
document.body.removeChild(ta);
|
|
6115
|
-
|
|
6116
|
-
var orig = triggerBtn.textContent;
|
|
6117
|
-
triggerBtn.textContent = "已复制";
|
|
6118
|
-
setTimeout(function() { triggerBtn.textContent = orig; }, 1500);
|
|
6119
|
-
}
|
|
6282
|
+
onSuccess();
|
|
6120
6283
|
});
|
|
6121
6284
|
}
|
|
6122
6285
|
|
|
@@ -6181,6 +6344,11 @@
|
|
|
6181
6344
|
var apkMessageEl = document.getElementById("android-apk-message");
|
|
6182
6345
|
var androidApk = data.androidApk || {};
|
|
6183
6346
|
var isInApk = !!_apkVersion;
|
|
6347
|
+
var hasApkInfo = isInApk || !!androidApk.github || !!androidApk.local;
|
|
6348
|
+
if (apkSection) {
|
|
6349
|
+
if (hasApkInfo) apkSection.classList.remove("hidden");
|
|
6350
|
+
else apkSection.classList.add("hidden");
|
|
6351
|
+
}
|
|
6184
6352
|
|
|
6185
6353
|
if (isInApk) {
|
|
6186
6354
|
// ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
|
|
@@ -6624,12 +6792,11 @@
|
|
|
6624
6792
|
// ── Notification Settings Helpers ──
|
|
6625
6793
|
|
|
6626
6794
|
function _updateAppIconSelection(activeIcon) {
|
|
6627
|
-
var opts = document.querySelectorAll(".app-icon-option");
|
|
6795
|
+
var opts = document.querySelectorAll(".settings-app-icon-option");
|
|
6628
6796
|
for (var i = 0; i < opts.length; i++) {
|
|
6629
|
-
var
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
}
|
|
6797
|
+
var isActive = opts[i].getAttribute("data-icon") === activeIcon;
|
|
6798
|
+
opts[i].classList.toggle("selected", isActive);
|
|
6799
|
+
opts[i].setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
6633
6800
|
}
|
|
6634
6801
|
}
|
|
6635
6802
|
|
|
@@ -8214,7 +8381,6 @@
|
|
|
8214
8381
|
if (!data) return null;
|
|
8215
8382
|
updateSessionSnapshot(data);
|
|
8216
8383
|
updateSessionsList();
|
|
8217
|
-
switchToSessionView(data.id);
|
|
8218
8384
|
subscribeToSession(data.id);
|
|
8219
8385
|
return loadOutput(data.id).then(function() {
|
|
8220
8386
|
focusInputBox(true);
|
|
@@ -8985,6 +9151,9 @@
|
|
|
8985
9151
|
else showToast(data.error, "error");
|
|
8986
9152
|
return null;
|
|
8987
9153
|
}
|
|
9154
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
9155
|
+
return s.claudeSessionId !== claudeSessionId;
|
|
9156
|
+
});
|
|
8988
9157
|
state.selectedId = data.id;
|
|
8989
9158
|
persistSelectedId();
|
|
8990
9159
|
state.drafts[data.id] = "";
|
|
@@ -9014,6 +9183,11 @@
|
|
|
9014
9183
|
console.log("[WAND] resumeSessionFromList sessionId:", sessionId);
|
|
9015
9184
|
return resumeSession(sessionId).then(function(data) {
|
|
9016
9185
|
if (!data) return null;
|
|
9186
|
+
if (data.claudeSessionId) {
|
|
9187
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
9188
|
+
return s.claudeSessionId !== data.claudeSessionId;
|
|
9189
|
+
});
|
|
9190
|
+
}
|
|
9017
9191
|
return activateSession(data).then(function() {
|
|
9018
9192
|
return data;
|
|
9019
9193
|
});
|
|
@@ -9121,11 +9295,13 @@
|
|
|
9121
9295
|
resumeClaudeHistorySession(claudeSessionId, cwd)
|
|
9122
9296
|
.then(function(data) {
|
|
9123
9297
|
if (data && data.id) {
|
|
9298
|
+
state.claudeHistory = state.claudeHistory.filter(function(s) {
|
|
9299
|
+
return s.claudeSessionId !== claudeSessionId;
|
|
9300
|
+
});
|
|
9124
9301
|
state.selectedId = data.id;
|
|
9125
9302
|
persistSelectedId();
|
|
9126
9303
|
state.drafts[data.id] = "";
|
|
9127
|
-
|
|
9128
|
-
selectSession(data.id);
|
|
9304
|
+
activateSession(data).then(function() {
|
|
9129
9305
|
closeSessionsDrawer();
|
|
9130
9306
|
});
|
|
9131
9307
|
}
|
|
@@ -9971,15 +10147,30 @@
|
|
|
9971
10147
|
syncInputBoxScroll(inputBox);
|
|
9972
10148
|
}
|
|
9973
10149
|
|
|
10150
|
+
// Keyboard just opened — terminal viewport now shares space with
|
|
10151
|
+
// the keyboard; visible rows shrink even if cols stayed the same.
|
|
10152
|
+
// Without an immediate refit, any chunk arriving while the keyboard
|
|
10153
|
+
// animates in renders against the old grid and tears the screen.
|
|
10154
|
+
if (!keyboardOpen && isKeyboardOpen) {
|
|
10155
|
+
ensureTerminalFit("keyboard-open");
|
|
10156
|
+
}
|
|
10157
|
+
|
|
9974
10158
|
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
9975
10159
|
// after a delay so the keyboard dismiss animation and layout settle.
|
|
9976
10160
|
if (keyboardOpen && !isKeyboardOpen) {
|
|
9977
10161
|
setTimeout(function() {
|
|
9978
|
-
ensureTerminalFit();
|
|
10162
|
+
ensureTerminalFit("keyboard-close");
|
|
9979
10163
|
maybeScrollTerminalToBottom("force");
|
|
9980
10164
|
}, 200);
|
|
9981
10165
|
}
|
|
9982
10166
|
|
|
10167
|
+
// visualViewport height changed without a keyboard transition —
|
|
10168
|
+
// covers iOS address-bar collapse/expand and split-screen drag.
|
|
10169
|
+
// Cheap to call: ensureTerminalFit early-exits if cols/rows stable.
|
|
10170
|
+
if (heightChanged && keyboardOpen === isKeyboardOpen) {
|
|
10171
|
+
ensureTerminalFit("viewport");
|
|
10172
|
+
}
|
|
10173
|
+
|
|
9983
10174
|
keyboardOpen = isKeyboardOpen;
|
|
9984
10175
|
lastHeight = vv.height;
|
|
9985
10176
|
}
|
|
@@ -10236,6 +10427,8 @@
|
|
|
10236
10427
|
if (!state.terminal) return false;
|
|
10237
10428
|
var el = document.getElementById("output");
|
|
10238
10429
|
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) return false;
|
|
10430
|
+
var prevCols = state.terminal.cols;
|
|
10431
|
+
var prevRows = state.terminal.rows;
|
|
10239
10432
|
requestAnimationFrame(function() {
|
|
10240
10433
|
requestAnimationFrame(function() {
|
|
10241
10434
|
if (!state.terminal) return;
|
|
@@ -10243,6 +10436,21 @@
|
|
|
10243
10436
|
state.terminal.remeasure();
|
|
10244
10437
|
}
|
|
10245
10438
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
10439
|
+
// Cache the container width that produced this cols/rows so the
|
|
10440
|
+
// hot-path chunk writer can detect drift cheaply (avoids running
|
|
10441
|
+
// a full remeasure on every WebSocket message).
|
|
10442
|
+
state.lastFitContainerWidth = el.offsetWidth;
|
|
10443
|
+
state.lastFitContainerHeight = el.offsetHeight;
|
|
10444
|
+
// If cols actually changed, the previously written buffer was
|
|
10445
|
+
// wrapped to the old width. Replay the full buffer so historical
|
|
10446
|
+
// lines and any in-flight CSI cursor sequences re-render against
|
|
10447
|
+
// the new grid — this is what fixes the "torn" screens users see
|
|
10448
|
+
// after rotating, opening the keyboard, or resizing the panel.
|
|
10449
|
+
var skipReplay = state.suppressFitReplay === true;
|
|
10450
|
+
state.suppressFitReplay = false;
|
|
10451
|
+
if (!skipReplay && (state.terminal.cols !== prevCols || state.terminal.rows !== prevRows)) {
|
|
10452
|
+
if (state.terminalOutput) softResyncTerminal();
|
|
10453
|
+
}
|
|
10246
10454
|
if (state.terminalAutoFollow || isTerminalNearBottom()) {
|
|
10247
10455
|
maybeScrollTerminalToBottom("resize");
|
|
10248
10456
|
}
|
|
@@ -10251,6 +10459,43 @@
|
|
|
10251
10459
|
return true;
|
|
10252
10460
|
}
|
|
10253
10461
|
|
|
10462
|
+
// Cheap cols/rows drift check — call before writing a new PTY chunk so
|
|
10463
|
+
// the chunk renders against the correct grid even if ResizeObserver
|
|
10464
|
+
// hasn't fired yet (e.g. mobile keyboard mid-animation, iOS PWA address
|
|
10465
|
+
// bar collapse, panel drag in progress). Only runs a real remeasure
|
|
10466
|
+
// when the container width changed since the last fit; otherwise it is
|
|
10467
|
+
// effectively a single offsetWidth read.
|
|
10468
|
+
function maybeRefitTerminal() {
|
|
10469
|
+
if (!state.terminal) return;
|
|
10470
|
+
var el = document.getElementById("output");
|
|
10471
|
+
if (!el) return;
|
|
10472
|
+
var w = el.offsetWidth;
|
|
10473
|
+
var h = el.offsetHeight;
|
|
10474
|
+
if (w === 0 || h === 0) return;
|
|
10475
|
+
// First call: just record the baseline and let ensureTerminalFit
|
|
10476
|
+
// (called from initTerminal/mount) own the initial sync.
|
|
10477
|
+
if (state.lastFitContainerWidth === undefined) {
|
|
10478
|
+
state.lastFitContainerWidth = w;
|
|
10479
|
+
state.lastFitContainerHeight = h;
|
|
10480
|
+
return;
|
|
10481
|
+
}
|
|
10482
|
+
if (w === state.lastFitContainerWidth && h === state.lastFitContainerHeight) return;
|
|
10483
|
+
state.lastFitContainerWidth = w;
|
|
10484
|
+
state.lastFitContainerHeight = h;
|
|
10485
|
+
var prevCols = state.terminal.cols;
|
|
10486
|
+
var prevRows = state.terminal.rows;
|
|
10487
|
+
if (typeof state.terminal.remeasure === "function") {
|
|
10488
|
+
state.terminal.remeasure();
|
|
10489
|
+
}
|
|
10490
|
+
if (state.terminal.cols !== prevCols || state.terminal.rows !== prevRows) {
|
|
10491
|
+
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
10492
|
+
// Don't replay here: the caller is about to write a fresh chunk and
|
|
10493
|
+
// a softResync would race with it. The chunk itself will reach the
|
|
10494
|
+
// correct grid; older buffer drift is repaired by the next
|
|
10495
|
+
// ensureTerminalFit / health check / manual refresh.
|
|
10496
|
+
}
|
|
10497
|
+
}
|
|
10498
|
+
|
|
10254
10499
|
function scheduleTerminalResize(immediate) {
|
|
10255
10500
|
if (state.resizeTimer) {
|
|
10256
10501
|
clearTimeout(state.resizeTimer);
|
|
@@ -10430,6 +10675,10 @@
|
|
|
10430
10675
|
// Fast path: write chunk directly to avoid full-output comparison.
|
|
10431
10676
|
state.lastChunkAt = Date.now();
|
|
10432
10677
|
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
10678
|
+
// Detect cheap container-width drift before applying the chunk
|
|
10679
|
+
// so absolute-cursor CSI sequences in the chunk land on the
|
|
10680
|
+
// right grid (otherwise content tears or stacks at the top).
|
|
10681
|
+
maybeRefitTerminal();
|
|
10433
10682
|
state.terminal.write(msg.data.chunk);
|
|
10434
10683
|
state.terminalSessionId = msg.sessionId;
|
|
10435
10684
|
if (msg.data.output) {
|
|
@@ -10489,8 +10738,10 @@
|
|
|
10489
10738
|
} else {
|
|
10490
10739
|
endedNotifBody = endedSession ? (endedSession.command || msg.sessionId) : msg.sessionId;
|
|
10491
10740
|
}
|
|
10741
|
+
_vibrate(endedIsError ? "error" : "success");
|
|
10492
10742
|
notifyTaskEnded(msg.sessionId, endedNotifTitle, endedNotifBody);
|
|
10493
10743
|
clearSessionProgressNative(msg.sessionId);
|
|
10744
|
+
_syncWakeLock();
|
|
10494
10745
|
if (msg.sessionId !== state.selectedId || document.hidden) {
|
|
10495
10746
|
showNotificationBubble({
|
|
10496
10747
|
title: endedNotifTitle,
|
|
@@ -10629,6 +10880,7 @@
|
|
|
10629
10880
|
} else {
|
|
10630
10881
|
permBody += "\n" + permDetail;
|
|
10631
10882
|
}
|
|
10883
|
+
_vibrate("medium");
|
|
10632
10884
|
notifyPermissionRequest(msg.sessionId, permBody);
|
|
10633
10885
|
// In-app bubble if not currently viewing this session
|
|
10634
10886
|
if (msg.sessionId !== state.selectedId) {
|
|
@@ -10653,6 +10905,7 @@
|
|
|
10653
10905
|
}
|
|
10654
10906
|
updateSessionSnapshot(statusUpdate);
|
|
10655
10907
|
syncSessionProgressToNative(msg.sessionId);
|
|
10908
|
+
_syncWakeLock();
|
|
10656
10909
|
if (msg.sessionId === state.selectedId) {
|
|
10657
10910
|
updateTaskDisplay();
|
|
10658
10911
|
if (msg.data.approvalStats) {
|
|
@@ -10777,6 +11030,7 @@
|
|
|
10777
11030
|
}
|
|
10778
11031
|
|
|
10779
11032
|
function approvePermission() {
|
|
11033
|
+
_vibrate("light");
|
|
10780
11034
|
if (!state.selectedId) return;
|
|
10781
11035
|
var approveBtn = document.getElementById("approve-permission-btn");
|
|
10782
11036
|
var denyBtn = document.getElementById("deny-permission-btn");
|
|
@@ -10805,6 +11059,7 @@
|
|
|
10805
11059
|
}
|
|
10806
11060
|
|
|
10807
11061
|
function denyPermission() {
|
|
11062
|
+
_vibrate("light");
|
|
10808
11063
|
if (!state.selectedId) return;
|
|
10809
11064
|
var approveBtn = document.getElementById("approve-permission-btn");
|
|
10810
11065
|
var denyBtn = document.getElementById("deny-permission-btn");
|
|
@@ -11507,13 +11762,10 @@
|
|
|
11507
11762
|
var codeBlock = btn.closest(".code-block");
|
|
11508
11763
|
var code = codeBlock ? codeBlock.querySelector("code") : null;
|
|
11509
11764
|
if (code) {
|
|
11510
|
-
|
|
11765
|
+
copyToClipboard(code.textContent || "", null, function() {
|
|
11511
11766
|
btn.textContent = "Copied!";
|
|
11512
11767
|
btn.classList.add("copied");
|
|
11513
|
-
setTimeout(function() {
|
|
11514
|
-
btn.textContent = "Copy";
|
|
11515
|
-
btn.classList.remove("copied");
|
|
11516
|
-
}, 2000);
|
|
11768
|
+
setTimeout(function() { btn.textContent = "Copy"; btn.classList.remove("copied"); }, 2000);
|
|
11517
11769
|
});
|
|
11518
11770
|
}
|
|
11519
11771
|
});
|
|
@@ -11522,25 +11774,20 @@
|
|
|
11522
11774
|
|
|
11523
11775
|
function attachAllCopyHandlers(container) {
|
|
11524
11776
|
container.querySelectorAll(".code-copy").forEach(function(btn) {
|
|
11525
|
-
// Remove existing listeners by cloning
|
|
11526
11777
|
var clone = btn.cloneNode(true);
|
|
11527
11778
|
btn.parentNode.replaceChild(clone, btn);
|
|
11528
11779
|
clone.addEventListener("click", function() {
|
|
11529
11780
|
var codeBlock = clone.closest(".code-block");
|
|
11530
11781
|
var code = codeBlock ? codeBlock.querySelector("code") : null;
|
|
11531
11782
|
if (code) {
|
|
11532
|
-
|
|
11783
|
+
copyToClipboard(code.textContent || "", null, function() {
|
|
11533
11784
|
clone.textContent = "Copied!";
|
|
11534
11785
|
clone.classList.add("copied");
|
|
11535
|
-
setTimeout(function() {
|
|
11536
|
-
clone.textContent = "Copy";
|
|
11537
|
-
clone.classList.remove("copied");
|
|
11538
|
-
}, 2000);
|
|
11786
|
+
setTimeout(function() { clone.textContent = "Copy"; clone.classList.remove("copied"); }, 2000);
|
|
11539
11787
|
});
|
|
11540
11788
|
}
|
|
11541
11789
|
});
|
|
11542
11790
|
});
|
|
11543
|
-
// Attach message-level copy buttons for touch devices
|
|
11544
11791
|
attachMessageCopyButtons(container);
|
|
11545
11792
|
}
|
|
11546
11793
|
|
|
@@ -11560,7 +11807,7 @@
|
|
|
11560
11807
|
btn.addEventListener("click", function(e) {
|
|
11561
11808
|
e.stopPropagation();
|
|
11562
11809
|
var text = bubble.innerText || bubble.textContent || "";
|
|
11563
|
-
|
|
11810
|
+
copyToClipboard(text.trim(), null, function() {
|
|
11564
11811
|
btn.textContent = "已复制";
|
|
11565
11812
|
btn.classList.add("copied");
|
|
11566
11813
|
setTimeout(function() {
|
|
@@ -13629,6 +13876,30 @@
|
|
|
13629
13876
|
var _apkVersionMatch = navigator.userAgent.match(/WandApp\/([^\s]+)/);
|
|
13630
13877
|
var _apkVersion = _apkVersionMatch ? _apkVersionMatch[1] : null;
|
|
13631
13878
|
|
|
13879
|
+
function _vibrate(pattern) {
|
|
13880
|
+
if (!_hasNativeBridge || typeof WandNative.vibrate !== "function") return;
|
|
13881
|
+
try { WandNative.vibrate(pattern || "light"); } catch (_e) {}
|
|
13882
|
+
}
|
|
13883
|
+
|
|
13884
|
+
function _syncWakeLock() {
|
|
13885
|
+
if (!_hasNativeBridge) return;
|
|
13886
|
+
var anyActive = state.sessions.some(function(s) {
|
|
13887
|
+
return !s.archived && (s.status === "running" || s.status === "thinking" || s.status === "initializing");
|
|
13888
|
+
});
|
|
13889
|
+
if (typeof WandNative.setKeepScreenOn === "function") {
|
|
13890
|
+
try { WandNative.setKeepScreenOn(anyActive); } catch (_e) {}
|
|
13891
|
+
}
|
|
13892
|
+
if (anyActive) {
|
|
13893
|
+
if (typeof WandNative.startKeepAlive === "function") {
|
|
13894
|
+
try { WandNative.startKeepAlive(); } catch (_e) {}
|
|
13895
|
+
}
|
|
13896
|
+
} else {
|
|
13897
|
+
if (typeof WandNative.stopKeepAlive === "function") {
|
|
13898
|
+
try { WandNative.stopKeepAlive(); } catch (_e) {}
|
|
13899
|
+
}
|
|
13900
|
+
}
|
|
13901
|
+
}
|
|
13902
|
+
|
|
13632
13903
|
function _getNativePermission() {
|
|
13633
13904
|
if (_hasNativeBridge && typeof WandNative.getPermission === "function") {
|
|
13634
13905
|
try { return WandNative.getPermission(); } catch (_e) {}
|
|
@@ -13848,6 +14119,41 @@
|
|
|
13848
14119
|
try { WandNative.clearSessionProgress(sessionId); } catch (_e) {}
|
|
13849
14120
|
}
|
|
13850
14121
|
|
|
14122
|
+
// ── Android back button handler ──
|
|
14123
|
+
window.handleNativeBack = function() {
|
|
14124
|
+
var settingsModal = document.getElementById("settings-modal");
|
|
14125
|
+
if (settingsModal && !settingsModal.classList.contains("hidden")) {
|
|
14126
|
+
closeSettingsModal();
|
|
14127
|
+
return true;
|
|
14128
|
+
}
|
|
14129
|
+
var sessionModal = document.getElementById("session-modal");
|
|
14130
|
+
if (sessionModal && !sessionModal.classList.contains("hidden")) {
|
|
14131
|
+
closeSessionModal();
|
|
14132
|
+
return true;
|
|
14133
|
+
}
|
|
14134
|
+
var worktreeModal = document.getElementById("worktree-merge-modal");
|
|
14135
|
+
if (worktreeModal && !worktreeModal.classList.contains("hidden")) {
|
|
14136
|
+
closeWorktreeMergeModal();
|
|
14137
|
+
return true;
|
|
14138
|
+
}
|
|
14139
|
+
if (state.filePanelOpen && isMobileLayout()) {
|
|
14140
|
+
setFilePanelOpen(false);
|
|
14141
|
+
return true;
|
|
14142
|
+
}
|
|
14143
|
+
if (state.sessionsDrawerOpen && isMobileLayout()) {
|
|
14144
|
+
closeSessionsDrawer();
|
|
14145
|
+
return true;
|
|
14146
|
+
}
|
|
14147
|
+
if (isMobileLayout() && state.selectedId) {
|
|
14148
|
+
state.selectedId = null;
|
|
14149
|
+
persistSelectedId();
|
|
14150
|
+
state.sessionsDrawerOpen = true;
|
|
14151
|
+
render();
|
|
14152
|
+
return true;
|
|
14153
|
+
}
|
|
14154
|
+
return false;
|
|
14155
|
+
};
|
|
14156
|
+
|
|
13851
14157
|
/**
|
|
13852
14158
|
* Play a soft, rounded notification chime using Web Audio API.
|
|
13853
14159
|
* Two ascending sine tones with smooth gain envelope — gentle on the ears.
|