@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.
@@ -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.textContent = enabled ? "追底" : "暂停";
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="回到底部">↓ 最新</button>' +
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') + '" title="' + (state.chatAutoFollow ? '追踪底部:开启' : '追踪底部:已暂停') + '">' + (state.chatAutoFollow ? '追底' : '暂停') + '</button>' +
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="回到底部并继续追底">↓ 最新</button>' +
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-ghost btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
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-auto-update-row" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
1424
- '<span class="settings-label">自动更新</span>' +
1425
- '<label style="position:relative;cursor:pointer">' +
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" style="flex:1">-</span>' +
1440
- '<button id="download-github-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
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" style="flex:1">-</span>' +
1445
- '<button id="download-local-apk-btn" class="btn btn-ghost btn-sm hidden" type="button" style="margin-left:8px;flex-shrink:0">下载</button>' +
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="hidden" style="display:flex;align-items:center;justify-content:space-between;margin-top:10px">' +
1448
- '<span class="settings-label">自动更新</span>' +
1449
- '<label style="position:relative;cursor:pointer">' +
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-title" style="margin-bottom:8px">App 连接码</div>' +
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" style="font-size:12px;word-break:break-all">-</code>' +
1461
- '<button id="copy-connect-code-button" class="btn btn-ghost btn-sm" type="button" title="复制连接码">复制</button>' +
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-title">\u901a\u77e5\u504f\u597d</div>' +
1474
- '<div class="field field-inline">' +
1475
- '<input id="cfg-notif-sound" type="checkbox" class="field-checkbox" />' +
1476
- '<label class="field-label" for="cfg-notif-sound">\u64ad\u653e\u63d0\u793a\u97f3</label>' +
1477
- '</div>' +
1478
- '<p class="hint" style="margin-top:0;margin-bottom:10px">\u91cd\u8981\u901a\u77e5\uff08\u7248\u672c\u66f4\u65b0\u3001\u6743\u9650\u7b49\u5f85\u7b49\uff09\u65f6\u64ad\u653e\u67d4\u548c\u7684\u63d0\u793a\u97f3</p>' +
1479
- '<div class="field" id="notif-volume-field" style="margin-bottom:10px">' +
1480
- '<label class="field-label" style="margin-bottom:4px">\u97f3\u91cf</label>' +
1481
- '<div style="display:flex;align-items:center;gap:8px">' +
1482
- '<input id="cfg-notif-volume" type="range" min="0" max="100" step="5" style="flex:1;accent-color:var(--accent)" />' +
1483
- '<span id="cfg-notif-volume-val" style="min-width:32px;text-align:right;font-size:12px;color:var(--text-secondary)">80%</span>' +
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="field field-inline">' +
1487
- '<input id="cfg-notif-bubble" type="checkbox" class="field-checkbox" />' +
1488
- '<label class="field-label" for="cfg-notif-bubble">\u5e94\u7528\u5185\u901a\u77e5\u6c14\u6ce1</label>' +
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
- '<p class="hint" style="margin-top:0;margin-bottom:10px">\u5728\u9875\u9762\u9876\u90e8\u5f39\u51fa\u6d6e\u52a8\u901a\u77e5\u6c14\u6ce1</p>' +
1491
- '<div id="native-sound-section" class="settings-notification-section hidden" style="margin-top:6px">' +
1492
- '<div class="settings-section-title">\u7cfb\u7edf\u901a\u77e5\u94c3\u58f0</div>' +
1493
- '<div class="settings-about-row">' +
1494
- '<span class="settings-label">\u94c3\u58f0</span>' +
1495
- '<div style="display:flex;align-items:center;gap:6px">' +
1496
- '<select id="native-sound-select" class="field-select" style="min-width:100px"></select>' +
1497
- '<button id="native-sound-preview" class="btn btn-ghost btn-sm">\u25b6 \u8bd5\u542c</button>' +
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" style="margin-top:6px">' +
1503
- '<div class="settings-section-title">\u6d4f\u89c8\u5668\u901a\u77e5</div>' +
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">\u6388\u6743\u72b6\u6001</span>' +
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-ghost btn-sm hidden">\u6388\u6743\u901a\u77e5</button>' +
1510
- '<button id="notification-reset-btn" class="btn btn-ghost btn-sm hidden">\u91cd\u65b0\u6388\u6743</button>' +
1511
- '<button id="notification-test-btn" class="btn btn-ghost btn-sm">\u53d1\u9001\u6d4b\u8bd5\u901a\u77e5</button>' +
1512
- '<button id="notification-test-delay-btn" class="btn btn-ghost btn-sm">10 \u79d2\u540e\u53d1\u9001</button>' +
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 style="display:flex;gap:8px;align-items:center;">' +
1571
- '<select id="cfg-default-model" class="field-input" style="flex:1;">' +
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-ghost btn-sm" title="刷新模型列表">刷新</button>' +
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" style="margin-top:4px;">新建会话时默认使用该模型;运行中的会话可在输入框切换。</p>' +
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 style="margin-bottom:16px">' +
1588
- '<div class="settings-section-title">应用图标</div>' +
1589
- '<p class="hint" style="margin-top:-4px;margin-bottom:10px">选择 App 启动器图标,返回桌面后生效</p>' +
1590
- '<div id="app-icon-picker" style="display:flex;gap:16px">' +
1591
- '<div class="app-icon-option" data-icon="shorthair" style="cursor:pointer;text-align:center">' +
1592
- '<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">' +
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
- '<div class="app-icon-option" data-icon="garfield" style="cursor:pointer;text-align:center">' +
1598
- '<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">' +
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
- '</div>' +
1601
- '<span style="font-size:0.72rem;color:var(--text-secondary)">勤劳初二</span>' +
1602
- '</div>' +
1676
+ '</span>' +
1677
+ '<span class="settings-app-icon-label">勤劳初二</span>' +
1678
+ '</button>' +
1603
1679
  '</div>' +
1604
- '<p id="app-icon-message" class="hint hidden" style="margin-top:8px"></p>' +
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
- '<h3 class="settings-section-title">\ud83d\udd12 修改密码</h3>' +
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
- '<button id="save-password-button" class="btn btn-primary btn-block">保存密码</button>' +
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" style="color: var(--success);"></p>' +
1715
+ '<p id="settings-success" class="hint settings-success-message hidden"></p>' +
1632
1716
  '</div>' +
1633
1717
  '<div class="settings-card">' +
1634
- '<h3 class="settings-section-title">\ud83d\udd10 SSL 证书</h3>' +
1635
- '<p class="settings-hint" id="cert-status">加载中...</p>' +
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
- '<input id="cert-key-file" type="file" class="field-input field-file" accept=".key,.pem" />' +
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
- '<input id="cert-cert-file" type="file" class="field-input field-file" accept=".crt,.pem,.cert" />' +
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
- navigator.clipboard.writeText(fullId).then(function() {
4249
- var original = badge.textContent;
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
- term.init().then(function() {
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
- navigator.clipboard.writeText(text).then(function() {
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
- }).catch(function() {
6106
- // Fallback for non-secure contexts
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
- if (triggerBtn) {
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 preview = opts[i].querySelector(".app-icon-preview");
6630
- if (preview) {
6631
- preview.style.borderColor = opts[i].getAttribute("data-icon") === activeIcon ? "var(--accent)" : "transparent";
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
- loadSessions().then(function() {
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
- navigator.clipboard.writeText(code.textContent || "").then(function() {
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
- navigator.clipboard.writeText(code.textContent || "").then(function() {
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
- navigator.clipboard.writeText(text.trim()).then(function() {
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.