@co0ontty/wand 1.18.0 → 1.18.12
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 +2 -1
- package/dist/process-manager.js +14 -34
- package/dist/pty-text-utils.d.ts +1 -3
- package/dist/pty-text-utils.js +1 -3
- package/dist/server-session-routes.js +6 -12
- package/dist/storage.js +4 -8
- package/dist/structured-session-manager.d.ts +6 -1
- package/dist/structured-session-manager.js +156 -13
- package/dist/types.d.ts +0 -14
- package/dist/web-ui/content/scripts.js +768 -196
- package/dist/web-ui/content/styles.css +634 -39
- package/package.json +1 -1
|
@@ -167,6 +167,7 @@
|
|
|
167
167
|
return false;
|
|
168
168
|
}
|
|
169
169
|
})(),
|
|
170
|
+
topbarMoreOpen: false,
|
|
170
171
|
chatAutoFollow: (function() {
|
|
171
172
|
try {
|
|
172
173
|
var saved = localStorage.getItem(CHAT_AUTO_FOLLOW_STORAGE_KEY);
|
|
@@ -182,6 +183,8 @@
|
|
|
182
183
|
chatScrollHandler: null,
|
|
183
184
|
lastForegroundSyncAt: 0,
|
|
184
185
|
foregroundSyncTimer: null,
|
|
186
|
+
wsReconnectAttempts: 0,
|
|
187
|
+
wsReconnectTimer: null,
|
|
185
188
|
currentMessages: [],
|
|
186
189
|
lastRenderedHash: 0,
|
|
187
190
|
lastRenderedMsgCount: 0,
|
|
@@ -337,8 +340,11 @@
|
|
|
337
340
|
var enabled = !!state.chatAutoFollow;
|
|
338
341
|
button.classList.toggle("active", enabled);
|
|
339
342
|
button.setAttribute("aria-pressed", enabled ? "true" : "false");
|
|
340
|
-
button.setAttribute("title", enabled ? "
|
|
341
|
-
button.
|
|
343
|
+
button.setAttribute("title", enabled ? "追踪底部:开启(点击暂停)" : "追踪底部:已暂停(点击开启)");
|
|
344
|
+
button.setAttribute("aria-label", enabled ? "追踪底部:开启" : "追踪底部:已暂停");
|
|
345
|
+
button.innerHTML = enabled
|
|
346
|
+
? '<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>'
|
|
347
|
+
: '<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
348
|
}
|
|
343
349
|
|
|
344
350
|
function updateChatJumpToBottomButton() {
|
|
@@ -799,14 +805,11 @@
|
|
|
799
805
|
|
|
800
806
|
function updateInstallPrompt() {
|
|
801
807
|
// 显示或隐藏菜单栏中的安装按钮
|
|
808
|
+
var visible = !!(state.showInstallPrompt && state.deferredPrompt);
|
|
802
809
|
var installBtn = document.getElementById('pwa-install-button');
|
|
803
|
-
if (installBtn)
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
} else {
|
|
807
|
-
installBtn.classList.add('hidden');
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
+
if (installBtn) installBtn.classList.toggle('hidden', !visible);
|
|
811
|
+
var topbarInstallItem = document.getElementById('topbar-install-item');
|
|
812
|
+
if (topbarInstallItem) topbarInstallItem.classList.toggle('hidden', !visible);
|
|
810
813
|
}
|
|
811
814
|
|
|
812
815
|
function renderBootLoading() {
|
|
@@ -821,36 +824,48 @@
|
|
|
821
824
|
'</div>';
|
|
822
825
|
}
|
|
823
826
|
|
|
824
|
-
function scheduleForegroundSync(reason) {
|
|
827
|
+
function scheduleForegroundSync(reason, opts) {
|
|
825
828
|
if (!state.config) return;
|
|
826
829
|
if (document.hidden) return;
|
|
830
|
+
var immediate = opts && opts.immediate === true;
|
|
827
831
|
var now = Date.now();
|
|
828
|
-
|
|
832
|
+
// 节流只是为了防止 visibilitychange/focus/pageshow 在前台切换时
|
|
833
|
+
// 连珠炮式触发同一份重连工作,不再借此延迟实际同步——之前用
|
|
834
|
+
// 80ms 兜延迟的版本会在前台事件后再去 loadOutput 全量重写
|
|
835
|
+
// terminal,但 wterm cols 那时还没被 ResizeObserver 自适应到,
|
|
836
|
+
// 写进去的全是按错列宽排版的内容,结果"切回前台/刷新页面 →
|
|
837
|
+
// 中间一大段都看不到"反而成了常态。
|
|
838
|
+
if (!immediate && now - state.lastForegroundSyncAt < 1500) return;
|
|
829
839
|
state.lastForegroundSyncAt = now;
|
|
830
840
|
if (state.foregroundSyncTimer) {
|
|
831
841
|
clearTimeout(state.foregroundSyncTimer);
|
|
832
|
-
}
|
|
833
|
-
state.foregroundSyncTimer = setTimeout(function() {
|
|
834
842
|
state.foregroundSyncTimer = null;
|
|
835
|
-
|
|
836
|
-
|
|
843
|
+
}
|
|
844
|
+
syncOnForeground(reason, immediate);
|
|
837
845
|
}
|
|
838
846
|
|
|
839
|
-
function syncOnForeground(reason) {
|
|
847
|
+
function syncOnForeground(reason, force) {
|
|
840
848
|
if (!state.config) return Promise.resolve();
|
|
841
849
|
if (document.hidden) return Promise.resolve();
|
|
842
|
-
|
|
850
|
+
// On Android resume the previous WS may still report OPEN/CONNECTING
|
|
851
|
+
// for a few seconds because the close frame hasn't been delivered
|
|
852
|
+
// yet (TCP keepalive / Doze suspended the network stack). Force a
|
|
853
|
+
// fresh socket so we don't sit on a zombie connection.
|
|
854
|
+
if (force) {
|
|
855
|
+
forceReconnectWebSocket("resume-force");
|
|
856
|
+
} else if (!state.ws || (state.ws.readyState !== WebSocket.OPEN && state.ws.readyState !== WebSocket.CONNECTING)) {
|
|
843
857
|
initWebSocket();
|
|
844
858
|
}
|
|
845
859
|
if (state.claudeHistoryLoaded) {
|
|
846
860
|
loadClaudeHistory();
|
|
847
861
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
862
|
+
// 不再 loadOutput 当前会话——WS 重连后服务端会主动推一条 init
|
|
863
|
+
// 消息,那条路径已经走 ensureTerminalFitWithRetry 强制按真实
|
|
864
|
+
// cols 重排 history,足够覆盖前台恢复时的同步需求。这里多加
|
|
865
|
+
// 一次 fetch + syncTerminalBuffer 反而会在 ws/http 两路的 output
|
|
866
|
+
// 之间来回 reset,导致 alt-screen 中正在绘制的 Claude TUI 被
|
|
867
|
+
// 中途清掉。只把会话列表刷一下,保证状态条/会话名等元数据是新的。
|
|
868
|
+
return loadSessions({ skipSelectedOutputReload: true }).catch(function(e) {
|
|
854
869
|
console.error("[wand] foreground sync failed:", reason, e);
|
|
855
870
|
});
|
|
856
871
|
}
|
|
@@ -860,8 +875,15 @@
|
|
|
860
875
|
window.__wandForegroundSyncBound = true;
|
|
861
876
|
|
|
862
877
|
document.addEventListener("visibilitychange", function() {
|
|
863
|
-
if (
|
|
878
|
+
if (document.hidden) {
|
|
879
|
+
// Stop the reconnect backoff while hidden — the OS may freeze
|
|
880
|
+
// timers and then deliver them in a burst when we resume,
|
|
881
|
+
// creating a thundering-herd of connect attempts. The resume
|
|
882
|
+
// event will trigger one decisive reconnect instead.
|
|
883
|
+
cancelWsReconnect();
|
|
884
|
+
} else {
|
|
864
885
|
scheduleForegroundSync("visibility");
|
|
886
|
+
ensureTerminalFitWithRetry("visibility");
|
|
865
887
|
}
|
|
866
888
|
});
|
|
867
889
|
|
|
@@ -876,6 +898,18 @@
|
|
|
876
898
|
window.addEventListener("resume", function() {
|
|
877
899
|
scheduleForegroundSync("resume");
|
|
878
900
|
});
|
|
901
|
+
|
|
902
|
+
// Bridge from Android WebView host: MainActivity.onResume() calls
|
|
903
|
+
// evaluateJavascript to dispatch this event, which is the only
|
|
904
|
+
// reliable foreground signal once Doze/process-suspension has
|
|
905
|
+
// frozen page-level events (visibilitychange/focus/pageshow may
|
|
906
|
+
// fire late or not at all after a long suspend). Force-reconnect
|
|
907
|
+
// and force-refit immediately rather than waiting for the
|
|
908
|
+
// throttled scheduleForegroundSync path.
|
|
909
|
+
window.addEventListener("wand-android-resume", function() {
|
|
910
|
+
scheduleForegroundSync("android-resume", { immediate: true });
|
|
911
|
+
ensureTerminalFitWithRetry("android-resume");
|
|
912
|
+
});
|
|
879
913
|
}
|
|
880
914
|
|
|
881
915
|
function restoreLoginSession() {
|
|
@@ -1169,12 +1203,38 @@
|
|
|
1169
1203
|
'</aside>' +
|
|
1170
1204
|
'<main class="main-content">' +
|
|
1171
1205
|
'<div class="main-header-row">' +
|
|
1172
|
-
'<
|
|
1173
|
-
'<
|
|
1174
|
-
'<span
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1206
|
+
'<div class="topbar-left">' +
|
|
1207
|
+
'<button id="sessions-toggle-button" class="floating-sidebar-toggle' + (state.sessionsDrawerOpen ? ' active' : '') + '" aria-label="切换会话侧栏" type="button">' +
|
|
1208
|
+
'<span class="hamburger-icon">' +
|
|
1209
|
+
'<span></span><span></span><span></span>' +
|
|
1210
|
+
'</span>' +
|
|
1211
|
+
'</button>' +
|
|
1212
|
+
'<span class="topbar-brand" aria-hidden="true">W</span>' +
|
|
1213
|
+
'</div>' +
|
|
1214
|
+
'<div class="topbar-center">' +
|
|
1215
|
+
(selectedSession
|
|
1216
|
+
? (
|
|
1217
|
+
'<span class="topbar-session-title" title="' + escapeHtml(selectedSession.command || "") + '">' + escapeHtml(shortCommand(selectedSession.command)) + '</span>' +
|
|
1218
|
+
'<span class="session-status-pill ' + getSessionStatusClass(selectedSession) + '" title="' + escapeHtml(getSessionStatusLabel(selectedSession)) + '"><span class="session-status-dot"></span><span class="session-status-text">' + escapeHtml(getSessionStatusLabel(selectedSession)) + '</span></span>' +
|
|
1219
|
+
'<span class="current-task hidden" id="current-task"></span>' +
|
|
1220
|
+
(selectedSession.cwd ? '<span class="topbar-cwd" id="topbar-cwd" title="' + escapeHtml(selectedSession.cwd) + '" role="button" tabindex="0">' + escapeHtml(selectedSession.cwd) + '</span>' : '')
|
|
1221
|
+
)
|
|
1222
|
+
: '<span class="topbar-tagline">Wand 控制台</span>' +
|
|
1223
|
+
'<span class="current-task hidden" id="current-task"></span>'
|
|
1224
|
+
) +
|
|
1225
|
+
'</div>' +
|
|
1226
|
+
'<div class="topbar-right">' +
|
|
1227
|
+
(selectedSession && selectedSession.cwd ? '<button id="topbar-file-button" class="topbar-btn square' + (state.filePanelOpen ? ' active' : '') + '" type="button" aria-label="文件" title="文件"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></button>' : '') +
|
|
1228
|
+
'<div class="topbar-more-wrap">' +
|
|
1229
|
+
'<button id="topbar-more-button" class="topbar-btn square' + (state.topbarMoreOpen ? ' active' : '') + '" type="button" aria-label="更多" aria-haspopup="menu" aria-expanded="' + (state.topbarMoreOpen ? 'true' : 'false') + '" title="更多"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg></button>' +
|
|
1230
|
+
'<div id="topbar-more-menu" class="topbar-more-menu' + (state.topbarMoreOpen ? '' : ' hidden') + '" role="menu">' +
|
|
1231
|
+
'<button class="topbar-more-item" data-action="settings" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg><span>设置</span></button>' +
|
|
1232
|
+
'<button class="topbar-more-item" data-action="refresh" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg><span>刷新</span></button>' +
|
|
1233
|
+
'<button class="topbar-more-item' + (state.showInstallPrompt && state.deferredPrompt ? '' : ' hidden') + '" id="topbar-install-item" data-action="install" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg><span>安装应用</span></button>' +
|
|
1234
|
+
'<button class="topbar-more-item topbar-more-item-danger" data-action="logout" type="button" role="menuitem"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg><span>退出</span></button>' +
|
|
1235
|
+
'</div>' +
|
|
1236
|
+
'</div>' +
|
|
1237
|
+
'</div>' +
|
|
1178
1238
|
'</div>' +
|
|
1179
1239
|
// File panel backdrop (mobile)
|
|
1180
1240
|
'<div id="file-panel-backdrop" class="file-panel-backdrop' + (state.filePanelOpen ? " open" : "") + '"></div>' +
|
|
@@ -1208,7 +1268,7 @@
|
|
|
1208
1268
|
'</div>' +
|
|
1209
1269
|
'<div id="chat-output" class="chat-container hidden">' +
|
|
1210
1270
|
'<div class="chat-overlay-controls">' +
|
|
1211
|
-
'<button id="chat-follow-toggle" class="chat-follow-toggle topbar-btn' + (state.chatAutoFollow ? ' active' : '') + '" type="button" aria-pressed="' + (state.chatAutoFollow ? 'true' : 'false') + '"
|
|
1271
|
+
'<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>' +
|
|
1212
1272
|
'</div>' +
|
|
1213
1273
|
'<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>' +
|
|
1214
1274
|
'</div>' +
|
|
@@ -1412,58 +1472,81 @@
|
|
|
1412
1472
|
'<div class="settings-about-row"><span class="settings-label">Node.js 要求</span><span class="settings-value" id="settings-node-req">-</span></div>' +
|
|
1413
1473
|
'<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>' +
|
|
1414
1474
|
'</div>' +
|
|
1415
|
-
'<div class="settings-update-section">' +
|
|
1475
|
+
'<div class="settings-update-section" id="web-update-section">' +
|
|
1476
|
+
'<div class="settings-section-head">' +
|
|
1477
|
+
'<span class="settings-section-icon">🌐</span>' +
|
|
1478
|
+
'<div class="settings-section-head-text">' +
|
|
1479
|
+
'<h4 class="settings-section-heading">Web 端</h4>' +
|
|
1480
|
+
'<p class="settings-section-sub">浏览器访问的服务版本</p>' +
|
|
1481
|
+
'</div>' +
|
|
1482
|
+
'</div>' +
|
|
1416
1483
|
'<div class="settings-about-row">' +
|
|
1417
1484
|
'<span class="settings-label">最新版本</span>' +
|
|
1418
1485
|
'<span class="settings-value" id="settings-latest-version">-</span>' +
|
|
1419
1486
|
'</div>' +
|
|
1420
1487
|
'<div class="settings-update-actions">' +
|
|
1421
|
-
'<button id="check-update-button" class="btn btn-
|
|
1488
|
+
'<button id="check-update-button" class="btn btn-secondary btn-sm">\u68c0\u67e5\u66f4\u65b0</button>' +
|
|
1422
1489
|
'<button id="do-update-button" class="btn btn-primary btn-sm hidden">\u66f4\u65b0\u5230\u6700\u65b0\u7248</button>' +
|
|
1423
1490
|
'<button id="do-restart-button" class="btn btn-success btn-sm hidden">\u91cd\u542f\u751f\u6548</button>' +
|
|
1424
1491
|
'</div>' +
|
|
1425
1492
|
'<p id="update-message" class="hint hidden"></p>' +
|
|
1426
|
-
'<div class="settings-
|
|
1427
|
-
'<
|
|
1428
|
-
|
|
1493
|
+
'<div class="settings-toggle-row">' +
|
|
1494
|
+
'<div class="settings-toggle-text">' +
|
|
1495
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
1496
|
+
'<span class="settings-toggle-desc">检测到新版本将自动下载安装并重启服务。</span>' +
|
|
1497
|
+
'</div>' +
|
|
1498
|
+
'<label class="settings-switch">' +
|
|
1429
1499
|
'<input type="checkbox" id="auto-update-web-toggle" class="switch-toggle">' +
|
|
1430
1500
|
'<span class="switch-slider"></span>' +
|
|
1431
1501
|
'</label>' +
|
|
1432
1502
|
'</div>' +
|
|
1433
|
-
'<p class="hint" style="margin-top:2px">开启后,检测到新版本将自动下载安装并重启服务。</p>' +
|
|
1434
1503
|
'</div>' +
|
|
1435
|
-
'<div class="settings-update-section" id="android-apk-section">' +
|
|
1504
|
+
'<div class="settings-update-section hidden" id="android-apk-section">' +
|
|
1505
|
+
'<div class="settings-section-head">' +
|
|
1506
|
+
'<span class="settings-section-icon">📱</span>' +
|
|
1507
|
+
'<div class="settings-section-head-text">' +
|
|
1508
|
+
'<h4 class="settings-section-heading">Android App</h4>' +
|
|
1509
|
+
'<p class="settings-section-sub">原生客户端版本与 APK 下载</p>' +
|
|
1510
|
+
'</div>' +
|
|
1511
|
+
'</div>' +
|
|
1436
1512
|
'<div id="android-apk-current-row" class="settings-about-row hidden">' +
|
|
1437
1513
|
'<span class="settings-label">当前版本</span>' +
|
|
1438
1514
|
'<span class="settings-value" id="settings-android-apk-current">-</span>' +
|
|
1439
1515
|
'</div>' +
|
|
1440
|
-
'<div id="android-apk-github-row" class="settings-about-row hidden">' +
|
|
1516
|
+
'<div id="android-apk-github-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
1441
1517
|
'<span class="settings-label">线上版本</span>' +
|
|
1442
|
-
'<span class="settings-value" id="settings-android-apk-github"
|
|
1443
|
-
'<button id="download-github-apk-btn" class="btn btn-
|
|
1518
|
+
'<span class="settings-value settings-value-flex" id="settings-android-apk-github">-</span>' +
|
|
1519
|
+
'<button id="download-github-apk-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
1444
1520
|
'</div>' +
|
|
1445
|
-
'<div id="android-apk-local-row" class="settings-about-row hidden">' +
|
|
1521
|
+
'<div id="android-apk-local-row" class="settings-about-row settings-about-row-action hidden">' +
|
|
1446
1522
|
'<span class="settings-label">本地版本</span>' +
|
|
1447
|
-
'<span class="settings-value" id="settings-android-apk-local"
|
|
1448
|
-
'<button id="download-local-apk-btn" class="btn btn-
|
|
1523
|
+
'<span class="settings-value settings-value-flex" id="settings-android-apk-local">-</span>' +
|
|
1524
|
+
'<button id="download-local-apk-btn" class="btn btn-secondary btn-sm hidden" type="button">下载</button>' +
|
|
1449
1525
|
'</div>' +
|
|
1450
|
-
'<div id="android-auto-update-row" class="
|
|
1451
|
-
'<
|
|
1452
|
-
|
|
1526
|
+
'<div id="android-auto-update-row" class="settings-toggle-row hidden">' +
|
|
1527
|
+
'<div class="settings-toggle-text">' +
|
|
1528
|
+
'<span class="settings-toggle-title">自动更新</span>' +
|
|
1529
|
+
'<span class="settings-toggle-desc" id="android-auto-update-hint">检测到新版 APK 将自动下载安装。</span>' +
|
|
1530
|
+
'</div>' +
|
|
1531
|
+
'<label class="settings-switch">' +
|
|
1453
1532
|
'<input type="checkbox" id="auto-update-apk-toggle" class="switch-toggle">' +
|
|
1454
1533
|
'<span class="switch-slider"></span>' +
|
|
1455
1534
|
'</label>' +
|
|
1456
1535
|
'</div>' +
|
|
1457
|
-
'<p id="android-auto-update-hint" class="hint hidden" style="margin-top:2px">开启后,检测到新版 APK 将自动下载安装。</p>' +
|
|
1458
1536
|
'<p id="android-apk-message" class="hint hidden"></p>' +
|
|
1459
1537
|
'</div>' +
|
|
1460
1538
|
'<div class="settings-update-section" id="android-connect-section">' +
|
|
1461
|
-
'<div class="settings-section-
|
|
1539
|
+
'<div class="settings-section-head">' +
|
|
1540
|
+
'<span class="settings-section-icon">🔗</span>' +
|
|
1541
|
+
'<div class="settings-section-head-text">' +
|
|
1542
|
+
'<h4 class="settings-section-heading">App 连接码</h4>' +
|
|
1543
|
+
'<p class="settings-section-sub">粘贴到 Android App 即可自动连接,无需密码;改密码后失效。</p>' +
|
|
1544
|
+
'</div>' +
|
|
1545
|
+
'</div>' +
|
|
1462
1546
|
'<div class="settings-connect-url-box">' +
|
|
1463
|
-
'<code id="android-connect-code" class="settings-connect-url-text"
|
|
1464
|
-
'<button id="copy-connect-code-button" class="btn btn-
|
|
1547
|
+
'<code id="android-connect-code" class="settings-connect-url-text">-</code>' +
|
|
1548
|
+
'<button id="copy-connect-code-button" class="btn btn-secondary btn-sm" type="button" title="复制连接码">复制</button>' +
|
|
1465
1549
|
'</div>' +
|
|
1466
|
-
'<p class="hint">复制此连接码粘贴到 Android App 即可自动连接,无需输入密码。修改密码后连接码自动失效。</p>' +
|
|
1467
1550
|
'</div>' +
|
|
1468
1551
|
'</div>' +
|
|
1469
1552
|
|
|
@@ -1473,54 +1556,88 @@
|
|
|
1473
1556
|
'<h3 class="settings-panel-title">通知</h3>' +
|
|
1474
1557
|
'<p class="settings-panel-desc">设置提示音、系统通知和浏览器通知的行为。</p>' +
|
|
1475
1558
|
'</div>' +
|
|
1476
|
-
'<div class="settings-section
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
'
|
|
1484
|
-
'<div
|
|
1485
|
-
'<
|
|
1486
|
-
|
|
1559
|
+
'<div class="settings-notification-section">' +
|
|
1560
|
+
'<div class="settings-section-head">' +
|
|
1561
|
+
'<span class="settings-section-icon">🔔</span>' +
|
|
1562
|
+
'<div class="settings-section-head-text">' +
|
|
1563
|
+
'<h4 class="settings-section-heading">通知偏好</h4>' +
|
|
1564
|
+
'<p class="settings-section-sub">提示音与应用内通知气泡</p>' +
|
|
1565
|
+
'</div>' +
|
|
1566
|
+
'</div>' +
|
|
1567
|
+
'<div class="settings-toggle-row">' +
|
|
1568
|
+
'<div class="settings-toggle-text">' +
|
|
1569
|
+
'<label class="settings-toggle-title" for="cfg-notif-sound">播放提示音</label>' +
|
|
1570
|
+
'<span class="settings-toggle-desc">重要通知(版本更新、权限等待等)时播放柔和提示音。</span>' +
|
|
1571
|
+
'</div>' +
|
|
1572
|
+
'<label class="settings-switch">' +
|
|
1573
|
+
'<input id="cfg-notif-sound" type="checkbox" class="switch-toggle" />' +
|
|
1574
|
+
'<span class="switch-slider"></span>' +
|
|
1575
|
+
'</label>' +
|
|
1576
|
+
'</div>' +
|
|
1577
|
+
'<div class="settings-range-row" id="notif-volume-field">' +
|
|
1578
|
+
'<label class="settings-range-label" for="cfg-notif-volume">音量</label>' +
|
|
1579
|
+
'<input id="cfg-notif-volume" type="range" min="0" max="100" step="5" class="settings-range" />' +
|
|
1580
|
+
'<span id="cfg-notif-volume-val" class="settings-range-value">80%</span>' +
|
|
1581
|
+
'</div>' +
|
|
1582
|
+
'<div class="settings-toggle-row">' +
|
|
1583
|
+
'<div class="settings-toggle-text">' +
|
|
1584
|
+
'<label class="settings-toggle-title" for="cfg-notif-bubble">应用内通知气泡</label>' +
|
|
1585
|
+
'<span class="settings-toggle-desc">在页面顶部弹出浮动通知气泡。</span>' +
|
|
1586
|
+
'</div>' +
|
|
1587
|
+
'<label class="settings-switch">' +
|
|
1588
|
+
'<input id="cfg-notif-bubble" type="checkbox" class="switch-toggle" />' +
|
|
1589
|
+
'<span class="switch-slider"></span>' +
|
|
1590
|
+
'</label>' +
|
|
1487
1591
|
'</div>' +
|
|
1488
1592
|
'</div>' +
|
|
1489
|
-
'<div class="
|
|
1490
|
-
'<
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
'<div class="settings-section-title">\u7cfb\u7edf\u901a\u77e5\u94c3\u58f0</div>' +
|
|
1496
|
-
'<div class="settings-about-row">' +
|
|
1497
|
-
'<span class="settings-label">\u94c3\u58f0</span>' +
|
|
1498
|
-
'<div style="display:flex;align-items:center;gap:6px">' +
|
|
1499
|
-
'<select id="native-sound-select" class="field-select" style="min-width:100px"></select>' +
|
|
1500
|
-
'<button id="native-sound-preview" class="btn btn-ghost btn-sm">\u25b6 \u8bd5\u542c</button>' +
|
|
1593
|
+
'<div id="native-sound-section" class="settings-notification-section hidden">' +
|
|
1594
|
+
'<div class="settings-section-head">' +
|
|
1595
|
+
'<span class="settings-section-icon">🎵</span>' +
|
|
1596
|
+
'<div class="settings-section-head-text">' +
|
|
1597
|
+
'<h4 class="settings-section-heading">系统通知铃声</h4>' +
|
|
1598
|
+
'<p class="settings-section-sub">选择 Android 系统通知使用的铃声</p>' +
|
|
1501
1599
|
'</div>' +
|
|
1502
1600
|
'</div>' +
|
|
1503
|
-
'<
|
|
1601
|
+
'<div class="settings-row-with-action">' +
|
|
1602
|
+
'<select id="native-sound-select" class="field-input field-select"></select>' +
|
|
1603
|
+
'<button id="native-sound-preview" class="btn btn-secondary btn-sm" type="button">▶ 试听</button>' +
|
|
1604
|
+
'</div>' +
|
|
1504
1605
|
'</div>' +
|
|
1505
|
-
'<div id="native-haptic-section" class="settings-notification-section hidden"
|
|
1506
|
-
'<div class="settings-section-
|
|
1507
|
-
|
|
1508
|
-
'<
|
|
1509
|
-
|
|
1606
|
+
'<div id="native-haptic-section" class="settings-notification-section hidden">' +
|
|
1607
|
+
'<div class="settings-section-head">' +
|
|
1608
|
+
'<span class="settings-section-icon">📳</span>' +
|
|
1609
|
+
'<div class="settings-section-head-text">' +
|
|
1610
|
+
'<h4 class="settings-section-heading">触感反馈</h4>' +
|
|
1611
|
+
'<p class="settings-section-sub">按钮操作和任务完成时提供振动反馈</p>' +
|
|
1612
|
+
'</div>' +
|
|
1613
|
+
'</div>' +
|
|
1614
|
+
'<div class="settings-toggle-row">' +
|
|
1615
|
+
'<div class="settings-toggle-text">' +
|
|
1616
|
+
'<label class="settings-toggle-title" for="cfg-haptic-enabled">启用触感反馈</label>' +
|
|
1617
|
+
'</div>' +
|
|
1618
|
+
'<label class="settings-switch">' +
|
|
1619
|
+
'<input id="cfg-haptic-enabled" type="checkbox" class="switch-toggle" />' +
|
|
1620
|
+
'<span class="switch-slider"></span>' +
|
|
1621
|
+
'</label>' +
|
|
1510
1622
|
'</div>' +
|
|
1511
|
-
'<p class="hint" style="margin-top:0">\u6309\u94ae\u64cd\u4f5c\u548c\u4efb\u52a1\u5b8c\u6210\u65f6\u63d0\u4f9b\u632f\u52a8\u53cd\u9988</p>' +
|
|
1512
1623
|
'</div>' +
|
|
1513
|
-
'<div class="settings-notification-section"
|
|
1514
|
-
'<div class="settings-section-
|
|
1624
|
+
'<div class="settings-notification-section">' +
|
|
1625
|
+
'<div class="settings-section-head">' +
|
|
1626
|
+
'<span class="settings-section-icon">🌐</span>' +
|
|
1627
|
+
'<div class="settings-section-head-text">' +
|
|
1628
|
+
'<h4 class="settings-section-heading">浏览器通知</h4>' +
|
|
1629
|
+
'<p class="settings-section-sub">来自系统通知中心的弹窗</p>' +
|
|
1630
|
+
'</div>' +
|
|
1631
|
+
'</div>' +
|
|
1515
1632
|
'<div class="settings-about-row">' +
|
|
1516
|
-
'<span class="settings-label"
|
|
1633
|
+
'<span class="settings-label">授权状态</span>' +
|
|
1517
1634
|
'<span class="settings-value" id="notification-permission-status">-</span>' +
|
|
1518
1635
|
'</div>' +
|
|
1519
1636
|
'<div class="settings-update-actions">' +
|
|
1520
|
-
'<button id="notification-request-btn" class="btn btn-
|
|
1521
|
-
'<button id="notification-reset-btn" class="btn btn-
|
|
1522
|
-
'<button id="notification-test-btn" class="btn btn-
|
|
1523
|
-
'<button id="notification-test-delay-btn" class="btn btn-
|
|
1637
|
+
'<button id="notification-request-btn" class="btn btn-secondary btn-sm hidden" type="button">授权通知</button>' +
|
|
1638
|
+
'<button id="notification-reset-btn" class="btn btn-secondary btn-sm hidden" type="button">重新授权</button>' +
|
|
1639
|
+
'<button id="notification-test-btn" class="btn btn-secondary btn-sm" type="button">发送测试通知</button>' +
|
|
1640
|
+
'<button id="notification-test-delay-btn" class="btn btn-secondary btn-sm" type="button">10 秒后发送</button>' +
|
|
1524
1641
|
'</div>' +
|
|
1525
1642
|
'<p id="notification-test-message" class="hint hidden"></p>' +
|
|
1526
1643
|
'</div>' +
|
|
@@ -1578,13 +1695,13 @@
|
|
|
1578
1695
|
'<p class="field-hint" style="margin-top:-4px;">设置回复语言后,Claude 将尽量使用指定语言回复。</p>' +
|
|
1579
1696
|
'<div class="field">' +
|
|
1580
1697
|
'<label class="field-label" for="cfg-default-model">默认模型</label>' +
|
|
1581
|
-
'<div
|
|
1582
|
-
'<select id="cfg-default-model" class="field-input
|
|
1698
|
+
'<div class="settings-row-with-action">' +
|
|
1699
|
+
'<select id="cfg-default-model" class="field-input field-select">' +
|
|
1583
1700
|
'<option value="">跟随 Claude Code 默认</option>' +
|
|
1584
1701
|
'</select>' +
|
|
1585
|
-
'<button type="button" id="cfg-default-model-refresh" class="btn btn-
|
|
1702
|
+
'<button type="button" id="cfg-default-model-refresh" class="btn btn-secondary btn-sm" title="刷新模型列表">刷新</button>' +
|
|
1586
1703
|
'</div>' +
|
|
1587
|
-
'<p class="field-hint" id="cfg-default-model-version"
|
|
1704
|
+
'<p class="field-hint" id="cfg-default-model-version">新建会话时默认使用该模型;运行中的会话可在输入框切换。</p>' +
|
|
1588
1705
|
'</div>' +
|
|
1589
1706
|
'<div class="field">' +
|
|
1590
1707
|
'<label class="field-label" for="cfg-cwd">默认工作目录</label>' +
|
|
@@ -1595,24 +1712,29 @@
|
|
|
1595
1712
|
'<input id="cfg-shell" type="text" class="field-input" placeholder="/bin/bash" />' +
|
|
1596
1713
|
'</div>' +
|
|
1597
1714
|
(typeof WandNative !== "undefined" && typeof WandNative.getAppIcon === "function" ?
|
|
1598
|
-
'<div
|
|
1599
|
-
'<div class="settings-section-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
'<
|
|
1604
|
-
PIXEL_AVATAR.user +
|
|
1605
|
-
'</div>' +
|
|
1606
|
-
'<span style="font-size:0.72rem;color:var(--text-secondary)">赛博虎妞</span>' +
|
|
1715
|
+
'<div class="settings-app-icon-block">' +
|
|
1716
|
+
'<div class="settings-section-head">' +
|
|
1717
|
+
'<span class="settings-section-icon">🎨</span>' +
|
|
1718
|
+
'<div class="settings-section-head-text">' +
|
|
1719
|
+
'<h4 class="settings-section-heading">应用图标</h4>' +
|
|
1720
|
+
'<p class="settings-section-sub">选择 App 启动器图标,返回桌面后生效</p>' +
|
|
1607
1721
|
'</div>' +
|
|
1608
|
-
|
|
1609
|
-
|
|
1722
|
+
'</div>' +
|
|
1723
|
+
'<div id="app-icon-picker" class="settings-app-icon-picker">' +
|
|
1724
|
+
'<button type="button" class="settings-app-icon-option" data-icon="shorthair">' +
|
|
1725
|
+
'<span class="settings-app-icon-preview">' +
|
|
1726
|
+
PIXEL_AVATAR.user +
|
|
1727
|
+
'</span>' +
|
|
1728
|
+
'<span class="settings-app-icon-label">赛博虎妞</span>' +
|
|
1729
|
+
'</button>' +
|
|
1730
|
+
'<button type="button" class="settings-app-icon-option" data-icon="garfield">' +
|
|
1731
|
+
'<span class="settings-app-icon-preview">' +
|
|
1610
1732
|
PIXEL_AVATAR.assistant +
|
|
1611
|
-
'</
|
|
1612
|
-
'<span
|
|
1613
|
-
'</
|
|
1733
|
+
'</span>' +
|
|
1734
|
+
'<span class="settings-app-icon-label">勤劳初二</span>' +
|
|
1735
|
+
'</button>' +
|
|
1614
1736
|
'</div>' +
|
|
1615
|
-
'<p id="app-icon-message" class="hint hidden"
|
|
1737
|
+
'<p id="app-icon-message" class="hint hidden"></p>' +
|
|
1616
1738
|
'</div>'
|
|
1617
1739
|
: '') +
|
|
1618
1740
|
'<div class="settings-actions settings-actions-sticky">' +
|
|
@@ -1628,7 +1750,13 @@
|
|
|
1628
1750
|
'<p class="settings-panel-desc">管理登录密码与 SSL 证书,敏感变更请确认后再保存。</p>' +
|
|
1629
1751
|
'</div>' +
|
|
1630
1752
|
'<div class="settings-card">' +
|
|
1631
|
-
'<
|
|
1753
|
+
'<div class="settings-card-head">' +
|
|
1754
|
+
'<span class="settings-card-icon">\ud83d\udd12</span>' +
|
|
1755
|
+
'<div class="settings-card-head-text">' +
|
|
1756
|
+
'<h3 class="settings-card-title">修改密码</h3>' +
|
|
1757
|
+
'<p class="settings-card-desc">至少 6 个字符;保存后下次登录生效。</p>' +
|
|
1758
|
+
'</div>' +
|
|
1759
|
+
'</div>' +
|
|
1632
1760
|
'<div class="field">' +
|
|
1633
1761
|
'<label class="field-label" for="new-password">新密码</label>' +
|
|
1634
1762
|
'<input id="new-password" type="password" class="field-input" placeholder="输入新密码(至少 6 个字符)" autocomplete="new-password" />' +
|
|
@@ -1637,22 +1765,45 @@
|
|
|
1637
1765
|
'<label class="field-label" for="confirm-password">确认密码</label>' +
|
|
1638
1766
|
'<input id="confirm-password" type="password" class="field-input" placeholder="再次输入新密码" autocomplete="new-password" />' +
|
|
1639
1767
|
'</div>' +
|
|
1640
|
-
'<
|
|
1768
|
+
'<div class="settings-card-actions">' +
|
|
1769
|
+
'<button id="save-password-button" class="btn btn-primary">保存密码</button>' +
|
|
1770
|
+
'</div>' +
|
|
1641
1771
|
'<p id="settings-error" class="error-message hidden"></p>' +
|
|
1642
|
-
'<p id="settings-success" class="hint hidden"
|
|
1772
|
+
'<p id="settings-success" class="hint settings-success-message hidden"></p>' +
|
|
1643
1773
|
'</div>' +
|
|
1644
1774
|
'<div class="settings-card">' +
|
|
1645
|
-
'<
|
|
1646
|
-
|
|
1775
|
+
'<div class="settings-card-head">' +
|
|
1776
|
+
'<span class="settings-card-icon">\ud83d\udd10</span>' +
|
|
1777
|
+
'<div class="settings-card-head-text">' +
|
|
1778
|
+
'<h3 class="settings-card-title">SSL 证书</h3>' +
|
|
1779
|
+
'<p class="settings-card-desc" id="cert-status">加载中...</p>' +
|
|
1780
|
+
'</div>' +
|
|
1781
|
+
'</div>' +
|
|
1647
1782
|
'<div class="field">' +
|
|
1648
1783
|
'<label class="field-label" for="cert-key-file">私钥文件 (.key)</label>' +
|
|
1649
|
-
'<
|
|
1784
|
+
'<div class="file-picker">' +
|
|
1785
|
+
'<input id="cert-key-file" type="file" class="file-picker-input" accept=".key,.pem" />' +
|
|
1786
|
+
'<label for="cert-key-file" class="file-picker-trigger">' +
|
|
1787
|
+
'<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>' +
|
|
1788
|
+
'<span class="file-picker-label">选择私钥</span>' +
|
|
1789
|
+
'</label>' +
|
|
1790
|
+
'<span class="file-picker-name" data-default="未选择文件">未选择文件</span>' +
|
|
1791
|
+
'</div>' +
|
|
1650
1792
|
'</div>' +
|
|
1651
1793
|
'<div class="field">' +
|
|
1652
1794
|
'<label class="field-label" for="cert-cert-file">证书文件 (.crt/.pem)</label>' +
|
|
1653
|
-
'<
|
|
1795
|
+
'<div class="file-picker">' +
|
|
1796
|
+
'<input id="cert-cert-file" type="file" class="file-picker-input" accept=".crt,.pem,.cert" />' +
|
|
1797
|
+
'<label for="cert-cert-file" class="file-picker-trigger">' +
|
|
1798
|
+
'<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>' +
|
|
1799
|
+
'<span class="file-picker-label">选择证书</span>' +
|
|
1800
|
+
'</label>' +
|
|
1801
|
+
'<span class="file-picker-name" data-default="未选择文件">未选择文件</span>' +
|
|
1802
|
+
'</div>' +
|
|
1803
|
+
'</div>' +
|
|
1804
|
+
'<div class="settings-card-actions">' +
|
|
1805
|
+
'<button id="upload-cert-button" class="btn btn-primary">上传证书</button>' +
|
|
1654
1806
|
'</div>' +
|
|
1655
|
-
'<button id="upload-cert-button" class="btn btn-primary btn-block">上传证书</button>' +
|
|
1656
1807
|
'<p id="cert-message" class="hint hidden"></p>' +
|
|
1657
1808
|
'</div>' +
|
|
1658
1809
|
'</div>' +
|
|
@@ -1727,7 +1878,7 @@
|
|
|
1727
1878
|
}
|
|
1728
1879
|
|
|
1729
1880
|
function renderSessions() {
|
|
1730
|
-
var activeSessions = state.sessions.filter(function(session) { return !session.archived
|
|
1881
|
+
var activeSessions = state.sessions.filter(function(session) { return !session.archived; });
|
|
1731
1882
|
var archivedSessions = state.sessions.filter(function(session) { return session.archived; });
|
|
1732
1883
|
var groups = [];
|
|
1733
1884
|
groups.push(renderSessionManageBar());
|
|
@@ -1900,9 +2051,7 @@
|
|
|
1900
2051
|
}
|
|
1901
2052
|
|
|
1902
2053
|
function getSelectableSessions() {
|
|
1903
|
-
return state.sessions.
|
|
1904
|
-
return session.archived || !session.resumedToSessionId;
|
|
1905
|
-
});
|
|
2054
|
+
return state.sessions.slice();
|
|
1906
2055
|
}
|
|
1907
2056
|
|
|
1908
2057
|
function countSelectableItems() {
|
|
@@ -2649,10 +2798,8 @@
|
|
|
2649
2798
|
var statusMap = {
|
|
2650
2799
|
"stopped": "已停止",
|
|
2651
2800
|
"running": "运行中",
|
|
2652
|
-
"
|
|
2653
|
-
"
|
|
2654
|
-
"waiting-input": "等待输入",
|
|
2655
|
-
"initializing": "启动中"
|
|
2801
|
+
"exited": "已退出",
|
|
2802
|
+
"failed": "已失败"
|
|
2656
2803
|
};
|
|
2657
2804
|
return statusMap[session.status] || session.status;
|
|
2658
2805
|
}
|
|
@@ -3363,7 +3510,7 @@
|
|
|
3363
3510
|
// App icon picker (APK only)
|
|
3364
3511
|
var appIconPicker = document.getElementById("app-icon-picker");
|
|
3365
3512
|
if (appIconPicker) {
|
|
3366
|
-
var appIconOpts = appIconPicker.querySelectorAll(".app-icon-option");
|
|
3513
|
+
var appIconOpts = appIconPicker.querySelectorAll(".settings-app-icon-option");
|
|
3367
3514
|
for (var ai = 0; ai < appIconOpts.length; ai++) {
|
|
3368
3515
|
appIconOpts[ai].addEventListener("click", function() {
|
|
3369
3516
|
var iconName = this.getAttribute("data-icon");
|
|
@@ -3384,6 +3531,24 @@
|
|
|
3384
3531
|
}
|
|
3385
3532
|
var uploadCertBtn = document.getElementById("upload-cert-button");
|
|
3386
3533
|
if (uploadCertBtn) uploadCertBtn.addEventListener("click", uploadCertificates);
|
|
3534
|
+
var filePickerInputs = document.querySelectorAll(".file-picker-input");
|
|
3535
|
+
for (var fpi = 0; fpi < filePickerInputs.length; fpi++) {
|
|
3536
|
+
(function(input) {
|
|
3537
|
+
input.addEventListener("change", function() {
|
|
3538
|
+
var picker = input.closest(".file-picker");
|
|
3539
|
+
if (!picker) return;
|
|
3540
|
+
var nameEl = picker.querySelector(".file-picker-name");
|
|
3541
|
+
if (!nameEl) return;
|
|
3542
|
+
if (input.files && input.files[0]) {
|
|
3543
|
+
nameEl.textContent = input.files[0].name;
|
|
3544
|
+
picker.classList.add("file-picker-has-file");
|
|
3545
|
+
} else {
|
|
3546
|
+
nameEl.textContent = nameEl.getAttribute("data-default") || "未选择文件";
|
|
3547
|
+
picker.classList.remove("file-picker-has-file");
|
|
3548
|
+
}
|
|
3549
|
+
});
|
|
3550
|
+
})(filePickerInputs[fpi]);
|
|
3551
|
+
}
|
|
3387
3552
|
var checkUpdateBtn = document.getElementById("check-update-button");
|
|
3388
3553
|
if (checkUpdateBtn) checkUpdateBtn.addEventListener("click", checkForUpdate);
|
|
3389
3554
|
var doUpdateBtn = document.getElementById("do-update-button");
|
|
@@ -3678,6 +3843,88 @@
|
|
|
3678
3843
|
var filePanelBackdrop = document.getElementById("file-panel-backdrop");
|
|
3679
3844
|
if (filePanelBackdrop) filePanelBackdrop.addEventListener("click", closeFilePanel);
|
|
3680
3845
|
|
|
3846
|
+
// Topbar: file button (mirrors toggleFilePanel)
|
|
3847
|
+
var topbarFileBtn = document.getElementById("topbar-file-button");
|
|
3848
|
+
if (topbarFileBtn) topbarFileBtn.addEventListener("click", toggleFilePanel);
|
|
3849
|
+
|
|
3850
|
+
// Topbar: cwd click → open file panel
|
|
3851
|
+
var topbarCwdEl = document.getElementById("topbar-cwd");
|
|
3852
|
+
if (topbarCwdEl) {
|
|
3853
|
+
topbarCwdEl.addEventListener("click", function() {
|
|
3854
|
+
if (!state.filePanelOpen) toggleFilePanel();
|
|
3855
|
+
});
|
|
3856
|
+
topbarCwdEl.addEventListener("keydown", function(e) {
|
|
3857
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3858
|
+
e.preventDefault();
|
|
3859
|
+
if (!state.filePanelOpen) toggleFilePanel();
|
|
3860
|
+
}
|
|
3861
|
+
});
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
// Topbar: more menu
|
|
3865
|
+
var topbarMoreBtn = document.getElementById("topbar-more-button");
|
|
3866
|
+
var topbarMoreMenu = document.getElementById("topbar-more-menu");
|
|
3867
|
+
if (topbarMoreBtn && topbarMoreMenu) {
|
|
3868
|
+
topbarMoreBtn.addEventListener("click", function(e) {
|
|
3869
|
+
e.stopPropagation();
|
|
3870
|
+
state.topbarMoreOpen = !state.topbarMoreOpen;
|
|
3871
|
+
topbarMoreMenu.classList.toggle("hidden", !state.topbarMoreOpen);
|
|
3872
|
+
topbarMoreBtn.classList.toggle("active", state.topbarMoreOpen);
|
|
3873
|
+
topbarMoreBtn.setAttribute("aria-expanded", state.topbarMoreOpen ? "true" : "false");
|
|
3874
|
+
});
|
|
3875
|
+
topbarMoreMenu.addEventListener("click", function(e) {
|
|
3876
|
+
var btn = e.target && e.target.closest ? e.target.closest(".topbar-more-item") : null;
|
|
3877
|
+
if (!btn) return;
|
|
3878
|
+
var action = btn.getAttribute("data-action");
|
|
3879
|
+
// Close menu first regardless of action
|
|
3880
|
+
state.topbarMoreOpen = false;
|
|
3881
|
+
topbarMoreMenu.classList.add("hidden");
|
|
3882
|
+
topbarMoreBtn.classList.remove("active");
|
|
3883
|
+
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
3884
|
+
switch (action) {
|
|
3885
|
+
case "settings":
|
|
3886
|
+
openSettingsModal();
|
|
3887
|
+
break;
|
|
3888
|
+
case "refresh":
|
|
3889
|
+
window.location.reload();
|
|
3890
|
+
break;
|
|
3891
|
+
case "install":
|
|
3892
|
+
if (state.deferredPrompt) {
|
|
3893
|
+
state.deferredPrompt.prompt();
|
|
3894
|
+
state.deferredPrompt.userChoice.then(function() {
|
|
3895
|
+
state.deferredPrompt = null;
|
|
3896
|
+
state.showInstallPrompt = false;
|
|
3897
|
+
updateInstallPrompt();
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
break;
|
|
3901
|
+
case "logout":
|
|
3902
|
+
logout();
|
|
3903
|
+
break;
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
// Close on outside click
|
|
3907
|
+
document.addEventListener("click", function(e) {
|
|
3908
|
+
if (!state.topbarMoreOpen) return;
|
|
3909
|
+
var wrap = topbarMoreMenu.parentElement;
|
|
3910
|
+
if (wrap && !wrap.contains(e.target)) {
|
|
3911
|
+
state.topbarMoreOpen = false;
|
|
3912
|
+
topbarMoreMenu.classList.add("hidden");
|
|
3913
|
+
topbarMoreBtn.classList.remove("active");
|
|
3914
|
+
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
3915
|
+
}
|
|
3916
|
+
});
|
|
3917
|
+
// Close on ESC
|
|
3918
|
+
document.addEventListener("keydown", function(e) {
|
|
3919
|
+
if (e.key === "Escape" && state.topbarMoreOpen) {
|
|
3920
|
+
state.topbarMoreOpen = false;
|
|
3921
|
+
topbarMoreMenu.classList.add("hidden");
|
|
3922
|
+
topbarMoreBtn.classList.remove("active");
|
|
3923
|
+
topbarMoreBtn.setAttribute("aria-expanded", "false");
|
|
3924
|
+
}
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3681
3928
|
// Terminal scale controls (topbar)
|
|
3682
3929
|
var scaleDownBtn = document.getElementById("terminal-scale-down-top");
|
|
3683
3930
|
var scaleUpBtn = document.getElementById("terminal-scale-up-top");
|
|
@@ -4547,9 +4794,148 @@
|
|
|
4547
4794
|
requestSyncScrollbar();
|
|
4548
4795
|
}
|
|
4549
4796
|
|
|
4797
|
+
// ──────── East-Asian-Wide padding for wterm WASM ────────
|
|
4798
|
+
//
|
|
4799
|
+
// wterm's WASM grid (as of @wterm/core 0.1.8/0.1.9) treats every
|
|
4800
|
+
// codepoint as occupying exactly 1 cell, while node-pty's backend
|
|
4801
|
+
// and Claude Code's TUI emit cursor-positioning sequences that
|
|
4802
|
+
// assume CJK / fullwidth / emoji codepoints occupy 2 columns
|
|
4803
|
+
// (Unicode TR11 East-Asian-Width = W or F). The mismatch makes
|
|
4804
|
+
// every CSI cursor move after CJK output drift by N/2 columns,
|
|
4805
|
+
// causing in-place rewrites (thinking spinner, todo list,
|
|
4806
|
+
// permission menus) to leave torn residue like "替替换换".
|
|
4807
|
+
//
|
|
4808
|
+
// Fix: insert U+2060 (Word Joiner — zero-width, unbreakable) after
|
|
4809
|
+
// each wide codepoint before handing the byte stream to the WASM
|
|
4810
|
+
// grid. The WJ takes one cell, so wide chars now occupy 2 cells —
|
|
4811
|
+
// matching the backend's column accounting exactly. The browser
|
|
4812
|
+
// renders WJ at zero width, so the visual layout stays correct.
|
|
4813
|
+
//
|
|
4814
|
+
// The scanner is ANSI-aware: it tracks ESC / CSI / OSC / DCS
|
|
4815
|
+
// / PM / APC state across chunk boundaries so wide codepoints
|
|
4816
|
+
// inside escape sequences (e.g. OSC window-title payloads) are
|
|
4817
|
+
// not padded — that would break sequence parsing.
|
|
4818
|
+
function isEastAsianWide(cp) {
|
|
4819
|
+
if (cp < 0x1100) return false;
|
|
4820
|
+
return (
|
|
4821
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
4822
|
+
(cp >= 0x2329 && cp <= 0x232a) ||
|
|
4823
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
4824
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
4825
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
4826
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
4827
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
4828
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
4829
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
4830
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
4831
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
4832
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
4833
|
+
(cp >= 0x1f000 && cp <= 0x1f9ff) ||
|
|
4834
|
+
(cp >= 0x20000 && cp <= 0x2fffd) ||
|
|
4835
|
+
(cp >= 0x30000 && cp <= 0x3fffd)
|
|
4836
|
+
);
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
var WAND_WIDE_FILLER = "\u2060";
|
|
4840
|
+
|
|
4841
|
+
function createWideParserState() { return { mode: "normal" }; }
|
|
4842
|
+
|
|
4843
|
+
function widePadAnsi(data, st) {
|
|
4844
|
+
if (!data) return "";
|
|
4845
|
+
var s = String(data);
|
|
4846
|
+
var out = "";
|
|
4847
|
+
for (var i = 0; i < s.length; i++) {
|
|
4848
|
+
var code = s.charCodeAt(i);
|
|
4849
|
+
var cp = code;
|
|
4850
|
+
var consumed = 1;
|
|
4851
|
+
if (code >= 0xd800 && code <= 0xdbff && i + 1 < s.length) {
|
|
4852
|
+
var lo = s.charCodeAt(i + 1);
|
|
4853
|
+
if (lo >= 0xdc00 && lo <= 0xdfff) {
|
|
4854
|
+
cp = (code - 0xd800) * 0x400 + (lo - 0xdc00) + 0x10000;
|
|
4855
|
+
consumed = 2;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
var ch = consumed === 2 ? s.substr(i, 2) : s.charAt(i);
|
|
4859
|
+
switch (st.mode) {
|
|
4860
|
+
case "normal":
|
|
4861
|
+
if (cp === 0x1b) { st.mode = "esc"; out += ch; }
|
|
4862
|
+
else if (cp === 0x9b) { st.mode = "csi"; out += ch; }
|
|
4863
|
+
else if (cp === 0x9d || cp === 0x90 || cp === 0x9e || cp === 0x9f) {
|
|
4864
|
+
st.mode = "string"; out += ch;
|
|
4865
|
+
} else {
|
|
4866
|
+
out += ch;
|
|
4867
|
+
if (isEastAsianWide(cp)) out += WAND_WIDE_FILLER;
|
|
4868
|
+
}
|
|
4869
|
+
break;
|
|
4870
|
+
case "esc":
|
|
4871
|
+
out += ch;
|
|
4872
|
+
if (cp === 0x5b) st.mode = "csi";
|
|
4873
|
+
else if (cp === 0x5d || cp === 0x50 || cp === 0x58 ||
|
|
4874
|
+
cp === 0x5e || cp === 0x5f) st.mode = "string";
|
|
4875
|
+
else st.mode = "normal";
|
|
4876
|
+
break;
|
|
4877
|
+
case "csi":
|
|
4878
|
+
out += ch;
|
|
4879
|
+
if (cp >= 0x40 && cp <= 0x7e) st.mode = "normal";
|
|
4880
|
+
break;
|
|
4881
|
+
case "string":
|
|
4882
|
+
out += ch;
|
|
4883
|
+
if (cp === 0x07 || cp === 0x9c) st.mode = "normal";
|
|
4884
|
+
else if (cp === 0x1b) st.mode = "string-esc";
|
|
4885
|
+
break;
|
|
4886
|
+
case "string-esc":
|
|
4887
|
+
out += ch;
|
|
4888
|
+
if (cp === 0x5c) st.mode = "normal";
|
|
4889
|
+
else st.mode = "string";
|
|
4890
|
+
break;
|
|
4891
|
+
}
|
|
4892
|
+
i += consumed - 1;
|
|
4893
|
+
}
|
|
4894
|
+
return out;
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
function wandTerminalWrite(terminal, data) {
|
|
4898
|
+
if (!terminal || data == null) return;
|
|
4899
|
+
if (!state.wideParserState) state.wideParserState = createWideParserState();
|
|
4900
|
+
terminal.write(widePadAnsi(data, state.wideParserState));
|
|
4901
|
+
}
|
|
4902
|
+
|
|
4903
|
+
function resetWideParserState() {
|
|
4904
|
+
state.wideParserState = createWideParserState();
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
// Strip the wide-pad filler from copied text so users pasting
|
|
4908
|
+
// selected terminal output don't get hidden U+2060 sprinkled
|
|
4909
|
+
// through every CJK string.
|
|
4910
|
+
function stripWideFillerForCopy() {
|
|
4911
|
+
if (typeof document === "undefined") return;
|
|
4912
|
+
document.addEventListener("copy", function(e) {
|
|
4913
|
+
var sel = window.getSelection && window.getSelection();
|
|
4914
|
+
if (!sel || sel.isCollapsed) return;
|
|
4915
|
+
var anchor = sel.anchorNode;
|
|
4916
|
+
var node = anchor && anchor.nodeType === 3 ? anchor.parentNode : anchor;
|
|
4917
|
+
var output = document.getElementById("output");
|
|
4918
|
+
if (!output || !node || !output.contains(node)) return;
|
|
4919
|
+
var text = sel.toString();
|
|
4920
|
+
if (text.indexOf(WAND_WIDE_FILLER) === -1) return;
|
|
4921
|
+
if (e.clipboardData) {
|
|
4922
|
+
e.clipboardData.setData("text/plain", text.split(WAND_WIDE_FILLER).join(""));
|
|
4923
|
+
e.preventDefault();
|
|
4924
|
+
}
|
|
4925
|
+
});
|
|
4926
|
+
}
|
|
4927
|
+
stripWideFillerForCopy();
|
|
4928
|
+
|
|
4550
4929
|
function resetTerminal() {
|
|
4551
|
-
if (!state.terminal || typeof state.terminal.
|
|
4552
|
-
|
|
4930
|
+
if (!state.terminal || typeof state.terminal.write !== "function") return;
|
|
4931
|
+
// @wterm/dom 的 WTerm 类没有暴露 reset() 方法(grep 全包零匹配),
|
|
4932
|
+
// 所以早期的 state.terminal.reset() 调用是 no-op——softResyncTerminal
|
|
4933
|
+
// 实际只做了"再写一份 terminalOutput 追加",旧 grid 不会被清空,
|
|
4934
|
+
// 这就是"点刷新按钮没用、只有窗口尺寸变化才修"的根因。
|
|
4935
|
+
// 改用 ANSI RIS (Reset to Initial State, ESC c) 让 WASM 状态机自己
|
|
4936
|
+
// 重置 grid / 光标 / 属性 / scrollback,所有 VT 实现都支持这个序列。
|
|
4937
|
+
state.terminal.write("\x1bc");
|
|
4938
|
+
resetWideParserState();
|
|
4553
4939
|
}
|
|
4554
4940
|
|
|
4555
4941
|
// Soft resync terminal: reset WASM grid and replay full output buffer.
|
|
@@ -4558,12 +4944,15 @@
|
|
|
4558
4944
|
function softResyncTerminal() {
|
|
4559
4945
|
if (!state.terminal || !state.terminalOutput) return false;
|
|
4560
4946
|
resetTerminal();
|
|
4561
|
-
state.terminal
|
|
4947
|
+
wandTerminalWrite(state.terminal, state.terminalOutput);
|
|
4562
4948
|
state.lastTerminalResyncAt = Date.now();
|
|
4563
4949
|
maybeScrollTerminalToBottom("output");
|
|
4564
4950
|
// Remeasure against real container: the refresh button used to only
|
|
4565
4951
|
// reset+write, so a stale cols/rows (set at mount time with hidden
|
|
4566
4952
|
// container) would survive the refresh and keep wrapping output wrong.
|
|
4953
|
+
// Suppress the auto-replay branch in ensureTerminalFit — we just
|
|
4954
|
+
// replayed, no point doing it again on the next rAF tick.
|
|
4955
|
+
state.suppressFitReplay = true;
|
|
4567
4956
|
ensureTerminalFit("refresh");
|
|
4568
4957
|
return true;
|
|
4569
4958
|
}
|
|
@@ -4619,7 +5008,7 @@
|
|
|
4619
5008
|
if (normalizedOutput !== currentOutput) {
|
|
4620
5009
|
resetTerminal();
|
|
4621
5010
|
if (normalizedOutput) {
|
|
4622
|
-
state.terminal
|
|
5011
|
+
wandTerminalWrite(state.terminal, normalizedOutput);
|
|
4623
5012
|
}
|
|
4624
5013
|
wrote = true;
|
|
4625
5014
|
}
|
|
@@ -4630,7 +5019,7 @@
|
|
|
4630
5019
|
} else if (normalizedOutput.startsWith(currentOutput)) {
|
|
4631
5020
|
var delta = normalizedOutput.slice(currentOutput.length);
|
|
4632
5021
|
if (delta) {
|
|
4633
|
-
state.terminal
|
|
5022
|
+
wandTerminalWrite(state.terminal, delta);
|
|
4634
5023
|
wrote = true;
|
|
4635
5024
|
}
|
|
4636
5025
|
} else if (currentOutput && currentOutput.startsWith(normalizedOutput)) {
|
|
@@ -4638,7 +5027,7 @@
|
|
|
4638
5027
|
} else {
|
|
4639
5028
|
resetTerminal();
|
|
4640
5029
|
if (normalizedOutput) {
|
|
4641
|
-
state.terminal
|
|
5030
|
+
wandTerminalWrite(state.terminal, normalizedOutput);
|
|
4642
5031
|
}
|
|
4643
5032
|
wrote = true;
|
|
4644
5033
|
}
|
|
@@ -4685,10 +5074,32 @@
|
|
|
4685
5074
|
},
|
|
4686
5075
|
onResize: function(cols, rows) {
|
|
4687
5076
|
sendTerminalResize(cols, rows);
|
|
5077
|
+
// wterm 自身 ResizeObserver 在容器尺寸变化时主动调 resize(),
|
|
5078
|
+
// bridge.resize() 把 grid 按新 cols 重排,但 scrollback 仍是
|
|
5079
|
+
// 按旧 cols 写入 WASM 的、新 grid 又被清空到干净状态。wand
|
|
5080
|
+
// 这层缓存的 terminalOutput 才是完整原始字节流,必须按新
|
|
5081
|
+
// cols 重放一次,grid + scrollback 才会和实际历史对齐。
|
|
5082
|
+
// 同步立即重放——不要走 setTimeout(0):移动端 WebView 在
|
|
5083
|
+
// 前后台切换或键盘动画期间,macrotask 经常被推迟到 wterm
|
|
5084
|
+
// 的下一帧 render 之后,结果用户先看到一帧空 grid 才看到
|
|
5085
|
+
// replay 完的内容,体感上就是"刷新都没用、动一下窗口才好"。
|
|
5086
|
+
if (state.terminal && state.terminalOutput) {
|
|
5087
|
+
state.suppressFitReplay = true;
|
|
5088
|
+
softResyncTerminal();
|
|
5089
|
+
}
|
|
4688
5090
|
}
|
|
4689
5091
|
});
|
|
4690
5092
|
|
|
4691
|
-
|
|
5093
|
+
// Wait for the monospace webfont (if any) before init so the very first
|
|
5094
|
+
// _measureCharSize() inside wterm uses the final glyph metrics. Otherwise
|
|
5095
|
+
// the fallback font's narrower glyphs make wterm calculate too many cols,
|
|
5096
|
+
// and subsequent chunks render with broken wrapping until the user
|
|
5097
|
+
// triggers a resize. Cap the wait so a missing font never blocks startup.
|
|
5098
|
+
var fontsReady = (document.fonts && typeof document.fonts.ready === "object")
|
|
5099
|
+
? Promise.race([document.fonts.ready, new Promise(function(r) { setTimeout(r, 800); })])
|
|
5100
|
+
: Promise.resolve();
|
|
5101
|
+
|
|
5102
|
+
fontsReady.then(function() { return term.init(); }).then(function() {
|
|
4692
5103
|
state.terminal = term;
|
|
4693
5104
|
state.terminalInitializing = false;
|
|
4694
5105
|
applyTerminalScale();
|
|
@@ -4729,7 +5140,7 @@
|
|
|
4729
5140
|
syncTerminalBuffer(session.id, session.output || "", { mode: "append", scroll: false });
|
|
4730
5141
|
}
|
|
4731
5142
|
} else {
|
|
4732
|
-
term
|
|
5143
|
+
wandTerminalWrite(term, "点击上方「新对话」开始你的第一次对话。\r\n");
|
|
4733
5144
|
}
|
|
4734
5145
|
|
|
4735
5146
|
state.terminalClickHandler = focusInputBox;
|
|
@@ -4870,7 +5281,7 @@
|
|
|
4870
5281
|
return "会话已结束,无法继续发送";
|
|
4871
5282
|
}
|
|
4872
5283
|
return session && isStructuredSession(session) && session.structuredState && session.structuredState.inFlight
|
|
4873
|
-
? "
|
|
5284
|
+
? "思考中 · 发送新消息将中断当前回复"
|
|
4874
5285
|
: "输入消息...";
|
|
4875
5286
|
}
|
|
4876
5287
|
|
|
@@ -5499,12 +5910,17 @@
|
|
|
5499
5910
|
initTerminal();
|
|
5500
5911
|
}
|
|
5501
5912
|
|
|
5502
|
-
if (selectedSession
|
|
5503
|
-
syncTerminalBuffer(selectedSession.id, selectedSession.output || "", { mode: "append", scroll: false });
|
|
5504
|
-
} else if (!selectedSession) {
|
|
5913
|
+
if (!selectedSession) {
|
|
5505
5914
|
state.terminalSessionId = null;
|
|
5506
5915
|
state.terminalOutput = "";
|
|
5507
5916
|
}
|
|
5917
|
+
// 之前这里会用 selectedSession.output 再 syncTerminalBuffer 一次。
|
|
5918
|
+
// 但 updateShellChrome 在 updateSessionsList、status 推送、init
|
|
5919
|
+
// 等多个高频路径都会被调,每次都拿"可能不带 output 的 slim 快照"
|
|
5920
|
+
// 兜回来 sync 一遍:要么早返回浪费判断,要么 prefix 不匹配触发
|
|
5921
|
+
// reset+全量重写、把 alt-screen 中正在绘制的 Claude TUI 切走。
|
|
5922
|
+
// terminal 写入应当只走 chunk hot-path 与 ws init 这两条权威路径,
|
|
5923
|
+
// 这里不再插手,避免引入二次覆盖。
|
|
5508
5924
|
|
|
5509
5925
|
if (state.terminal && selectedSession && state.currentView === "terminal") {
|
|
5510
5926
|
maybeScrollTerminalToBottom("view");
|
|
@@ -6220,6 +6636,11 @@
|
|
|
6220
6636
|
var apkMessageEl = document.getElementById("android-apk-message");
|
|
6221
6637
|
var androidApk = data.androidApk || {};
|
|
6222
6638
|
var isInApk = !!_apkVersion;
|
|
6639
|
+
var hasApkInfo = isInApk || !!androidApk.github || !!androidApk.local;
|
|
6640
|
+
if (apkSection) {
|
|
6641
|
+
if (hasApkInfo) apkSection.classList.remove("hidden");
|
|
6642
|
+
else apkSection.classList.add("hidden");
|
|
6643
|
+
}
|
|
6223
6644
|
|
|
6224
6645
|
if (isInApk) {
|
|
6225
6646
|
// ── APK 内模式:显示当前版本 + 线上版本 + 本地版本 ──
|
|
@@ -6663,12 +7084,11 @@
|
|
|
6663
7084
|
// ── Notification Settings Helpers ──
|
|
6664
7085
|
|
|
6665
7086
|
function _updateAppIconSelection(activeIcon) {
|
|
6666
|
-
var opts = document.querySelectorAll(".app-icon-option");
|
|
7087
|
+
var opts = document.querySelectorAll(".settings-app-icon-option");
|
|
6667
7088
|
for (var i = 0; i < opts.length; i++) {
|
|
6668
|
-
var
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
}
|
|
7089
|
+
var isActive = opts[i].getAttribute("data-icon") === activeIcon;
|
|
7090
|
+
opts[i].classList.toggle("selected", isActive);
|
|
7091
|
+
opts[i].setAttribute("aria-pressed", isActive ? "true" : "false");
|
|
6672
7092
|
}
|
|
6673
7093
|
}
|
|
6674
7094
|
|
|
@@ -8027,29 +8447,25 @@
|
|
|
8027
8447
|
return Promise.resolve();
|
|
8028
8448
|
}
|
|
8029
8449
|
|
|
8030
|
-
var
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
}), userMsgs);
|
|
8050
|
-
updateInputHint("思考中…");
|
|
8051
|
-
renderChat(true);
|
|
8052
|
-
}
|
|
8450
|
+
var isInterrupting = !!(session.structuredState && session.structuredState.inFlight && session.status === "running");
|
|
8451
|
+
// Immediately render user message with thinking indicator
|
|
8452
|
+
var userTurn = { role: "user", content: [{ type: "text", text: input }] };
|
|
8453
|
+
var userMsgs = stripRenderOnlyStructuredMessages(Array.isArray(session.messages) ? session.messages.slice() : []);
|
|
8454
|
+
userMsgs.push(userTurn);
|
|
8455
|
+
var optimisticStructuredState = Object.assign({}, session.structuredState || {}, { inFlight: true });
|
|
8456
|
+
updateSessionSnapshot({
|
|
8457
|
+
id: session.id,
|
|
8458
|
+
status: "running",
|
|
8459
|
+
messages: userMsgs,
|
|
8460
|
+
structuredState: optimisticStructuredState,
|
|
8461
|
+
});
|
|
8462
|
+
state.currentMessages = buildMessagesForRender(Object.assign({}, session, {
|
|
8463
|
+
status: "running",
|
|
8464
|
+
messages: userMsgs,
|
|
8465
|
+
structuredState: optimisticStructuredState,
|
|
8466
|
+
}), userMsgs);
|
|
8467
|
+
updateInputHint("思考中…");
|
|
8468
|
+
renderChat(true);
|
|
8053
8469
|
|
|
8054
8470
|
if (inputBox) {
|
|
8055
8471
|
inputBox.value = "";
|
|
@@ -8066,17 +8482,21 @@
|
|
|
8066
8482
|
method: "POST",
|
|
8067
8483
|
headers: { "Content-Type": "application/json" },
|
|
8068
8484
|
credentials: "same-origin",
|
|
8069
|
-
body: JSON.stringify({ input: input })
|
|
8485
|
+
body: JSON.stringify({ input: input, interrupt: isInterrupting || undefined })
|
|
8486
|
+
})
|
|
8487
|
+
.then(function(res) {
|
|
8488
|
+
if (!res.ok) {
|
|
8489
|
+
return res.json().catch(function() { return { error: "请求失败" }; }).then(function(payload) {
|
|
8490
|
+
throw new Error((payload && payload.error) || "无法发送结构化消息。");
|
|
8491
|
+
});
|
|
8492
|
+
}
|
|
8493
|
+
return res.json();
|
|
8070
8494
|
})
|
|
8071
|
-
.then(function(res) { return res.json(); })
|
|
8072
8495
|
.then(function(snapshot) {
|
|
8073
8496
|
if (snapshot && snapshot.error) {
|
|
8074
8497
|
throw new Error(snapshot.error);
|
|
8075
8498
|
}
|
|
8076
8499
|
if (snapshot && snapshot.id) {
|
|
8077
|
-
// If a WS update has already bumped the queue epoch, the HTTP
|
|
8078
|
-
// response's queuedMessages is stale — drop it to avoid
|
|
8079
|
-
// re-introducing already-dequeued items.
|
|
8080
8500
|
if (state.queueEpoch > epochBeforePost && snapshot.queuedMessages) {
|
|
8081
8501
|
delete snapshot.queuedMessages;
|
|
8082
8502
|
}
|
|
@@ -8084,13 +8504,8 @@
|
|
|
8084
8504
|
var refreshedSession = state.sessions.find(function(s) { return s.id === snapshot.id; }) || snapshot;
|
|
8085
8505
|
state.currentMessages = buildMessagesForRender(refreshedSession, getPreferredMessages(refreshedSession, snapshot.output, false));
|
|
8086
8506
|
renderChat(true);
|
|
8087
|
-
if (
|
|
8088
|
-
|
|
8089
|
-
if (queuedCount > 0) {
|
|
8090
|
-
showToast("已排队(第 " + queuedCount + " 条),将在当前消息处理完成后自动发送。", "info");
|
|
8091
|
-
}
|
|
8092
|
-
} else {
|
|
8093
|
-
updateInputHint("Enter 发送 · Shift+Enter 换行");
|
|
8507
|
+
if (isInterrupting) {
|
|
8508
|
+
showToast("已中断上一条回复,正在处理新消息…", "info");
|
|
8094
8509
|
}
|
|
8095
8510
|
}
|
|
8096
8511
|
})
|
|
@@ -8774,6 +9189,12 @@
|
|
|
8774
9189
|
function flushPendingMessages() {
|
|
8775
9190
|
if (state.pendingMessages.length === 0) return;
|
|
8776
9191
|
|
|
9192
|
+
var selectedSession = getSelectedSession();
|
|
9193
|
+
if (isStructuredSession(selectedSession)) {
|
|
9194
|
+
state.pendingMessages = [];
|
|
9195
|
+
return;
|
|
9196
|
+
}
|
|
9197
|
+
|
|
8777
9198
|
// Send queued messages in order, bypassing the session-running check
|
|
8778
9199
|
// since our local state may be stale right after reconnect
|
|
8779
9200
|
var queue = state.pendingMessages.slice();
|
|
@@ -10019,15 +10440,30 @@
|
|
|
10019
10440
|
syncInputBoxScroll(inputBox);
|
|
10020
10441
|
}
|
|
10021
10442
|
|
|
10443
|
+
// Keyboard just opened — terminal viewport now shares space with
|
|
10444
|
+
// the keyboard; visible rows shrink even if cols stayed the same.
|
|
10445
|
+
// Without an immediate refit, any chunk arriving while the keyboard
|
|
10446
|
+
// animates in renders against the old grid and tears the screen.
|
|
10447
|
+
if (!keyboardOpen && isKeyboardOpen) {
|
|
10448
|
+
ensureTerminalFit("keyboard-open");
|
|
10449
|
+
}
|
|
10450
|
+
|
|
10022
10451
|
// Keyboard just closed — force terminal refit and scroll to bottom
|
|
10023
10452
|
// after a delay so the keyboard dismiss animation and layout settle.
|
|
10024
10453
|
if (keyboardOpen && !isKeyboardOpen) {
|
|
10025
10454
|
setTimeout(function() {
|
|
10026
|
-
ensureTerminalFit();
|
|
10455
|
+
ensureTerminalFit("keyboard-close");
|
|
10027
10456
|
maybeScrollTerminalToBottom("force");
|
|
10028
10457
|
}, 200);
|
|
10029
10458
|
}
|
|
10030
10459
|
|
|
10460
|
+
// visualViewport height changed without a keyboard transition —
|
|
10461
|
+
// covers iOS address-bar collapse/expand and split-screen drag.
|
|
10462
|
+
// Cheap to call: ensureTerminalFit early-exits if cols/rows stable.
|
|
10463
|
+
if (heightChanged && keyboardOpen === isKeyboardOpen) {
|
|
10464
|
+
ensureTerminalFit("viewport");
|
|
10465
|
+
}
|
|
10466
|
+
|
|
10031
10467
|
keyboardOpen = isKeyboardOpen;
|
|
10032
10468
|
lastHeight = vv.height;
|
|
10033
10469
|
}
|
|
@@ -10280,10 +10716,68 @@
|
|
|
10280
10716
|
// new output renders with broken wrapping (content visually piles at
|
|
10281
10717
|
// the top). Call this after any layout change that might have altered
|
|
10282
10718
|
// container geometry (mount, session switch, view switch, refresh).
|
|
10719
|
+
// Same as ensureTerminalFit, but if the container is currently 0×0
|
|
10720
|
+
// (typical right after Android WebView.onResume — the page hasn't
|
|
10721
|
+
// re-laid-out yet), keep retrying through requestAnimationFrame /
|
|
10722
|
+
// setTimeout up to ~5 frames. Each attempt forces a layout read
|
|
10723
|
+
// (offsetHeight) so the browser has to flush styles.
|
|
10724
|
+
// Without this, the very first ensureTerminalFit silently fails,
|
|
10725
|
+
// cols/rows stay at the pre-suspend values, and freshly arriving
|
|
10726
|
+
// PTY chunks wrap against the wrong width — that's exactly the
|
|
10727
|
+
// "content piles at the top after resuming the app" bug.
|
|
10728
|
+
function ensureTerminalFitWithRetry(reason) {
|
|
10729
|
+
if (!state.terminal) return;
|
|
10730
|
+
var attempts = 0;
|
|
10731
|
+
var maxAttempts = 8;
|
|
10732
|
+
function tryFit() {
|
|
10733
|
+
if (!state.terminal) return;
|
|
10734
|
+
var el = document.getElementById("output");
|
|
10735
|
+
if (el) {
|
|
10736
|
+
// Force a layout flush so offsetWidth reflects the post-resume
|
|
10737
|
+
// container size, not a stale 0 from the suspended frame.
|
|
10738
|
+
void el.offsetHeight;
|
|
10739
|
+
}
|
|
10740
|
+
if (el && el.offsetWidth > 0 && el.offsetHeight > 0) {
|
|
10741
|
+
ensureTerminalFit(reason);
|
|
10742
|
+
// After fit, force a buffer replay: even when cols didn't
|
|
10743
|
+
// change, the WASM grid state may be inconsistent after a
|
|
10744
|
+
// long suspend (DOM rows clipped, scroll position lost).
|
|
10745
|
+
// softResyncTerminal is cheap because terminalOutput is
|
|
10746
|
+
// already in memory.
|
|
10747
|
+
if (state.terminalOutput) {
|
|
10748
|
+
state.suppressFitReplay = true;
|
|
10749
|
+
softResyncTerminal();
|
|
10750
|
+
}
|
|
10751
|
+
return;
|
|
10752
|
+
}
|
|
10753
|
+
if (++attempts >= maxAttempts) return;
|
|
10754
|
+
// Mix rAF and timeout: some Android WebView versions skip rAF
|
|
10755
|
+
// during the first frame after resume, so falling back to a
|
|
10756
|
+
// 16ms timer guarantees forward progress.
|
|
10757
|
+
if (attempts <= 4) {
|
|
10758
|
+
requestAnimationFrame(tryFit);
|
|
10759
|
+
} else {
|
|
10760
|
+
setTimeout(tryFit, 32);
|
|
10761
|
+
}
|
|
10762
|
+
}
|
|
10763
|
+
tryFit();
|
|
10764
|
+
}
|
|
10765
|
+
|
|
10283
10766
|
function ensureTerminalFit(reason) {
|
|
10284
10767
|
if (!state.terminal) return false;
|
|
10285
10768
|
var el = document.getElementById("output");
|
|
10286
|
-
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0)
|
|
10769
|
+
if (!el || el.offsetWidth === 0 || el.offsetHeight === 0) {
|
|
10770
|
+
// 容器暂时没有可见尺寸(hidden、动画过渡、键盘弹起前的 layout
|
|
10771
|
+
// 中间帧、Android WebView resume 头几帧),不要静默放弃——
|
|
10772
|
+
// 改成丢给 ensureTerminalFitWithRetry 兜底,等容器有了真实
|
|
10773
|
+
// 尺寸再 fit + replay。否则一旦错过这一次,只能等下一次外部
|
|
10774
|
+
// 触发(旋转屏幕、开关键盘等),中间的输出就会一直按错误
|
|
10775
|
+
// 宽度堆在视图上方,看起来像"中间一大段都没有显示"。
|
|
10776
|
+
ensureTerminalFitWithRetry(reason || "fit-retry");
|
|
10777
|
+
return false;
|
|
10778
|
+
}
|
|
10779
|
+
var prevCols = state.terminal.cols;
|
|
10780
|
+
var prevRows = state.terminal.rows;
|
|
10287
10781
|
requestAnimationFrame(function() {
|
|
10288
10782
|
requestAnimationFrame(function() {
|
|
10289
10783
|
if (!state.terminal) return;
|
|
@@ -10291,6 +10785,21 @@
|
|
|
10291
10785
|
state.terminal.remeasure();
|
|
10292
10786
|
}
|
|
10293
10787
|
sendTerminalResize(state.terminal.cols, state.terminal.rows);
|
|
10788
|
+
// Cache the container width that produced this cols/rows so the
|
|
10789
|
+
// hot-path chunk writer can detect drift cheaply (avoids running
|
|
10790
|
+
// a full remeasure on every WebSocket message).
|
|
10791
|
+
state.lastFitContainerWidth = el.offsetWidth;
|
|
10792
|
+
state.lastFitContainerHeight = el.offsetHeight;
|
|
10793
|
+
// If cols actually changed, the previously written buffer was
|
|
10794
|
+
// wrapped to the old width. Replay the full buffer so historical
|
|
10795
|
+
// lines and any in-flight CSI cursor sequences re-render against
|
|
10796
|
+
// the new grid — this is what fixes the "torn" screens users see
|
|
10797
|
+
// after rotating, opening the keyboard, or resizing the panel.
|
|
10798
|
+
var skipReplay = state.suppressFitReplay === true;
|
|
10799
|
+
state.suppressFitReplay = false;
|
|
10800
|
+
if (!skipReplay && (state.terminal.cols !== prevCols || state.terminal.rows !== prevRows)) {
|
|
10801
|
+
if (state.terminalOutput) softResyncTerminal();
|
|
10802
|
+
}
|
|
10294
10803
|
if (state.terminalAutoFollow || isTerminalNearBottom()) {
|
|
10295
10804
|
maybeScrollTerminalToBottom("resize");
|
|
10296
10805
|
}
|
|
@@ -10338,7 +10847,54 @@
|
|
|
10338
10847
|
if (timeEls.length > 0) scheduleSessionListUpdate();
|
|
10339
10848
|
}, 30000);
|
|
10340
10849
|
|
|
10341
|
-
function
|
|
10850
|
+
function cancelWsReconnect() {
|
|
10851
|
+
if (state.wsReconnectTimer) {
|
|
10852
|
+
clearTimeout(state.wsReconnectTimer);
|
|
10853
|
+
state.wsReconnectTimer = null;
|
|
10854
|
+
}
|
|
10855
|
+
}
|
|
10856
|
+
|
|
10857
|
+
// Drop any in-flight socket and start a new one *now* — used by the
|
|
10858
|
+
// Android resume bridge to recover from zombie connections (socket
|
|
10859
|
+
// still says OPEN, but the TCP path was torn down by Doze). Skips
|
|
10860
|
+
// the backoff timer; the caller has already decided this is urgent.
|
|
10861
|
+
function forceReconnectWebSocket(reason) {
|
|
10862
|
+
cancelWsReconnect();
|
|
10863
|
+
if (state.ws) {
|
|
10864
|
+
var stale = state.ws;
|
|
10865
|
+
// Detach handlers so the imminent close doesn't trigger another
|
|
10866
|
+
// reconnect path while we're already starting a fresh one.
|
|
10867
|
+
try { stale.onclose = null; } catch (e) { /* ignore */ }
|
|
10868
|
+
try { stale.onerror = null; } catch (e) { /* ignore */ }
|
|
10869
|
+
try { stale.close(); } catch (e) { /* ignore */ }
|
|
10870
|
+
state.ws = null;
|
|
10871
|
+
}
|
|
10872
|
+
state.wsConnected = false;
|
|
10873
|
+
state.wsReconnectAttempts = 0;
|
|
10874
|
+
initWebSocket(reason);
|
|
10875
|
+
}
|
|
10876
|
+
|
|
10877
|
+
function scheduleWsReconnect() {
|
|
10878
|
+
if (state.wsReconnectTimer) return;
|
|
10879
|
+
// Don't burn battery reconnecting while hidden — the resume
|
|
10880
|
+
// listener will kick a fresh connect when we're foreground.
|
|
10881
|
+
if (document.hidden) return;
|
|
10882
|
+
var attempt = state.wsReconnectAttempts || 0;
|
|
10883
|
+
// 0.5s, 1s, 2s, 4s, then capped at 8s. Faster than the old
|
|
10884
|
+
// fixed 2s on the first retry (matters for transient blips)
|
|
10885
|
+
// and bounded so a flapping server doesn't get hammered.
|
|
10886
|
+
var delays = [500, 1000, 2000, 4000, 8000];
|
|
10887
|
+
var delay = delays[attempt < delays.length ? attempt : delays.length - 1];
|
|
10888
|
+
state.wsReconnectAttempts = attempt + 1;
|
|
10889
|
+
state.wsReconnectTimer = setTimeout(function() {
|
|
10890
|
+
state.wsReconnectTimer = null;
|
|
10891
|
+
if (state.config && !state.ws && !document.hidden) {
|
|
10892
|
+
initWebSocket("backoff");
|
|
10893
|
+
}
|
|
10894
|
+
}, delay);
|
|
10895
|
+
}
|
|
10896
|
+
|
|
10897
|
+
function initWebSocket(reason) {
|
|
10342
10898
|
if (!window.WebSocket) return false;
|
|
10343
10899
|
|
|
10344
10900
|
// Prevent duplicate connections
|
|
@@ -10356,6 +10912,10 @@
|
|
|
10356
10912
|
ws.onopen = function() {
|
|
10357
10913
|
state.ws = ws;
|
|
10358
10914
|
state.wsConnected = true;
|
|
10915
|
+
// Reset backoff on a successful connect so the next disconnect
|
|
10916
|
+
// starts the ladder from 500ms again.
|
|
10917
|
+
state.wsReconnectAttempts = 0;
|
|
10918
|
+
cancelWsReconnect();
|
|
10359
10919
|
// Subscribe to current session if any
|
|
10360
10920
|
subscribeToSession(state.selectedId);
|
|
10361
10921
|
// Flush pending messages after reconnection
|
|
@@ -10363,7 +10923,10 @@
|
|
|
10363
10923
|
// Re-fit terminal on reconnect — the viewport may have changed
|
|
10364
10924
|
// while disconnected, so remeasure against real container size
|
|
10365
10925
|
// rather than sending stale cols/rows from before the disconnect.
|
|
10366
|
-
|
|
10926
|
+
// Use the retry variant: when the reconnect is triggered by
|
|
10927
|
+
// Android resume, the WebView container may still be 0×0 for
|
|
10928
|
+
// the first 1–2 frames while layout settles.
|
|
10929
|
+
ensureTerminalFitWithRetry("ws-reconnect");
|
|
10367
10930
|
};
|
|
10368
10931
|
|
|
10369
10932
|
ws.onmessage = function(event) {
|
|
@@ -10378,12 +10941,7 @@
|
|
|
10378
10941
|
ws.onclose = function() {
|
|
10379
10942
|
state.ws = null;
|
|
10380
10943
|
state.wsConnected = false;
|
|
10381
|
-
|
|
10382
|
-
setTimeout(function() {
|
|
10383
|
-
if (state.config && !state.ws) {
|
|
10384
|
-
initWebSocket();
|
|
10385
|
-
}
|
|
10386
|
-
}, 2000);
|
|
10944
|
+
scheduleWsReconnect();
|
|
10387
10945
|
};
|
|
10388
10946
|
|
|
10389
10947
|
ws.onerror = function() {
|
|
@@ -10392,6 +10950,9 @@
|
|
|
10392
10950
|
|
|
10393
10951
|
return true;
|
|
10394
10952
|
} catch (e) {
|
|
10953
|
+
// Constructor threw (rare — bad URL, blocked scheme). Try again
|
|
10954
|
+
// through the backoff path so we don't get stuck.
|
|
10955
|
+
scheduleWsReconnect();
|
|
10395
10956
|
return false;
|
|
10396
10957
|
}
|
|
10397
10958
|
}
|
|
@@ -10478,7 +11039,13 @@
|
|
|
10478
11039
|
// Fast path: write chunk directly to avoid full-output comparison.
|
|
10479
11040
|
state.lastChunkAt = Date.now();
|
|
10480
11041
|
state.terminalLiveStreamSessions[msg.sessionId] = true;
|
|
10481
|
-
|
|
11042
|
+
// 不再在 hot-path 调 maybeRefitTerminal/remeasure。它会偷偷把
|
|
11043
|
+
// wterm 的 this.cols 改成新值,让 wterm 自己的 ResizeObserver
|
|
11044
|
+
// 误判 newCols === this.cols 而跳过 wterm.resize() —— 那条路径
|
|
11045
|
+
// 才会真正调 Renderer.setup() 重建 DOM 行。绕过它就让容器尺寸
|
|
11046
|
+
// 变化的视觉错位无法被自愈,直到用户手动改窗口才修。现在让
|
|
11047
|
+
// wterm 内部 ResizeObserver 独占 cols 跟踪职责。
|
|
11048
|
+
wandTerminalWrite(state.terminal, msg.data.chunk);
|
|
10482
11049
|
state.terminalSessionId = msg.sessionId;
|
|
10483
11050
|
if (msg.data.output) {
|
|
10484
11051
|
state.terminalOutput = normalizeTerminalOutput(msg.data.output);
|
|
@@ -10612,14 +11179,19 @@
|
|
|
10612
11179
|
renderChat(true);
|
|
10613
11180
|
updateTaskDisplay();
|
|
10614
11181
|
updateApprovalStats();
|
|
10615
|
-
|
|
10616
|
-
//
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
11182
|
+
// ws 重新订阅时拿到的是服务端 ring buffer 的最新窗口(最多
|
|
11183
|
+
// 120KB);客户端缓存的 terminalOutput 可能早于服务端窗口
|
|
11184
|
+
// 的起点。append 模式有 prefix 检查,prefix 不匹配就 reset+
|
|
11185
|
+
// 全量重写、全等就直接 return false——前者会把 alt-screen
|
|
11186
|
+
// 中的 Claude TUI 切走,后者会把"应该按真实 cols 重写"的
|
|
11187
|
+
// 机会跳过。改用 replace 强制 reset+按当前 cols 重写一次,
|
|
11188
|
+
// 这是订阅时唯一可信的全量基线。
|
|
11189
|
+
updateTerminalOutput(msg.data.output || "", msg.sessionId, "replace");
|
|
11190
|
+
// 紧接着等容器有真实尺寸再 fit + softResync:wterm 启动
|
|
11191
|
+
// 硬编码 cols=120,replace 写入也可能落在错的列宽上,
|
|
11192
|
+
// ResizeObserver 的回调是异步的,得用 fit-with-retry 兜
|
|
11193
|
+
// 一次,确保最终一定按真实宽度重排。
|
|
11194
|
+
ensureTerminalFitWithRetry("init");
|
|
10623
11195
|
}
|
|
10624
11196
|
break;
|
|
10625
11197
|
case 'usage':
|